aboutsummaryrefslogtreecommitdiff
path: root/src/ARMeilleure/Common
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/ARMeilleure/Common
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/ARMeilleure/Common')
-rw-r--r--src/ARMeilleure/Common/AddressTable.cs252
-rw-r--r--src/ARMeilleure/Common/Allocator.cs24
-rw-r--r--src/ARMeilleure/Common/ArenaAllocator.cs187
-rw-r--r--src/ARMeilleure/Common/BitMap.cs222
-rw-r--r--src/ARMeilleure/Common/BitUtils.cs57
-rw-r--r--src/ARMeilleure/Common/Counter.cs98
-rw-r--r--src/ARMeilleure/Common/EntryTable.cs188
-rw-r--r--src/ARMeilleure/Common/EnumUtils.cs12
-rw-r--r--src/ARMeilleure/Common/NativeAllocator.cs27
9 files changed, 1067 insertions, 0 deletions
diff --git a/src/ARMeilleure/Common/AddressTable.cs b/src/ARMeilleure/Common/AddressTable.cs
new file mode 100644
index 00000000..9db2d00d
--- /dev/null
+++ b/src/ARMeilleure/Common/AddressTable.cs
@@ -0,0 +1,252 @@
+using ARMeilleure.Diagnostics;
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ARMeilleure.Common
+{
+ /// <summary>
+ /// Represents a table of guest address to a value.
+ /// </summary>
+ /// <typeparam name="TEntry">Type of the value</typeparam>
+ unsafe class AddressTable<TEntry> : IDisposable where TEntry : unmanaged
+ {
+ /// <summary>
+ /// Represents a level in an <see cref="AddressTable{TEntry}"/>.
+ /// </summary>
+ public readonly struct Level
+ {
+ /// <summary>
+ /// Gets the index of the <see cref="Level"/> in the guest address.
+ /// </summary>
+ public int Index { get; }
+
+ /// <summary>
+ /// Gets the length of the <see cref="Level"/> in the guest address.
+ /// </summary>
+ public int Length { get; }
+
+ /// <summary>
+ /// Gets the mask which masks the bits used by the <see cref="Level"/>.
+ /// </summary>
+ public ulong Mask => ((1ul << Length) - 1) << Index;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Level"/> structure with the specified
+ /// <paramref name="index"/> and <paramref name="length"/>.
+ /// </summary>
+ /// <param name="index">Index of the <see cref="Level"/></param>
+ /// <param name="length">Length of the <see cref="Level"/></param>
+ public Level(int index, int length)
+ {
+ (Index, Length) = (index, length);
+ }
+
+ /// <summary>
+ /// Gets the value of the <see cref="Level"/> from the specified guest <paramref name="address"/>.
+ /// </summary>
+ /// <param name="address">Guest address</param>
+ /// <returns>Value of the <see cref="Level"/> from the specified guest <paramref name="address"/></returns>
+ public int GetValue(ulong address)
+ {
+ return (int)((address & Mask) >> Index);
+ }
+ }
+
+ private bool _disposed;
+ private TEntry** _table;
+ private readonly List<IntPtr> _pages;
+
+ /// <summary>
+ /// Gets the bits used by the <see cref="Levels"/> of the <see cref="AddressTable{TEntry}"/> instance.
+ /// </summary>
+ public ulong Mask { get; }
+
+ /// <summary>
+ /// Gets the <see cref="Level"/>s used by the <see cref="AddressTable{TEntry}"/> instance.
+ /// </summary>
+ public Level[] Levels { get; }
+
+ /// <summary>
+ /// Gets or sets the default fill value of newly created leaf pages.
+ /// </summary>
+ public TEntry Fill { get; set; }
+
+ /// <summary>
+ /// Gets the base address of the <see cref="EntryTable{TEntry}"/>.
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
+ public IntPtr Base
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ lock (_pages)
+ {
+ return (IntPtr)GetRootPage();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
+ /// <see cref="Level"/>.
+ /// </summary>
+ /// <exception cref="ArgumentNullException"><paramref name="levels"/> is null</exception>
+ /// <exception cref="ArgumentException">Length of <paramref name="levels"/> is less than 2</exception>
+ public AddressTable(Level[] levels)
+ {
+ ArgumentNullException.ThrowIfNull(levels);
+
+ if (levels.Length < 2)
+ {
+ throw new ArgumentException("Table must be at least 2 levels deep.", nameof(levels));
+ }
+
+ _pages = new List<IntPtr>(capacity: 16);
+
+ Levels = levels;
+ Mask = 0;
+
+ foreach (var level in Levels)
+ {
+ Mask |= level.Mask;
+ }
+ }
+
+ /// <summary>
+ /// Determines if the specified <paramref name="address"/> is in the range of the
+ /// <see cref="AddressTable{TEntry}"/>.
+ /// </summary>
+ /// <param name="address">Guest address</param>
+ /// <returns><see langword="true"/> if is valid; otherwise <see langword="false"/></returns>
+ public bool IsValid(ulong address)
+ {
+ return (address & ~Mask) == 0;
+ }
+
+ /// <summary>
+ /// Gets a reference to the value at the specified guest <paramref name="address"/>.
+ /// </summary>
+ /// <param name="address">Guest address</param>
+ /// <returns>Reference to the value at the specified guest <paramref name="address"/></returns>
+ /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
+ /// <exception cref="ArgumentException"><paramref name="address"/> is not mapped</exception>
+ public ref TEntry GetValue(ulong address)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ if (!IsValid(address))
+ {
+ throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
+ }
+
+ lock (_pages)
+ {
+ return ref GetPage(address)[Levels[^1].GetValue(address)];
+ }
+ }
+
+ /// <summary>
+ /// Gets the leaf page for the specified guest <paramref name="address"/>.
+ /// </summary>
+ /// <param name="address">Guest address</param>
+ /// <returns>Leaf page for the specified guest <paramref name="address"/></returns>
+ private TEntry* GetPage(ulong address)
+ {
+ TEntry** page = GetRootPage();
+
+ for (int i = 0; i < Levels.Length - 1; i++)
+ {
+ ref Level level = ref Levels[i];
+ ref TEntry* nextPage = ref page[level.GetValue(address)];
+
+ if (nextPage == null)
+ {
+ ref Level nextLevel = ref Levels[i + 1];
+
+ nextPage = i == Levels.Length - 2 ?
+ (TEntry*)Allocate(1 << nextLevel.Length, Fill, leaf: true) :
+ (TEntry*)Allocate(1 << nextLevel.Length, IntPtr.Zero, leaf: false);
+ }
+
+ page = (TEntry**)nextPage;
+ }
+
+ return (TEntry*)page;
+ }
+
+ /// <summary>
+ /// Lazily initialize and get the root page of the <see cref="AddressTable{TEntry}"/>.
+ /// </summary>
+ /// <returns>Root page of the <see cref="AddressTable{TEntry}"/></returns>
+ private TEntry** GetRootPage()
+ {
+ if (_table == null)
+ {
+ _table = (TEntry**)Allocate(1 << Levels[0].Length, fill: IntPtr.Zero, leaf: false);
+ }
+
+ return _table;
+ }
+
+ /// <summary>
+ /// Allocates a block of memory of the specified type and length.
+ /// </summary>
+ /// <typeparam name="T">Type of elements</typeparam>
+ /// <param name="length">Number of elements</param>
+ /// <param name="fill">Fill value</param>
+ /// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
+ /// <returns>Allocated block</returns>
+ private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
+ {
+ var size = sizeof(T) * length;
+ var page = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
+ var span = new Span<T>((void*)page, length);
+
+ span.Fill(fill);
+
+ _pages.Add(page);
+
+ TranslatorEventSource.Log.AddressTableAllocated(size, leaf);
+
+ return page;
+ }
+
+ /// <summary>
+ /// Releases all resources used by the <see cref="AddressTable{TEntry}"/> instance.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases all unmanaged and optionally managed resources used by the <see cref="AddressTable{TEntry}"/>
+ /// instance.
+ /// </summary>
+ /// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ foreach (var page in _pages)
+ {
+ Marshal.FreeHGlobal(page);
+ }
+
+ _disposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Frees resources used by the <see cref="AddressTable{TEntry}"/> instance.
+ /// </summary>
+ ~AddressTable()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/Allocator.cs b/src/ARMeilleure/Common/Allocator.cs
new file mode 100644
index 00000000..247a8e8b
--- /dev/null
+++ b/src/ARMeilleure/Common/Allocator.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace ARMeilleure.Common
+{
+ unsafe abstract class Allocator : IDisposable
+ {
+ public T* Allocate<T>(ulong count = 1) where T : unmanaged
+ {
+ return (T*)Allocate(count * (uint)sizeof(T));
+ }
+
+ public abstract void* Allocate(ulong size);
+
+ public abstract void Free(void* block);
+
+ protected virtual void Dispose(bool disposing) { }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/ArenaAllocator.cs b/src/ARMeilleure/Common/ArenaAllocator.cs
new file mode 100644
index 00000000..bce6794a
--- /dev/null
+++ b/src/ARMeilleure/Common/ArenaAllocator.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace ARMeilleure.Common
+{
+ unsafe sealed class ArenaAllocator : Allocator
+ {
+ private class PageInfo
+ {
+ public byte* Pointer;
+ public byte Unused;
+ public int UnusedCounter;
+ }
+
+ private int _lastReset;
+ private ulong _index;
+ private int _pageIndex;
+ private PageInfo _page;
+ private List<PageInfo> _pages;
+ private readonly ulong _pageSize;
+ private readonly uint _pageCount;
+ private readonly List<IntPtr> _extras;
+
+ public ArenaAllocator(uint pageSize, uint pageCount)
+ {
+ _lastReset = Environment.TickCount;
+
+ // Set _index to pageSize so that the first allocation goes through the slow path.
+ _index = pageSize;
+ _pageIndex = -1;
+
+ _page = null;
+ _pages = new List<PageInfo>();
+ _pageSize = pageSize;
+ _pageCount = pageCount;
+
+ _extras = new List<IntPtr>();
+ }
+
+ public Span<T> AllocateSpan<T>(ulong count) where T : unmanaged
+ {
+ return new Span<T>(Allocate<T>(count), (int)count);
+ }
+
+ public override void* Allocate(ulong size)
+ {
+ if (_index + size <= _pageSize)
+ {
+ byte* result = _page.Pointer + _index;
+
+ _index += size;
+
+ return result;
+ }
+
+ return AllocateSlow(size);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void* AllocateSlow(ulong size)
+ {
+ if (size > _pageSize)
+ {
+ void* extra = NativeAllocator.Instance.Allocate(size);
+
+ _extras.Add((IntPtr)extra);
+
+ return extra;
+ }
+
+ if (_index + size > _pageSize)
+ {
+ _index = 0;
+ _pageIndex++;
+ }
+
+ if (_pageIndex < _pages.Count)
+ {
+ _page = _pages[_pageIndex];
+ _page.Unused = 0;
+ }
+ else
+ {
+ _page = new PageInfo();
+ _page.Pointer = (byte*)NativeAllocator.Instance.Allocate(_pageSize);
+
+ _pages.Add(_page);
+ }
+
+ byte* result = _page.Pointer + _index;
+
+ _index += size;
+
+ return result;
+ }
+
+ public override void Free(void* block) { }
+
+ public void Reset()
+ {
+ _index = _pageSize;
+ _pageIndex = -1;
+ _page = null;
+
+ // Free excess pages that was allocated.
+ while (_pages.Count > _pageCount)
+ {
+ NativeAllocator.Instance.Free(_pages[_pages.Count - 1].Pointer);
+
+ _pages.RemoveAt(_pages.Count - 1);
+ }
+
+ // Free extra blocks that are not page-sized
+ foreach (IntPtr ptr in _extras)
+ {
+ NativeAllocator.Instance.Free((void*)ptr);
+ }
+
+ _extras.Clear();
+
+ // Free pooled pages that has not been used in a while. Remove pages at the back first, because we try to
+ // keep the pages at the front alive, since they're more likely to be hot and in the d-cache.
+ bool removing = true;
+
+ // If arena is used frequently, keep pages for longer. Otherwise keep pages for a shorter amount of time.
+ int now = Environment.TickCount;
+ int count = (now - _lastReset) switch {
+ >= 5000 => 0,
+ >= 2500 => 50,
+ >= 1000 => 100,
+ >= 10 => 1500,
+ _ => 5000
+ };
+
+ for (int i = _pages.Count - 1; i >= 0; i--)
+ {
+ PageInfo page = _pages[i];
+
+ if (page.Unused == 0)
+ {
+ page.UnusedCounter = 0;
+ }
+
+ page.UnusedCounter += page.Unused;
+ page.Unused = 1;
+
+ // If page not used after `count` resets, remove it.
+ if (removing && page.UnusedCounter >= count)
+ {
+ NativeAllocator.Instance.Free(page.Pointer);
+
+ _pages.RemoveAt(i);
+ }
+ else
+ {
+ removing = false;
+ }
+ }
+
+ _lastReset = now;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_pages != null)
+ {
+ foreach (PageInfo info in _pages)
+ {
+ NativeAllocator.Instance.Free(info.Pointer);
+ }
+
+ foreach (IntPtr ptr in _extras)
+ {
+ NativeAllocator.Instance.Free((void*)ptr);
+ }
+
+ _pages = null;
+ }
+ }
+
+ ~ArenaAllocator()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/BitMap.cs b/src/ARMeilleure/Common/BitMap.cs
new file mode 100644
index 00000000..27ef031f
--- /dev/null
+++ b/src/ARMeilleure/Common/BitMap.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace ARMeilleure.Common
+{
+ unsafe class BitMap : IEnumerable<int>, IDisposable
+ {
+ private const int IntSize = 64;
+ private const int IntMask = IntSize - 1;
+
+ private int _count;
+ private long* _masks;
+ private readonly Allocator _allocator;
+
+ public BitMap(Allocator allocator)
+ {
+ _allocator = allocator;
+ }
+
+ public BitMap(Allocator allocator, int capacity) : this(allocator)
+ {
+ EnsureCapacity(capacity);
+ }
+
+ public bool Set(int bit)
+ {
+ EnsureCapacity(bit + 1);
+
+ int wordIndex = bit / IntSize;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ if ((_masks[wordIndex] & wordMask) != 0)
+ {
+ return false;
+ }
+
+ _masks[wordIndex] |= wordMask;
+
+ return true;
+ }
+
+ public void Clear(int bit)
+ {
+ EnsureCapacity(bit + 1);
+
+ int wordIndex = bit / IntSize;
+ int wordBit = bit & IntMask;
+
+ long wordMask = 1L << wordBit;
+
+ _masks[wordIndex] &= ~wordMask;
+ }
+
+ public bool IsSet(int bit)
+ {
+ EnsureCapacity(bit + 1);
+
+ int wordIndex = bit / IntSize;
+ int wordBit = bit & IntMask;
+
+ return (_masks[wordIndex] & (1L << wordBit)) != 0;
+ }
+
+ public int FindFirstUnset()
+ {
+ for (int index = 0; index < _count; index++)
+ {
+ long mask = _masks[index];
+
+ if (mask != -1L)
+ {
+ return BitOperations.TrailingZeroCount(~mask) + index * IntSize;
+ }
+ }
+
+ return _count * IntSize;
+ }
+
+ public bool Set(BitMap map)
+ {
+ EnsureCapacity(map._count * IntSize);
+
+ bool modified = false;
+
+ for (int index = 0; index < _count; index++)
+ {
+ long newValue = _masks[index] | map._masks[index];
+
+ if (_masks[index] != newValue)
+ {
+ _masks[index] = newValue;
+
+ modified = true;
+ }
+ }
+
+ return modified;
+ }
+
+ public bool Clear(BitMap map)
+ {
+ EnsureCapacity(map._count * IntSize);
+
+ bool modified = false;
+
+ for (int index = 0; index < _count; index++)
+ {
+ long newValue = _masks[index] & ~map._masks[index];
+
+ if (_masks[index] != newValue)
+ {
+ _masks[index] = newValue;
+
+ modified = true;
+ }
+ }
+
+ return modified;
+ }
+
+ private void EnsureCapacity(int size)
+ {
+ int count = (size + IntMask) / IntSize;
+
+ if (count > _count)
+ {
+ var oldMask = _masks;
+ var oldSpan = new Span<long>(_masks, _count);
+
+ _masks = _allocator.Allocate<long>((uint)count);
+ _count = count;
+
+ var newSpan = new Span<long>(_masks, _count);
+
+ oldSpan.CopyTo(newSpan);
+ newSpan.Slice(oldSpan.Length).Clear();
+
+ _allocator.Free(oldMask);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_masks != null)
+ {
+ _allocator.Free(_masks);
+
+ _masks = null;
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ IEnumerator<int> IEnumerable<int>.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public Enumerator GetEnumerator()
+ {
+ return new Enumerator(this);
+ }
+
+ public struct Enumerator : IEnumerator<int>
+ {
+ private long _index;
+ private long _mask;
+ private int _bit;
+ private readonly BitMap _map;
+
+ public int Current => (int)_index * IntSize + _bit;
+ object IEnumerator.Current => Current;
+
+ public Enumerator(BitMap map)
+ {
+ _index = -1;
+ _mask = 0;
+ _bit = 0;
+ _map = map;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext()
+ {
+ if (_mask != 0)
+ {
+ _mask &= ~(1L << _bit);
+ }
+
+ // Manually hoist these loads, because RyuJIT does not.
+ long count = (uint)_map._count;
+ long* masks = _map._masks;
+
+ while (_mask == 0)
+ {
+ if (++_index >= count)
+ {
+ return false;
+ }
+
+ _mask = masks[_index];
+ }
+
+ _bit = BitOperations.TrailingZeroCount(_mask);
+
+ return true;
+ }
+
+ public void Reset() { }
+
+ public void Dispose() { }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/ARMeilleure/Common/BitUtils.cs b/src/ARMeilleure/Common/BitUtils.cs
new file mode 100644
index 00000000..e7697ff3
--- /dev/null
+++ b/src/ARMeilleure/Common/BitUtils.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Numerics;
+
+namespace ARMeilleure.Common
+{
+ static class BitUtils
+ {
+ private static ReadOnlySpan<sbyte> HbsNibbleLut => new sbyte[] { -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3 };
+
+ public static long FillWithOnes(int bits)
+ {
+ return bits == 64 ? -1L : (1L << bits) - 1;
+ }
+
+ public static int HighestBitSet(int value)
+ {
+ return 31 - BitOperations.LeadingZeroCount((uint)value);
+ }
+
+ public static int HighestBitSetNibble(int value)
+ {
+ return HbsNibbleLut[value];
+ }
+
+ public static long Replicate(long bits, int size)
+ {
+ long output = 0;
+
+ for (int bit = 0; bit < 64; bit += size)
+ {
+ output |= bits << bit;
+ }
+
+ return output;
+ }
+
+ public static int RotateRight(int bits, int shift, int size)
+ {
+ return (int)RotateRight((uint)bits, shift, size);
+ }
+
+ public static uint RotateRight(uint bits, int shift, int size)
+ {
+ return (bits >> shift) | (bits << (size - shift));
+ }
+
+ public static long RotateRight(long bits, int shift, int size)
+ {
+ return (long)RotateRight((ulong)bits, shift, size);
+ }
+
+ public static ulong RotateRight(ulong bits, int shift, int size)
+ {
+ return (bits >> shift) | (bits << (size - shift));
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/Counter.cs b/src/ARMeilleure/Common/Counter.cs
new file mode 100644
index 00000000..d7210d15
--- /dev/null
+++ b/src/ARMeilleure/Common/Counter.cs
@@ -0,0 +1,98 @@
+using System;
+
+namespace ARMeilleure.Common
+{
+ /// <summary>
+ /// Represents a numeric counter which can be used for instrumentation of compiled code.
+ /// </summary>
+ /// <typeparam name="T">Type of the counter</typeparam>
+ class Counter<T> : IDisposable where T : unmanaged
+ {
+ private bool _disposed;
+ /// <summary>
+ /// Index in the <see cref="EntryTable{T}"/>
+ /// </summary>
+ private readonly int _index;
+ private readonly EntryTable<T> _countTable;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Counter{T}"/> class from the specified
+ /// <see cref="EntryTable{T}"/> instance and index.
+ /// </summary>
+ /// <param name="countTable"><see cref="EntryTable{T}"/> instance</param>
+ /// <exception cref="ArgumentNullException"><paramref name="countTable"/> is <see langword="null"/></exception>
+ /// <exception cref="ArgumentException"><typeparamref name="T"/> is unsupported</exception>
+ public Counter(EntryTable<T> countTable)
+ {
+ if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) &&
+ typeof(T) != typeof(short) && typeof(T) != typeof(ushort) &&
+ typeof(T) != typeof(int) && typeof(T) != typeof(uint) &&
+ typeof(T) != typeof(long) && typeof(T) != typeof(ulong) &&
+ typeof(T) != typeof(nint) && typeof(T) != typeof(nuint) &&
+ typeof(T) != typeof(float) && typeof(T) != typeof(double))
+ {
+ throw new ArgumentException("Counter does not support the specified type.");
+ }
+
+ _countTable = countTable ?? throw new ArgumentNullException(nameof(countTable));
+ _index = countTable.Allocate();
+ }
+
+ /// <summary>
+ /// Gets a reference to the value of the counter.
+ /// </summary>
+ /// <exception cref="ObjectDisposedException"><see cref="Counter{T}"/> instance was disposed</exception>
+ /// <remarks>
+ /// This can refer to freed memory if the owning <see cref="EntryTable{TEntry}"/> is disposed.
+ /// </remarks>
+ public ref T Value
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ return ref _countTable.GetValue(_index);
+ }
+ }
+
+ /// <summary>
+ /// Releases all resources used by the <see cref="Counter{T}"/> instance.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases all unmanaged and optionally managed resources used by the <see cref="Counter{T}"/> instance.
+ /// </summary>
+ /// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resources</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ try
+ {
+ // The index into the EntryTable is essentially an unmanaged resource since we allocate and free the
+ // resource ourselves.
+ _countTable.Free(_index);
+ }
+ catch (ObjectDisposedException)
+ {
+ // Can happen because _countTable may be disposed before the Counter instance.
+ }
+
+ _disposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Frees resources used by the <see cref="Counter{T}"/> instance.
+ /// </summary>
+ ~Counter()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/EntryTable.cs b/src/ARMeilleure/Common/EntryTable.cs
new file mode 100644
index 00000000..6f205797
--- /dev/null
+++ b/src/ARMeilleure/Common/EntryTable.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace ARMeilleure.Common
+{
+ /// <summary>
+ /// Represents an expandable table of the type <typeparamref name="TEntry"/>, whose entries will remain at the same
+ /// address through out the table's lifetime.
+ /// </summary>
+ /// <typeparam name="TEntry">Type of the entry in the table</typeparam>
+ class EntryTable<TEntry> : IDisposable where TEntry : unmanaged
+ {
+ private bool _disposed;
+ private int _freeHint;
+ private readonly int _pageCapacity; // Number of entries per page.
+ private readonly int _pageLogCapacity;
+ private readonly Dictionary<int, IntPtr> _pages;
+ private readonly BitMap _allocated;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EntryTable{TEntry}"/> class with the desired page size in
+ /// bytes.
+ /// </summary>
+ /// <param name="pageSize">Desired page size in bytes</param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="pageSize"/> is less than 0</exception>
+ /// <exception cref="ArgumentException"><typeparamref name="TEntry"/>'s size is zero</exception>
+ /// <remarks>
+ /// The actual page size may be smaller or larger depending on the size of <typeparamref name="TEntry"/>.
+ /// </remarks>
+ public unsafe EntryTable(int pageSize = 4096)
+ {
+ if (pageSize < 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size cannot be negative.");
+ }
+
+ if (sizeof(TEntry) == 0)
+ {
+ throw new ArgumentException("Size of TEntry cannot be zero.");
+ }
+
+ _allocated = new BitMap(NativeAllocator.Instance);
+ _pages = new Dictionary<int, IntPtr>();
+ _pageLogCapacity = BitOperations.Log2((uint)(pageSize / sizeof(TEntry)));
+ _pageCapacity = 1 << _pageLogCapacity;
+ }
+
+ /// <summary>
+ /// Allocates an entry in the <see cref="EntryTable{TEntry}"/>.
+ /// </summary>
+ /// <returns>Index of entry allocated in the table</returns>
+ /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
+ public int Allocate()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ lock (_allocated)
+ {
+ if (_allocated.IsSet(_freeHint))
+ {
+ _freeHint = _allocated.FindFirstUnset();
+ }
+
+ int index = _freeHint++;
+ var page = GetPage(index);
+
+ _allocated.Set(index);
+
+ GetValue(page, index) = default;
+
+ return index;
+ }
+ }
+
+ /// <summary>
+ /// Frees the entry at the specified <paramref name="index"/>.
+ /// </summary>
+ /// <param name="index">Index of entry to free</param>
+ /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
+ public void Free(int index)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ lock (_allocated)
+ {
+ if (_allocated.IsSet(index))
+ {
+ _allocated.Clear(index);
+
+ _freeHint = index;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a reference to the entry at the specified allocated <paramref name="index"/>.
+ /// </summary>
+ /// <param name="index">Index of the entry</param>
+ /// <returns>Reference to the entry at the specified <paramref name="index"/></returns>
+ /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
+ /// <exception cref="ArgumentException">Entry at <paramref name="index"/> is not allocated</exception>
+ public ref TEntry GetValue(int index)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ lock (_allocated)
+ {
+ if (!_allocated.IsSet(index))
+ {
+ throw new ArgumentException("Entry at the specified index was not allocated", nameof(index));
+ }
+
+ var page = GetPage(index);
+
+ return ref GetValue(page, index);
+ }
+ }
+
+ /// <summary>
+ /// Gets a reference to the entry at using the specified <paramref name="index"/> from the specified
+ /// <paramref name="page"/>.
+ /// </summary>
+ /// <param name="page">Page to use</param>
+ /// <param name="index">Index to use</param>
+ /// <returns>Reference to the entry</returns>
+ private ref TEntry GetValue(Span<TEntry> page, int index)
+ {
+ return ref page[index & (_pageCapacity - 1)];
+ }
+
+ /// <summary>
+ /// Gets the page for the specified <see cref="index"/>.
+ /// </summary>
+ /// <param name="index">Index to use</param>
+ /// <returns>Page for the specified <see cref="index"/></returns>
+ private unsafe Span<TEntry> GetPage(int index)
+ {
+ var pageIndex = (int)((uint)(index & ~(_pageCapacity - 1)) >> _pageLogCapacity);
+
+ if (!_pages.TryGetValue(pageIndex, out IntPtr page))
+ {
+ page = (IntPtr)NativeAllocator.Instance.Allocate((uint)sizeof(TEntry) * (uint)_pageCapacity);
+
+ _pages.Add(pageIndex, page);
+ }
+
+ return new Span<TEntry>((void*)page, _pageCapacity);
+ }
+
+ /// <summary>
+ /// Releases all resources used by the <see cref="EntryTable{TEntry}"/> instance.
+ /// </summary>
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases all unmanaged and optionally managed resources used by the <see cref="EntryTable{TEntry}"/>
+ /// instance.
+ /// </summary>
+ /// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
+ protected unsafe virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ _allocated.Dispose();
+
+ foreach (var page in _pages.Values)
+ {
+ NativeAllocator.Instance.Free((void*)page);
+ }
+
+ _disposed = true;
+ }
+ }
+
+ /// <summary>
+ /// Frees resources used by the <see cref="EntryTable{TEntry}"/> instance.
+ /// </summary>
+ ~EntryTable()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/EnumUtils.cs b/src/ARMeilleure/Common/EnumUtils.cs
new file mode 100644
index 00000000..2a4aa645
--- /dev/null
+++ b/src/ARMeilleure/Common/EnumUtils.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace ARMeilleure.Common
+{
+ static class EnumUtils
+ {
+ public static int GetCount(Type enumType)
+ {
+ return Enum.GetNames(enumType).Length;
+ }
+ }
+}
diff --git a/src/ARMeilleure/Common/NativeAllocator.cs b/src/ARMeilleure/Common/NativeAllocator.cs
new file mode 100644
index 00000000..71c04a9b
--- /dev/null
+++ b/src/ARMeilleure/Common/NativeAllocator.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace ARMeilleure.Common
+{
+ unsafe sealed class NativeAllocator : Allocator
+ {
+ public static NativeAllocator Instance { get; } = new();
+
+ public override void* Allocate(ulong size)
+ {
+ void* result = (void*)Marshal.AllocHGlobal((IntPtr)size);
+
+ if (result == null)
+ {
+ throw new OutOfMemoryException();
+ }
+
+ return result;
+ }
+
+ public override void Free(void* block)
+ {
+ Marshal.FreeHGlobal((IntPtr)block);
+ }
+ }
+}