diff --git a/Ghost.ArcEntities/Archetype.cs b/Ghost.ArcEntities/Archetype.cs index 8c41a47..74bc207 100644 --- a/Ghost.ArcEntities/Archetype.cs +++ b/Ghost.ArcEntities/Archetype.cs @@ -5,17 +5,17 @@ using System.Runtime.CompilerServices; namespace Ghost.ArcEntities; -internal unsafe struct Archetype : IDisposable +public unsafe struct Archetype : IDisposable { + private UnsafeList _chunks; private UnsafeArray _offsets; - private UnsafeArray _componentIDs; private UnsafeArray _componentIDToOffset; private int _entityCapacity; private int _maxComponentID; - - private UnsafeList _chunks; + private int _entityIdsOffset; public int EntityCapacity => _entityCapacity; + public int ChunkCount => _chunks.Count; public Archetype(ReadOnlySpan components) { @@ -30,19 +30,26 @@ internal unsafe struct Archetype : IDisposable // Calculate total size per entity to get an initial capacity estimate var bytesPerEntity = entitySize; - _maxComponentID = 0; + var maxComponentID = 0; for (var i = 0; i < components.Length; i++) { 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; + _offsets = new UnsafeArray(components.Length, Allocator.Persistent); _componentIDToOffset = new UnsafeArray(_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) { @@ -52,13 +59,13 @@ internal unsafe struct Archetype : IDisposable currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1); currentOffset += _entityCapacity * entitySize; - var entityOffset = currentOffset; - var tempOffsets = new int[components.Length]; + _entityIdsOffset = currentOffset; + 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 align = components[i].alignment; + var size = sortedComponents[i].size; + var align = sortedComponents[i].alignment; currentOffset = (currentOffset + align - 1) & ~(align - 1); tempOffsets[i] = currentOffset; @@ -73,9 +80,10 @@ internal unsafe struct Archetype : IDisposable 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; @@ -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); - _chunks.Add(chunk); + return ref _chunks[index]; + } - 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)] public UnsafeArray GetComponentArray(int chunkIndex) where T : unmanaged { - var id = ComponentType.id; + var id = ComponentTypeID.value; if (id >= _componentIDToOffset.Count) { return default; diff --git a/Ghost.ArcEntities/Component.cs b/Ghost.ArcEntities/Component.cs index 58dc143..b28512c 100644 --- a/Ghost.ArcEntities/Component.cs +++ b/Ghost.ArcEntities/Component.cs @@ -5,34 +5,50 @@ using System.Runtime.CompilerServices; namespace Ghost.ArcEntities; -internal struct ComponentInfo +public struct ComponentInfo { public int size; public int alignment; public int id; } -internal static unsafe class ComponentType +internal static unsafe class ComponentTypeID where T : unmanaged { - public static readonly int Size = sizeof(T); - public static readonly int Alignment = (int)MemoryUtility.AlignOf(); - public static readonly int id = ComponentRegister.s_nextComponentTypeID++; - - public static ComponentInfo GetInfo() - { - return new ComponentInfo - { - size = Size, - alignment = Alignment, - id = id - }; - } + public static readonly int value = ComponentRegister.s_nextComponentTypeID++; } -internal class ComponentRegister +internal static class ComponentRegister { internal static int s_nextComponentTypeID = 0; + internal static List s_registeredComponents = new(); + + internal unsafe static int GetOrRegisterComponent() + where T : unmanaged + { + var typeId = ComponentTypeID.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(), + id = typeId + }; + } + + return typeId; + } + + internal static ComponentInfo GetComponentInfo(int typeId) + { + return s_registeredComponents[typeId]; + } } internal unsafe struct Chuck : IDisposable @@ -43,7 +59,12 @@ internal unsafe struct Chuck : IDisposable private int _count; private int _capacity; - public int Count => _count; + public int Count + { + get => _count; + set => _count = value; + } + public int Capacity => _capacity; public Chuck(int size, int capacity) diff --git a/Ghost.ArcEntities/EntityQuery.cs b/Ghost.ArcEntities/EntityQuery.cs new file mode 100644 index 0000000..12952ce --- /dev/null +++ b/Ghost.ArcEntities/EntityQuery.cs @@ -0,0 +1,60 @@ +namespace Ghost.ArcEntities; + +public unsafe class EntityQuery + 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 _cache = new(); + + public void AddMatchingArchetype(Archetype archetype) + { + // We look up the offsets ONCE when the archetype is registered + int off1 = archetype.GetOffset(ComponentTypeID.value); + int off2 = archetype.GetOffset(ComponentTypeID.value); + + _cache.Add(new ArchetypeCache + { + Archetype = archetype, + Offset1 = off1, + Offset2 = off2 + }); + } + + // The Optimized Iteration Loop + public void ForEach(delegate* 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]); + } + } + } + } +}