/ src / ARMeilleure / Common / EntryTable.cs
EntryTable.cs
  1  using System;
  2  using System.Collections.Generic;
  3  using System.Numerics;
  4  
  5  namespace ARMeilleure.Common
  6  {
  7      /// <summary>
  8      /// Represents an expandable table of the type <typeparamref name="TEntry"/>, whose entries will remain at the same
  9      /// address through out the table's lifetime.
 10      /// </summary>
 11      /// <typeparam name="TEntry">Type of the entry in the table</typeparam>
 12      class EntryTable<TEntry> : IDisposable where TEntry : unmanaged
 13      {
 14          private bool _disposed;
 15          private int _freeHint;
 16          private readonly int _pageCapacity; // Number of entries per page.
 17          private readonly int _pageLogCapacity;
 18          private readonly Dictionary<int, IntPtr> _pages;
 19          private readonly BitMap _allocated;
 20  
 21          /// <summary>
 22          /// Initializes a new instance of the <see cref="EntryTable{TEntry}"/> class with the desired page size in
 23          /// bytes.
 24          /// </summary>
 25          /// <param name="pageSize">Desired page size in bytes</param>
 26          /// <exception cref="ArgumentOutOfRangeException"><paramref name="pageSize"/> is less than 0</exception>
 27          /// <exception cref="ArgumentException"><typeparamref name="TEntry"/>'s size is zero</exception>
 28          /// <remarks>
 29          /// The actual page size may be smaller or larger depending on the size of <typeparamref name="TEntry"/>.
 30          /// </remarks>
 31          public unsafe EntryTable(int pageSize = 4096)
 32          {
 33              if (pageSize < 0)
 34              {
 35                  throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size cannot be negative.");
 36              }
 37  
 38              if (sizeof(TEntry) == 0)
 39              {
 40                  throw new ArgumentException("Size of TEntry cannot be zero.");
 41              }
 42  
 43              _allocated = new BitMap(NativeAllocator.Instance);
 44              _pages = new Dictionary<int, IntPtr>();
 45              _pageLogCapacity = BitOperations.Log2((uint)(pageSize / sizeof(TEntry)));
 46              _pageCapacity = 1 << _pageLogCapacity;
 47          }
 48  
 49          /// <summary>
 50          /// Allocates an entry in the <see cref="EntryTable{TEntry}"/>.
 51          /// </summary>
 52          /// <returns>Index of entry allocated in the table</returns>
 53          /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
 54          public int Allocate()
 55          {
 56              ObjectDisposedException.ThrowIf(_disposed, this);
 57  
 58              lock (_allocated)
 59              {
 60                  if (_allocated.IsSet(_freeHint))
 61                  {
 62                      _freeHint = _allocated.FindFirstUnset();
 63                  }
 64  
 65                  int index = _freeHint++;
 66                  var page = GetPage(index);
 67  
 68                  _allocated.Set(index);
 69  
 70                  GetValue(page, index) = default;
 71  
 72                  return index;
 73              }
 74          }
 75  
 76          /// <summary>
 77          /// Frees the entry at the specified <paramref name="index"/>.
 78          /// </summary>
 79          /// <param name="index">Index of entry to free</param>
 80          /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
 81          public void Free(int index)
 82          {
 83              ObjectDisposedException.ThrowIf(_disposed, this);
 84  
 85              lock (_allocated)
 86              {
 87                  if (_allocated.IsSet(index))
 88                  {
 89                      _allocated.Clear(index);
 90  
 91                      _freeHint = index;
 92                  }
 93              }
 94          }
 95  
 96          /// <summary>
 97          /// Gets a reference to the entry at the specified allocated <paramref name="index"/>.
 98          /// </summary>
 99          /// <param name="index">Index of the entry</param>
100          /// <returns>Reference to the entry at the specified <paramref name="index"/></returns>
101          /// <exception cref="ObjectDisposedException"><see cref="EntryTable{TEntry}"/> instance was disposed</exception>
102          /// <exception cref="ArgumentException">Entry at <paramref name="index"/> is not allocated</exception>
103          public ref TEntry GetValue(int index)
104          {
105              ObjectDisposedException.ThrowIf(_disposed, this);
106  
107              lock (_allocated)
108              {
109                  if (!_allocated.IsSet(index))
110                  {
111                      throw new ArgumentException("Entry at the specified index was not allocated", nameof(index));
112                  }
113  
114                  var page = GetPage(index);
115  
116                  return ref GetValue(page, index);
117              }
118          }
119  
120          /// <summary>
121          /// Gets a reference to the entry at using the specified <paramref name="index"/> from the specified
122          /// <paramref name="page"/>.
123          /// </summary>
124          /// <param name="page">Page to use</param>
125          /// <param name="index">Index to use</param>
126          /// <returns>Reference to the entry</returns>
127          private ref TEntry GetValue(Span<TEntry> page, int index)
128          {
129              return ref page[index & (_pageCapacity - 1)];
130          }
131  
132          /// <summary>
133          /// Gets the page for the specified <see cref="index"/>.
134          /// </summary>
135          /// <param name="index">Index to use</param>
136          /// <returns>Page for the specified <see cref="index"/></returns>
137          private unsafe Span<TEntry> GetPage(int index)
138          {
139              var pageIndex = (int)((uint)(index & ~(_pageCapacity - 1)) >> _pageLogCapacity);
140  
141              if (!_pages.TryGetValue(pageIndex, out IntPtr page))
142              {
143                  page = (IntPtr)NativeAllocator.Instance.Allocate((uint)sizeof(TEntry) * (uint)_pageCapacity);
144  
145                  _pages.Add(pageIndex, page);
146              }
147  
148              return new Span<TEntry>((void*)page, _pageCapacity);
149          }
150  
151          /// <summary>
152          /// Releases all resources used by the <see cref="EntryTable{TEntry}"/> instance.
153          /// </summary>
154          public void Dispose()
155          {
156              Dispose(true);
157              GC.SuppressFinalize(this);
158          }
159  
160          /// <summary>
161          /// Releases all unmanaged and optionally managed resources used by the <see cref="EntryTable{TEntry}"/>
162          /// instance.
163          /// </summary>
164          /// <param name="disposing"><see langword="true"/> to dispose managed resources also; otherwise just unmanaged resouces</param>
165          protected unsafe virtual void Dispose(bool disposing)
166          {
167              if (!_disposed)
168              {
169                  _allocated.Dispose();
170  
171                  foreach (var page in _pages.Values)
172                  {
173                      NativeAllocator.Instance.Free((void*)page);
174                  }
175  
176                  _disposed = true;
177              }
178          }
179  
180          /// <summary>
181          /// Frees resources used by the <see cref="EntryTable{TEntry}"/> instance.
182          /// </summary>
183          ~EntryTable()
184          {
185              Dispose(false);
186          }
187      }
188  }