using UnityEngine;
namespace Unity.Netcode
{
internal struct IndexAllocatorEntry
{
internal int Pos; // Position where the memory of this slot is
internal int Length; // Length of the memory allocated to this slot
internal int Next; // Next and Prev define the order of the slots in the buffer
internal int Prev;
internal bool Free; // Whether this is a free slot
}
internal class IndexAllocator
{
private const int k_NotSet = -1;
private readonly int m_MaxSlot; // Maximum number of sections (free or not) in the buffer
private readonly int m_BufferSize; // Size of the buffer we allocated into
private int m_LastSlot = 0; // Last allocated slot
private IndexAllocatorEntry[] m_Slots; // Array of slots
private int[] m_IndexToSlot; // Mapping from the client's index to the slot index
internal IndexAllocator(int bufferSize, int maxSlot)
{
m_MaxSlot = maxSlot;
m_BufferSize = bufferSize;
m_Slots = new IndexAllocatorEntry[m_MaxSlot];
m_IndexToSlot = new int[m_MaxSlot];
Reset();
}
///
/// Reset this IndexAllocator to an empty one, with the same sized buffer and slots
///
internal void Reset()
{
// todo: could be made faster, for example by having a last index
// and not needing valid stuff past it
for (int i = 0; i < m_MaxSlot; i++)
{
m_Slots[i].Free = true;
m_Slots[i].Next = i + 1;
m_Slots[i].Prev = i - 1;
m_Slots[i].Pos = m_BufferSize;
m_Slots[i].Length = 0;
m_IndexToSlot[i] = k_NotSet;
}
m_Slots[0].Pos = 0;
m_Slots[0].Length = m_BufferSize;
m_Slots[0].Prev = k_NotSet;
m_Slots[m_MaxSlot - 1].Next = k_NotSet;
}
///
/// Returns the amount of memory used
///
///
/// Returns the amount of memory used, starting at 0, ending after the last used slot
///
internal int Range
{
get
{
// when the whole buffer is free, m_LastSlot points to an empty slot
if (m_Slots[m_LastSlot].Free)
{
return 0;
}
// otherwise return the end of the last slot used
return m_Slots[m_LastSlot].Pos + m_Slots[m_LastSlot].Length;
}
}
///
/// Allocate a slot with "size" position, for index "index"
///
/// The client index to identify this. Used in Deallocate to identify which slot
/// The size required.
/// Returns the position to use in the buffer
///
/// true if successful, false is there isn't enough memory available or no slots are large enough
///
internal bool Allocate(int index, int size, out int pos)
{
pos = 0;
// size must be positive, index must be within range
if (size < 0 || index < 0 || index >= m_MaxSlot)
{
return false;
}
// refuse allocation if the index is already in use
if (m_IndexToSlot[index] != k_NotSet)
{
return false;
}
// todo: this is the slowest part
// improvement 1: list of free blocks (minor)
// improvement 2: heap of free blocks
for (int i = 0; i < m_MaxSlot; i++)
{
if (m_Slots[i].Free && m_Slots[i].Length >= size)
{
m_IndexToSlot[index] = i;
int leftOver = m_Slots[i].Length - size;
int next = m_Slots[i].Next;
if (m_Slots[next].Free)
{
m_Slots[next].Pos -= leftOver;
m_Slots[next].Length += leftOver;
}
else
{
int add = MoveSlotAfter(i);
m_Slots[add].Pos = m_Slots[i].Pos + size;
m_Slots[add].Length = m_Slots[i].Length - size;
}
m_Slots[i].Free = false;
m_Slots[i].Length = size;
pos = m_Slots[i].Pos;
// if we allocate past the current range, we are the last slot
if (m_Slots[i].Pos + m_Slots[i].Length > Range)
{
m_LastSlot = i;
}
return true;
}
}
return false;
}
///
/// Deallocate a slot
///
/// The client index to identify this. Same index used in Allocate
///
/// true if successful, false is there isn't an allocated slot at this index
///
internal bool Deallocate(int index)
{
// size must be positive, index must be within range
if (index < 0 || index >= m_MaxSlot)
{
return false;
}
int slot = m_IndexToSlot[index];
if (slot == k_NotSet)
{
return false;
}
if (m_Slots[slot].Free)
{
return false;
}
m_Slots[slot].Free = true;
int prev = m_Slots[slot].Prev;
int next = m_Slots[slot].Next;
// if previous slot was free, merge and grow
if (prev != k_NotSet && m_Slots[prev].Free)
{
m_Slots[prev].Length += m_Slots[slot].Length;
m_Slots[slot].Length = 0;
// if the slot we're merging was the last one, the last one is now the one we merged with
if (slot == m_LastSlot)
{
m_LastSlot = prev;
}
// todo: verify what this does on full or nearly full cases
MoveSlotToEnd(slot);
slot = prev;
}
next = m_Slots[slot].Next;
// merge with next slot if it is free
if (next != k_NotSet && m_Slots[next].Free)
{
m_Slots[slot].Length += m_Slots[next].Length;
m_Slots[next].Length = 0;
MoveSlotToEnd(next);
}
// if we just deallocate the last one, we need to move last back
if (slot == m_LastSlot)
{
m_LastSlot = m_Slots[m_LastSlot].Prev;
// if there's nothing allocated anymore, use 0
if (m_LastSlot == k_NotSet)
{
m_LastSlot = 0;
}
}
// mark the index as available
m_IndexToSlot[index] = k_NotSet;
return true;
}
// Take a slot at the end and link it to go just after "slot".
// Used when allocating part of a slot and we need an entry for the rest
// Returns the slot that was picked
private int MoveSlotAfter(int slot)
{
int ret = m_Slots[m_MaxSlot - 1].Prev;
int p0 = m_Slots[ret].Prev;
m_Slots[p0].Next = m_MaxSlot - 1;
m_Slots[m_MaxSlot - 1].Prev = p0;
int p1 = m_Slots[slot].Next;
m_Slots[slot].Next = ret;
m_Slots[p1].Prev = ret;
m_Slots[ret].Prev = slot;
m_Slots[ret].Next = p1;
return ret;
}
// Move the slot "slot" to the end of the list.
// Used when merging two slots, that gives us an extra entry at the end
private void MoveSlotToEnd(int slot)
{
// if we're already there
if (m_Slots[slot].Next == k_NotSet)
{
return;
}
int prev = m_Slots[slot].Prev;
int next = m_Slots[slot].Next;
m_Slots[prev].Next = next;
if (next != k_NotSet)
{
m_Slots[next].Prev = prev;
}
int p0 = m_Slots[m_MaxSlot - 1].Prev;
m_Slots[p0].Next = slot;
m_Slots[slot].Next = m_MaxSlot - 1;
m_Slots[m_MaxSlot - 1].Prev = slot;
m_Slots[slot].Prev = p0;
m_Slots[slot].Pos = m_BufferSize;
}
// runs a bunch of consistency check on the Allocator
internal bool Verify()
{
int pos = k_NotSet;
int count = 0;
int total = 0;
int endPos = 0;
do
{
int prev = pos;
if (pos != k_NotSet)
{
pos = m_Slots[pos].Next;
if (pos == k_NotSet)
{
break;
}
}
else
{
pos = 0;
}
if (m_Slots[pos].Prev != prev)
{
// the previous is not correct
return false;
}
if (m_Slots[pos].Length < 0)
{
// Length should be positive
return false;
}
if (prev != k_NotSet && m_Slots[prev].Free && m_Slots[pos].Free && m_Slots[pos].Length > 0)
{
// should not have two consecutive free slots
return false;
}
if (m_Slots[pos].Pos != total)
{
// slots should all line up nicely
return false;
}
if (!m_Slots[pos].Free)
{
endPos = m_Slots[pos].Pos + m_Slots[pos].Length;
}
total += m_Slots[pos].Length;
count++;
} while (pos != k_NotSet);
if (count != m_MaxSlot)
{
// some slots were lost
return false;
}
if (total != m_BufferSize)
{
// total buffer should be accounted for
return false;
}
if (endPos != Range)
{
// end position should match reported end position
return false;
}
return true;
}
// Debug display the allocator structure
internal void DebugDisplay()
{
string logMessage = "IndexAllocator structure\n";
bool[] seen = new bool[m_MaxSlot];
int pos = 0;
int count = 0;
bool prevEmpty = false;
do
{
seen[pos] = true;
count++;
if (m_Slots[pos].Length == 0 && prevEmpty)
{
// don't display repetitive empty slots
}
else
{
logMessage += string.Format("{0}:{1}, {2} ({3}) \n", m_Slots[pos].Pos, m_Slots[pos].Length,
m_Slots[pos].Free ? "Free" : "Used", pos);
if (m_Slots[pos].Length == 0)
{
prevEmpty = true;
}
else
{
prevEmpty = false;
}
}
pos = m_Slots[pos].Next;
} while (pos != k_NotSet && !seen[pos]);
logMessage += string.Format("{0} Total entries\n", count);
Debug.Log(logMessage);
}
}
}