Updated Archetype

This commit is contained in:
2025-12-02 17:41:48 +09:00
parent 95cb9af16f
commit 63a70f1a74
3 changed files with 167 additions and 37 deletions

View File

@@ -5,17 +5,17 @@ using System.Runtime.CompilerServices;
namespace Ghost.ArcEntities; namespace Ghost.ArcEntities;
internal unsafe struct Archetype : IDisposable public unsafe struct Archetype : IDisposable
{ {
private UnsafeList<Chuck> _chunks;
private UnsafeArray<int> _offsets; private UnsafeArray<int> _offsets;
private UnsafeArray<int> _componentIDs;
private UnsafeArray<int> _componentIDToOffset; private UnsafeArray<int> _componentIDToOffset;
private int _entityCapacity; private int _entityCapacity;
private int _maxComponentID; private int _maxComponentID;
private int _entityIdsOffset;
private UnsafeList<Chuck> _chunks;
public int EntityCapacity => _entityCapacity; public int EntityCapacity => _entityCapacity;
public int ChunkCount => _chunks.Count;
public Archetype(ReadOnlySpan<ComponentInfo> components) public Archetype(ReadOnlySpan<ComponentInfo> components)
{ {
@@ -30,19 +30,26 @@ internal unsafe struct Archetype : IDisposable
// Calculate total size per entity to get an initial capacity estimate // Calculate total size per entity to get an initial capacity estimate
var bytesPerEntity = entitySize; var bytesPerEntity = entitySize;
_maxComponentID = 0; var maxComponentID = 0;
for (var i = 0; i < components.Length; i++) for (var i = 0; i < components.Length; i++)
{ {
bytesPerEntity += components[i].size; bytesPerEntity += components[i].size;
if (components[i].id > _maxComponentID) if (components[i].id > maxComponentID)
{ {
_maxComponentID = components[i].id; maxComponentID = components[i].id;
} }
} }
_maxComponentID = maxComponentID;
_entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity; _entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity;
_offsets = new UnsafeArray<int>(components.Length, Allocator.Persistent);
_componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent); _componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent);
for (var i = 0; i < _componentIDToOffset.Count; ++i) _componentIDToOffset[i] = -1;
_componentIDToOffset.AsSpan().Fill(-1);
var sortedComponents = new ComponentInfo[components.Length];
components.CopyTo(sortedComponents);
Array.Sort(sortedComponents, (a, b) => b.alignment.CompareTo(a.alignment));
while (_entityCapacity > 0) while (_entityCapacity > 0)
{ {
@@ -52,13 +59,13 @@ internal unsafe struct Archetype : IDisposable
currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1); currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1);
currentOffset += _entityCapacity * entitySize; currentOffset += _entityCapacity * entitySize;
var entityOffset = currentOffset; _entityIdsOffset = currentOffset;
var tempOffsets = new int[components.Length]; var tempOffsets = stackalloc int[components.Length];
for (var i = 0; i < components.Length; i++) for (var i = 0; i < sortedComponents.Length; i++)
{ {
var size = components[i].size; var size = sortedComponents[i].size;
var align = components[i].alignment; var align = sortedComponents[i].alignment;
currentOffset = (currentOffset + align - 1) & ~(align - 1); currentOffset = (currentOffset + align - 1) & ~(align - 1);
tempOffsets[i] = currentOffset; tempOffsets[i] = currentOffset;
@@ -73,9 +80,10 @@ internal unsafe struct Archetype : IDisposable
if (fits) if (fits)
{ {
for (var i = 0; i < components.Length; i++) for (var i = 0; i < sortedComponents.Length; i++)
{ {
_componentIDToOffset[components[i].id] = tempOffsets[i]; _offsets[i] = tempOffsets[i];
_componentIDToOffset[sortedComponents[i].id] = tempOffsets[i];
} }
return; return;
@@ -85,19 +93,60 @@ internal unsafe struct Archetype : IDisposable
} }
} }
public Chuck CreateChunk() internal ref Chuck GetChunkReference(int index)
{ {
var chunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity); return ref _chunks[index];
_chunks.Add(chunk); }
return chunk; [MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetOffset(int componentId)
{
if (componentId >= _componentIDToOffset.Count)
{
return -1;
}
return _componentIDToOffset[componentId];
}
public void RemoveEntity(int chunkIndex, int rowIndex)
{
var chunk = _chunks[chunkIndex];
int lastIndex = chunk.Count - 1;
// 1. If we are NOT removing the very last entity, we must swap.
if (rowIndex != lastIndex)
{
// A. We are moving the 'last' entity into the 'row' spot.
// We need to know WHO that last entity is so we can update the lookup map.
var pLastEntity = chunk.GetUnsafePtr() + _entityIdsOffset + (sizeof(Entity) * lastIndex);
var lastEntity = *(Entity*)pLastEntity;
// B. Now we can update the map
// World.UpdateLocation(lastEntity.ID, newIndex: rowIndex);
// C. Perform the memory copy (Swap components)
for (var i = 0; i <= _offsets.Count; i++)
{
var offset = _offsets[i];
var compSize = ComponentRegister.GetComponentInfo(i).size;
var pRow = chunk.GetUnsafePtr() + offset + (compSize * rowIndex);
var pLast = chunk.GetUnsafePtr() + offset + (compSize * lastIndex);
MemoryUtility.MemCpy(pLast, pRow, (nuint)compSize);
}
}
chunk.Count--;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public UnsafeArray<T> GetComponentArray<T>(int chunkIndex) public UnsafeArray<T> GetComponentArray<T>(int chunkIndex)
where T : unmanaged where T : unmanaged
{ {
var id = ComponentType<T>.id; var id = ComponentTypeID<T>.value;
if (id >= _componentIDToOffset.Count) if (id >= _componentIDToOffset.Count)
{ {
return default; return default;

View File

@@ -5,34 +5,50 @@ using System.Runtime.CompilerServices;
namespace Ghost.ArcEntities; namespace Ghost.ArcEntities;
internal struct ComponentInfo public struct ComponentInfo
{ {
public int size; public int size;
public int alignment; public int alignment;
public int id; public int id;
} }
internal static unsafe class ComponentType<T> internal static unsafe class ComponentTypeID<T>
where T : unmanaged where T : unmanaged
{ {
public static readonly int Size = sizeof(T); public static readonly int value = ComponentRegister.s_nextComponentTypeID++;
public static readonly int Alignment = (int)MemoryUtility.AlignOf<T>();
public static readonly int id = ComponentRegister.s_nextComponentTypeID++;
public static ComponentInfo GetInfo()
{
return new ComponentInfo
{
size = Size,
alignment = Alignment,
id = id
};
}
} }
internal class ComponentRegister internal static class ComponentRegister
{ {
internal static int s_nextComponentTypeID = 0; internal static int s_nextComponentTypeID = 0;
internal static List<ComponentInfo> s_registeredComponents = new();
internal unsafe static int GetOrRegisterComponent<T>()
where T : unmanaged
{
var typeId = ComponentTypeID<T>.value;
while (s_registeredComponents.Count <= typeId)
{
s_registeredComponents.Add(default);
}
if (s_registeredComponents[typeId].size == 0)
{
s_registeredComponents[typeId] = new ComponentInfo
{
size = sizeof(T),
alignment = (int)MemoryUtility.AlignOf<T>(),
id = typeId
};
}
return typeId;
}
internal static ComponentInfo GetComponentInfo(int typeId)
{
return s_registeredComponents[typeId];
}
} }
internal unsafe struct Chuck : IDisposable internal unsafe struct Chuck : IDisposable
@@ -43,7 +59,12 @@ internal unsafe struct Chuck : IDisposable
private int _count; private int _count;
private int _capacity; private int _capacity;
public int Count => _count; public int Count
{
get => _count;
set => _count = value;
}
public int Capacity => _capacity; public int Capacity => _capacity;
public Chuck(int size, int capacity) public Chuck(int size, int capacity)

View File

@@ -0,0 +1,60 @@
namespace Ghost.ArcEntities;
public unsafe class EntityQuery<T1, T2>
where T1 : unmanaged
where T2 : unmanaged
{
// The Cache Struct
struct ArchetypeCache
{
public Archetype Archetype;
public int Offset1; // Offset for T1
public int Offset2; // Offset for T2
}
private List<ArchetypeCache> _cache = new();
public void AddMatchingArchetype(Archetype archetype)
{
// We look up the offsets ONCE when the archetype is registered
int off1 = archetype.GetOffset(ComponentTypeID<T1>.value);
int off2 = archetype.GetOffset(ComponentTypeID<T2>.value);
_cache.Add(new ArchetypeCache
{
Archetype = archetype,
Offset1 = off1,
Offset2 = off2
});
}
// The Optimized Iteration Loop
public void ForEach(delegate*<ref T1, ref T2, void> action)
{
foreach (var cache in _cache)
{
var archetype = cache.Archetype;
var offset1 = cache.Offset1;
var offset2 = cache.Offset2;
// Iterate Chunks
for (int i = 0; i < archetype.ChunkCount; i++)
{
var chunk = archetype.GetChunkReference(i);
var chunkPtr = (byte*)chunk.GetUnsafePtr();
var count = chunk.Count;
// POINTER MATH ONLY - NO LOOKUPS
// We use the pre-calculated integer offsets
T1* ptr1 = (T1*)(chunkPtr + offset1);
T2* ptr2 = (T2*)(chunkPtr + offset2);
// The hot loop
for (int k = 0; k < count; k++)
{
action(ref ptr1[k], ref ptr2[k]);
}
}
}
}
}