diff --git a/Ghost.ArcEntities/Archetype.cs b/Ghost.ArcEntities/Archetype.cs new file mode 100644 index 0000000..8c41a47 --- /dev/null +++ b/Ghost.ArcEntities/Archetype.cs @@ -0,0 +1,127 @@ +using Misaki.HighPerformance.LowLevel.Buffer; +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; +using System.Runtime.CompilerServices; + +namespace Ghost.ArcEntities; + +internal unsafe struct Archetype : IDisposable +{ + private UnsafeArray _offsets; + private UnsafeArray _componentIDs; + private UnsafeArray _componentIDToOffset; + private int _entityCapacity; + private int _maxComponentID; + + private UnsafeList _chunks; + + public int EntityCapacity => _entityCapacity; + + public Archetype(ReadOnlySpan components) + { + _chunks = new UnsafeList(4, Allocator.Persistent); + CalculateLayout(components); + } + + private void CalculateLayout(ReadOnlySpan components) + { + var entitySize = sizeof(Entity); + var entityAlign = (int)MemoryUtility.AlignOf(); + + // Calculate total size per entity to get an initial capacity estimate + var bytesPerEntity = entitySize; + _maxComponentID = 0; + for (var i = 0; i < components.Length; i++) + { + bytesPerEntity += components[i].size; + if (components[i].id > _maxComponentID) + { + _maxComponentID = components[i].id; + } + } + + _entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity; + _componentIDToOffset = new UnsafeArray(_maxComponentID + 1, Allocator.Persistent); + for (var i = 0; i < _componentIDToOffset.Count; ++i) _componentIDToOffset[i] = -1; + + while (_entityCapacity > 0) + { + var currentOffset = 0; + var fits = true; + + currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1); + currentOffset += _entityCapacity * entitySize; + + var entityOffset = currentOffset; + var tempOffsets = new int[components.Length]; + + for (var i = 0; i < components.Length; i++) + { + var size = components[i].size; + var align = components[i].alignment; + + currentOffset = (currentOffset + align - 1) & ~(align - 1); + tempOffsets[i] = currentOffset; + currentOffset += _entityCapacity * size; + + if (currentOffset > Chuck.CHUNK_SIZE) + { + fits = false; + break; + } + } + + if (fits) + { + for (var i = 0; i < components.Length; i++) + { + _componentIDToOffset[components[i].id] = tempOffsets[i]; + } + + return; + } + + _entityCapacity--; + } + } + + public Chuck CreateChunk() + { + var chunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity); + _chunks.Add(chunk); + + return chunk; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public UnsafeArray GetComponentArray(int chunkIndex) + where T : unmanaged + { + var id = ComponentType.id; + if (id >= _componentIDToOffset.Count) + { + return default; + } + + var offset = _componentIDToOffset[id]; + if (offset == -1) + { + return default; + } + + var chunk = _chunks[chunkIndex]; + return new UnsafeArray((T*)((byte*)chunk.GetUnsafePtr() + offset), _entityCapacity); + } + + public void Dispose() + { + _componentIDToOffset.Dispose(); + + foreach (var chunk in _chunks) + { + chunk.Dispose(); + } + + _chunks.Dispose(); + } +} diff --git a/Ghost.ArcEntities/AssemblyInfo.cs b/Ghost.ArcEntities/AssemblyInfo.cs new file mode 100644 index 0000000..07c22ff --- /dev/null +++ b/Ghost.ArcEntities/AssemblyInfo.cs @@ -0,0 +1,4 @@ +global using EntityID = System.Int32; +global using GenerationID = System.UInt32; +global using WorldID = System.UInt16; + diff --git a/Ghost.ArcEntities/Class1.cs b/Ghost.ArcEntities/Class1.cs deleted file mode 100644 index 65bb0a8..0000000 --- a/Ghost.ArcEntities/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ghost.ArcEntities; - -public class Class1 -{ - -} diff --git a/Ghost.ArcEntities/Component.cs b/Ghost.ArcEntities/Component.cs new file mode 100644 index 0000000..58dc143 --- /dev/null +++ b/Ghost.ArcEntities/Component.cs @@ -0,0 +1,66 @@ +using Misaki.HighPerformance.LowLevel.Collections; +using Misaki.HighPerformance.LowLevel.Utilities; +using Misaki.HighPerformance.LowLevel.Buffer; +using System.Runtime.CompilerServices; + +namespace Ghost.ArcEntities; + +internal struct ComponentInfo +{ + public int size; + public int alignment; + public int id; +} + +internal static unsafe class ComponentType + 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 + }; + } +} + +internal class ComponentRegister +{ + internal static int s_nextComponentTypeID = 0; +} + +internal unsafe struct Chuck : IDisposable +{ + public const int CHUNK_SIZE = 16384; // 16 KB + + private UnsafeArray _data; + private int _count; + private int _capacity; + + public int Count => _count; + public int Capacity => _capacity; + + public Chuck(int size, int capacity) + { + _data = new UnsafeArray(size, Allocator.Persistent); + _capacity = capacity; + _count = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte* GetUnsafePtr() + { + return (byte*)_data.GetUnsafePtr(); + } + + public void Dispose() + { + _data.Dispose(); + } +} diff --git a/Ghost.ArcEntities/Entity.cs b/Ghost.ArcEntities/Entity.cs new file mode 100644 index 0000000..3f0e134 --- /dev/null +++ b/Ghost.ArcEntities/Entity.cs @@ -0,0 +1,81 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ghost.ArcEntities; + +[StructLayout(LayoutKind.Sequential, Size = 8)] +public struct Entity : IEquatable, IComparable +{ + public const EntityID INVALID_ID = -1; + + private EntityID _id; + private GenerationID _generation; + + public readonly EntityID ID + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _id; + } + + public readonly GenerationID Generation + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _generation; + } + + public readonly bool IsValid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ID != INVALID_ID; + } + + public static Entity Invalid + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(INVALID_ID, GenerationID.MaxValue); + } + + internal Entity(EntityID id, GenerationID generation) + { + _id = id; + _generation = generation; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void IncrementGeneration() => _generation++; + + public readonly bool Equals(Entity other) + { + return _id == other._id && _generation == other._generation; + } + + public readonly int CompareTo(Entity other) + { + return _id.CompareTo(other._id); + } + + public override readonly bool Equals(object? obj) + { + return obj is Entity other && Equals(other); + } + + public override readonly int GetHashCode() + { + return _id.GetHashCode(); + } + + public static bool operator ==(Entity left, Entity right) + { + return left.Equals(right); + } + + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } + + public override readonly string ToString() + { + return $"Entity {{ Index: {ID}, Generation: {Generation} }}"; + } +} diff --git a/Ghost.ArcEntities/Ghost.ArcEntities.csproj b/Ghost.ArcEntities/Ghost.ArcEntities.csproj index b760144..e73b545 100644 --- a/Ghost.ArcEntities/Ghost.ArcEntities.csproj +++ b/Ghost.ArcEntities/Ghost.ArcEntities.csproj @@ -4,6 +4,11 @@ net10.0 enable enable + true + + + + diff --git a/Ghost.ArcEntities/World.cs b/Ghost.ArcEntities/World.cs new file mode 100644 index 0000000..98d0ee8 --- /dev/null +++ b/Ghost.ArcEntities/World.cs @@ -0,0 +1,109 @@ +using System.Runtime.CompilerServices; + +namespace Ghost.ArcEntities; + +public partial class World +{ + private static List s_worlds = new(4); + private static Queue s_freeWorldSlots = new(); + + public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count; + + public static World Create(int entityCapacity = 16) + { + lock (s_worlds) + { + if (s_freeWorldSlots.TryDequeue(out var index)) + { + s_worlds[index] = new World(index, entityCapacity); + } + else + { + if (s_worlds.Count >= WorldID.MaxValue) + { + throw new InvalidOperationException("Maximum number of worlds reached"); + } + + index = (WorldID)s_worlds.Count; + s_worlds.Add(new World(index, entityCapacity)); + } + + return s_worlds[index]; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static World GetWorld(int index) + { + return s_worlds[index]; + } +} + +public partial class World : IDisposable, IEquatable +{ + private readonly WorldID _id; + + private bool _isDisposed = false; + + public WorldID ID => _id; + + private World(WorldID id, int entityCapacity) + { + _id = id; + } + + ~World() + { + Dispose(); + } + + public bool Equals(World? other) + { + if (other is null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return _id == other._id; + } + + public override int GetHashCode() + { + return _id.GetHashCode(); + } + + public override bool Equals(object? obj) + { + return obj is World other && Equals(other); + } + + public static bool operator ==(World? left, World? right) + { + return left?.Equals(right) ?? right is null; + } + + public static bool operator !=(World? left, World? right) + { + return !(left == right); + } + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + s_freeWorldSlots.Enqueue(_id); + + _isDisposed = true; + + GC.SuppressFinalize(this); + } +} +