feature/archetype-ecs #1

Merged
Misaki merged 23 commits from feature/archetype-ecs into develop 2025-12-17 08:27:32 +00:00
8 changed files with 614 additions and 124 deletions
Showing only changes of commit 948fae4401 - Show all commits

View File

@@ -1,3 +1,4 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
@@ -5,25 +6,120 @@ using System.Runtime.CompilerServices;
namespace Ghost.ArcEntities; namespace Ghost.ArcEntities;
public unsafe struct Archetype : IDisposable internal unsafe struct Chuck : IDisposable
{ {
public const int CHUNK_SIZE = 16384; // 16 KB
private UnsafeArray<byte> _data;
private int _count;
private int _capacity;
public int Count
{
get => _count;
set => _count = value;
}
public int Capacity => _capacity;
public Chuck(int size, int capacity)
{
_data = new UnsafeArray<byte>(size, Allocator.Persistent);
_capacity = capacity;
_count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePtr()
{
return (byte*)_data.GetUnsafePtr();
}
public void Dispose()
{
_data.Dispose();
}
}
internal struct Edge
{
public int componentID;
public Identifier<Archetype> targetArchetype;
}
internal unsafe struct Archetype : IIdentifierType, IDisposable
{
private struct ComponentMemoryLayout
{
public int offset;
public int size;
}
private readonly Identifier<Archetype> _id;
private readonly Identifier<World> _worldID;
private UnsafeBitSet _signature;
private UnsafeList<Chuck> _chunks; private UnsafeList<Chuck> _chunks;
private UnsafeArray<int> _offsets; private UnsafeArray<ComponentMemoryLayout> _layouts;
private UnsafeArray<int> _componentIDToOffset; private UnsafeArray<int> _componentIDToOffset;
private UnsafeList<Edge> _edgesAdd;
private UnsafeList<Edge> _edgesRemove;
private int _entityCapacity; private int _entityCapacity;
private int _maxComponentID; private int _maxComponentID;
private int _entityIdsOffset; private int _entityIdsOffset;
public UnsafeBitSet Signature => _signature;
public int EntityCapacity => _entityCapacity; public int EntityCapacity => _entityCapacity;
public int ChunkCount => _chunks.Count; public int ChunkCount => _chunks.Count;
public Archetype(ReadOnlySpan<ComponentInfo> components) public Archetype(Identifier<Archetype> id, Identifier<World> worldID, ReadOnlySpan<int> componentIds)
{ {
_id = id;
_worldID = worldID;
if (componentIds.IsEmpty)
{
_signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear);
_chunks = new UnsafeList<Chuck>(4, Allocator.Persistent); _chunks = new UnsafeList<Chuck>(4, Allocator.Persistent);
CalculateLayout(components);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
_signature.ClearAll();
_entityCapacity = Chuck.CHUNK_SIZE / sizeof(Entity);
return;
} }
private void CalculateLayout(ReadOnlySpan<ComponentInfo> components) var highestComponentID = 0;
for (var i = 0; i < componentIds.Length; i++)
{
if (componentIds[i] > highestComponentID)
{
highestComponentID = componentIds[i];
}
}
_signature = new UnsafeBitSet(highestComponentID, Allocator.Persistent, AllocationOption.Clear);
_chunks = new UnsafeList<Chuck>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
var pComponents = stackalloc ComponentInfo[componentIds.Length];
for (var i = 0; i < componentIds.Length; i++)
{
_signature.SetBit(componentIds[i]);
pComponents[i] = ComponentRegister.s_registeredComponents[componentIds[i]];
}
CalculateLayout(new Span<ComponentInfo>(pComponents, componentIds.Length));
}
private void CalculateLayout(Span<ComponentInfo> components)
{ {
var entitySize = sizeof(Entity); var entitySize = sizeof(Entity);
var entityAlign = (int)MemoryUtility.AlignOf<Entity>(); var entityAlign = (int)MemoryUtility.AlignOf<Entity>();
@@ -42,14 +138,12 @@ public unsafe struct Archetype : IDisposable
_maxComponentID = maxComponentID; _maxComponentID = maxComponentID;
_entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity; _entityCapacity = Chuck.CHUNK_SIZE / bytesPerEntity;
_offsets = new UnsafeArray<int>(components.Length, Allocator.Persistent); _layouts = new UnsafeArray<ComponentMemoryLayout>(components.Length, Allocator.Persistent);
_componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent); _componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent);
_componentIDToOffset.AsSpan().Fill(-1); _componentIDToOffset.AsSpan().Fill(-1);
var sortedComponents = new ComponentInfo[components.Length]; components.Sort((a, b) => b.alignment.CompareTo(a.alignment));
components.CopyTo(sortedComponents);
Array.Sort(sortedComponents, (a, b) => b.alignment.CompareTo(a.alignment));
while (_entityCapacity > 0) while (_entityCapacity > 0)
{ {
@@ -57,15 +151,16 @@ public unsafe struct Archetype : IDisposable
var fits = true; var fits = true;
currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1); currentOffset = (currentOffset + entityAlign - 1) & ~(entityAlign - 1);
currentOffset += _entityCapacity * entitySize;
_entityIdsOffset = currentOffset; _entityIdsOffset = currentOffset;
currentOffset += _entityCapacity * entitySize;
var tempOffsets = stackalloc int[components.Length]; var tempOffsets = stackalloc int[components.Length];
for (var i = 0; i < sortedComponents.Length; i++) for (var i = 0; i < components.Length; i++)
{ {
var size = sortedComponents[i].size; var size = components[i].size;
var align = sortedComponents[i].alignment; var align = components[i].alignment;
currentOffset = (currentOffset + align - 1) & ~(align - 1); currentOffset = (currentOffset + align - 1) & ~(align - 1);
tempOffsets[i] = currentOffset; tempOffsets[i] = currentOffset;
@@ -80,10 +175,15 @@ public unsafe struct Archetype : IDisposable
if (fits) if (fits)
{ {
for (var i = 0; i < sortedComponents.Length; i++) for (var i = 0; i < components.Length; i++)
{ {
_offsets[i] = tempOffsets[i]; _layouts[i] = new ComponentMemoryLayout
_componentIDToOffset[sortedComponents[i].id] = tempOffsets[i]; {
offset = tempOffsets[i],
size = components[i].size
};
_componentIDToOffset[components[i].id] = tempOffsets[i];
} }
return; return;
@@ -93,7 +193,42 @@ public unsafe struct Archetype : IDisposable
} }
} }
internal ref Chuck GetChunkReference(int index) public void AllocateEntity(out int chunkIndex, out int rowIndex)
{
for (var i = 0; i < _chunks.Count; i++)
{
var chunk = _chunks[i];
if (chunk.Count < _entityCapacity)
{
rowIndex = chunk.Count;
chunk.Count++;
chunkIndex = i;
return;
}
}
// Need to allocate a new chunk
var newChunk = new Chuck(Chuck.CHUNK_SIZE, _entityCapacity);
_chunks.Add(newChunk);
rowIndex = 0;
newChunk.Count++;
chunkIndex = _chunks.Count - 1;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetEntity(int chunkIndex, int rowIndex, Entity entity)
{
var chunk = _chunks[chunkIndex];
var chunkBase = chunk.GetUnsafePtr();
var pEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex);
MemoryUtility.MemCpy(&entity, pEntity, (nuint)sizeof(Entity));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref Chuck GetChunkReference(int index)
{ {
return ref _chunks[index]; return ref _chunks[index];
} }
@@ -109,37 +244,99 @@ public unsafe struct Archetype : IDisposable
return _componentIDToOffset[componentId]; return _componentIDToOffset[componentId];
} }
public void RemoveEntity(int chunkIndex, int rowIndex) public ResultStatus RemoveEntity(int chunkIndex, int rowIndex)
{ {
var chunk = _chunks[chunkIndex]; if (chunkIndex < 0 || chunkIndex >= _chunks.Count)
{
return ResultStatus.InvalidArgument;
}
ref var chunk = ref _chunks[chunkIndex];
int lastIndex = chunk.Count - 1; int lastIndex = chunk.Count - 1;
// 1. If we are NOT removing the very last entity, we must swap. // If we are NOT removing the very last entity, we must swap.
if (rowIndex != lastIndex) if (rowIndex != lastIndex)
{ {
// A. We are moving the 'last' entity into the 'row' spot. var chunkBase = chunk.GetUnsafePtr();
// We need to know WHO that last entity is so we can update the lookup map. var pLastEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * lastIndex);
var pRowEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex);
var pLastEntity = chunk.GetUnsafePtr() + _entityIdsOffset + (sizeof(Entity) * lastIndex); var wroldResult = World.GetWorld(_worldID);
var lastEntity = *(Entity*)pLastEntity; if (wroldResult.Status != ResultStatus.Success)
// 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]; return wroldResult.Status;
var compSize = ComponentRegister.GetComponentInfo(i).size; }
var pRow = chunk.GetUnsafePtr() + offset + (compSize * rowIndex); var result = wroldResult.Value.EntityManager.UpdateEntityLocation(*(Entity*)pLastEntity, _id, chunkIndex, rowIndex);
var pLast = chunk.GetUnsafePtr() + offset + (compSize * lastIndex); if (result != ResultStatus.Success)
{
return result;
}
MemoryUtility.MemCpy(pLast, pRow, (nuint)compSize); // Only operate the swap back after the update is succeed.
MemoryUtility.MemCpy(pLastEntity, pRowEntity, (nuint)sizeof(Entity));
for (var i = 0; i <= _layouts.Count; i++)
{
var layout = _layouts[i];
var pRow = chunk.GetUnsafePtr() + layout.offset + (layout.size * rowIndex);
var pLast = chunk.GetUnsafePtr() + layout.offset + (layout.size * lastIndex);
MemoryUtility.MemCpy(pLast, pRow, (nuint)layout.size);
} }
} }
chunk.Count--; chunk.Count--;
return ResultStatus.Success;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEdgeAdd(int componentID, Identifier<Archetype> targetArchetype)
{
_edgesAdd.Add(new Edge
{
componentID = componentID,
targetArchetype = targetArchetype
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result<int, ResultStatus> GetEdgeAdd(int componentID)
{
for (var i = 0; i < _edgesAdd.Count; i++)
{
if (_edgesAdd[i].componentID == componentID)
{
return Result.Create(_edgesAdd[i].targetArchetype.value, ResultStatus.Success);
}
}
return Result.Create(-1, ResultStatus.NotFound);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddEdgeRemove(int componentID, Identifier<Archetype> targetArchetype)
{
_edgesRemove.Add(new Edge
{
componentID = componentID,
targetArchetype = targetArchetype
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Result<int, ResultStatus> GetEdgeRemove(int componentID)
{
for (var i = 0; i < _edgesRemove.Count; i++)
{
if (_edgesRemove[i].componentID == componentID)
{
return Result.Create(_edgesRemove[i].targetArchetype.value, ResultStatus.Success);
}
}
return Result.Create(-1, ResultStatus.NotFound);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -164,13 +361,19 @@ public unsafe struct Archetype : IDisposable
public void Dispose() public void Dispose()
{ {
_componentIDToOffset.Dispose(); if (_chunks.IsCreated)
{
foreach (var chunk in _chunks) foreach (ref var chunk in _chunks)
{ {
chunk.Dispose(); chunk.Dispose();
} }
}
_signature.Dispose();
_chunks.Dispose(); _chunks.Dispose();
_componentIDToOffset.Dispose();
_layouts.Dispose();
_edgesAdd.Dispose();
_edgesRemove.Dispose();
} }
} }

View File

@@ -1,4 +1,3 @@
global using EntityID = System.Int32; global using EntityID = System.Int32;
global using GenerationID = System.UInt32; global using GenerationID = System.Int32;
global using WorldID = System.UInt16;

View File

@@ -1,12 +1,10 @@
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities; using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.CompilerServices;
namespace Ghost.ArcEntities; namespace Ghost.ArcEntities;
public struct ComponentInfo public struct ComponentInfo
{ {
// public FixedText64 stableName; // Do we actually need this?
public int size; public int size;
public int alignment; public int alignment;
public int id; public int id;
@@ -15,34 +13,48 @@ public struct ComponentInfo
internal static unsafe class ComponentTypeID<T> internal static unsafe class ComponentTypeID<T>
where T : unmanaged where T : unmanaged
{ {
public static readonly int value = ComponentRegister.s_nextComponentTypeID++; public static readonly int value = ComponentRegister.GetOrRegisterComponent<T>();
} }
internal static class ComponentRegister internal static class ComponentRegister
{ {
internal static int s_nextComponentTypeID = 0; private static int s_nextComponentTypeID = 0;
private static Dictionary<IntPtr, int> s_typeHandleToID = new();
internal static List<ComponentInfo> s_registeredComponents = new(); internal static List<ComponentInfo> s_registeredComponents = new();
internal static Dictionary<string, int> s_nameToRuntimeID = new();
internal unsafe static int GetOrRegisterComponent<T>() internal unsafe static int GetOrRegisterComponent<T>()
where T : unmanaged where T : unmanaged
{ {
var typeId = ComponentTypeID<T>.value; var typeHandle = typeof(T).TypeHandle.Value;
while (s_registeredComponents.Count <= typeId)
lock (s_registeredComponents)
{ {
s_registeredComponents.Add(default); if (s_typeHandleToID.TryGetValue(typeHandle, out int existingID))
{
return existingID;
} }
if (s_registeredComponents[typeId].size == 0) int newID = s_nextComponentTypeID++;
{ string stableName = typeof(T).FullName ?? typeof(T).Name;
s_registeredComponents[typeId] = new ComponentInfo
var info = new ComponentInfo
{ {
// stableName = new FixedText64(stableName),
size = sizeof(T), size = sizeof(T),
alignment = (int)MemoryUtility.AlignOf<T>(), alignment = (int)MemoryUtility.AlignOf<T>(),
id = typeId id = newID,
}; };
}
return typeId; while (s_registeredComponents.Count <= newID) s_registeredComponents.Add(default);
s_registeredComponents[newID] = info;
s_typeHandleToID[typeHandle] = newID;
s_nameToRuntimeID[stableName] = newID;
return newID;
}
} }
internal static ComponentInfo GetComponentInfo(int typeId) internal static ComponentInfo GetComponentInfo(int typeId)
@@ -50,38 +62,3 @@ internal static class ComponentRegister
return s_registeredComponents[typeId]; return s_registeredComponents[typeId];
} }
} }
internal unsafe struct Chuck : IDisposable
{
public const int CHUNK_SIZE = 16384; // 16 KB
private UnsafeArray<byte> _data;
private int _count;
private int _capacity;
public int Count
{
get => _count;
set => _count = value;
}
public int Capacity => _capacity;
public Chuck(int size, int capacity)
{
_data = new UnsafeArray<byte>(size, Allocator.Persistent);
_capacity = capacity;
_count = 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetUnsafePtr()
{
return (byte*)_data.GetUnsafePtr();
}
public void Dispose()
{
_data.Dispose();
}
}

View File

@@ -0,0 +1,110 @@
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
namespace Ghost.ArcEntities;
public unsafe class EntityCommandBuffer : IDisposable
{
private enum CommandType
{
CreateEntity,
DestroyEntity,
AddComponent,
RemoveComponent,
SetComponent,
}
private struct Command
{
public UnsafeArray<byte> data;
public CommandType type;
public Entity entity;
public int componentTypeID;
}
private readonly EntityManager _entityManager;
private UnsafeList<Command> _commands;
public EntityCommandBuffer(EntityManager entityManager)
{
_entityManager = entityManager;
_commands = new UnsafeList<Command>(32, Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
}
public void CreateEntity()
{
var command = new Command
{
type = CommandType.CreateEntity,
data = default,
entity = default,
componentTypeID = -1
};
_commands.Add(command);
}
public void DestroyEntity(Entity entity)
{
var command = new Command
{
type = CommandType.DestroyEntity,
data = default,
entity = entity,
componentTypeID = -1
};
_commands.Add(command);
}
public void AddComponent<T>(Entity entity, T component)
where T : unmanaged
{
var data = new UnsafeArray<byte>(sizeof(T), Misaki.HighPerformance.LowLevel.Buffer.Allocator.Persistent);
MemoryUtility.MemCpy(&component, data.GetUnsafePtr(), (nuint)sizeof(T));
var command = new Command
{
type = CommandType.AddComponent,
data = data,
entity = entity,
componentTypeID = ComponentTypeID<T>.value
};
_commands.Add(command);
}
public void RemoveComponent<T>(Entity entity)
where T : unmanaged
{
var command = new Command
{
type = CommandType.RemoveComponent,
data = default,
entity = entity,
componentTypeID = ComponentTypeID<T>.value
};
_commands.Add(command);
}
public void Reset()
{
foreach (ref var command in _commands)
{
command.data.Dispose();
}
_commands.Clear();
}
public void Dispose()
{
foreach (ref var command in _commands)
{
command.data.Dispose();
}
_commands.Dispose();
}
}

View File

@@ -0,0 +1,161 @@
using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
namespace Ghost.ArcEntities;
public class EntityManager : IDisposable
{
private struct EntityLocation
{
public Identifier<Archetype> archetypeID;
public int chunkIndex;
public int rowIndex;
}
private World _world;
private UnsafeSlotMap<EntityLocation> _entityLocations;
internal EntityManager(World world)
{
_world = world;
_entityLocations = new UnsafeSlotMap<EntityLocation>(16, Allocator.Persistent);
}
public Entity CreateEntity()
{
// Put into empty archetype
ref var emptyArchetype = ref _world.GetArchetypeReference(World.EmptyArchetypeID);
emptyArchetype.AllocateEntity(out var chunkIndex, out var rowIndex);
var id = _entityLocations.Add(new EntityLocation
{
archetypeID = World.EmptyArchetypeID,
chunkIndex = chunkIndex,
rowIndex = rowIndex
}, out var generation);
var entity = new Entity(id, generation);
emptyArchetype.SetEntity(chunkIndex, rowIndex, entity);
return entity;
}
public ResultStatus RemoveEntity(Entity entity)
{
if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location))
{
return ResultStatus.NotFound;
}
ref var archetype = ref _world.GetArchetypeReference(location.archetypeID);
var r = archetype.RemoveEntity(location.chunkIndex, location.rowIndex);
if (r != ResultStatus.Success)
{
return r;
}
if (!_entityLocations.Remove(entity.ID, entity.Generation))
{
return ResultStatus.NotFound;
}
return ResultStatus.Success;
}
internal ResultStatus UpdateEntityLocation(Entity entity, Identifier<Archetype> newArchetypeID, int newChunkIndex, int newRowIndex)
{
ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist);
if (!exist)
{
return ResultStatus.NotFound;
}
location.archetypeID = newArchetypeID;
location.chunkIndex = newChunkIndex;
location.rowIndex = newRowIndex;
return ResultStatus.Success;
}
public ResultStatus AddComponent<T>(Entity entity)
where T : unmanaged
{
// Find current location
ref var location = ref _entityLocations.GetElementReferenceAt(entity.ID, entity.Generation, out var exist);
if (!exist)
{
return ResultStatus.NotFound;
}
// Build new archetype signature
ref var archetype = ref _world.GetArchetypeReference(location.archetypeID);
var currentSignature = archetype.Signature;
var compID = ComponentTypeID<T>.value;
var largestComponentID = Math.Max(currentSignature.Count, compID);
var length = UnsafeBitSet.RequiredLength(largestComponentID + 1);
Span<uint> bits = stackalloc uint[length];
bits.Clear();
var newSignature = new SpanBitSet(bits);
var i = 0;
var compCount = 0;
do
{
i = currentSignature.NextSetBit(i);
if (i == -1)
{
break;
}
newSignature.SetBit(i);
compCount++;
} while (true);
compCount++;
newSignature.SetBit(compID);
// Find or create new archetype
var newSignatureHash = newSignature.GetHashCode();
var newArcID = _world.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsNotValid)
{
// Create new archetype
Span<int> componentTypeIDs = stackalloc int[compCount];
componentTypeIDs[0] = compID;
do
{
i = currentSignature.NextSetBit(i);
if (i == -1)
{
break;
}
componentTypeIDs[--compCount] = i;
} while (true);
_world.CreateArchetype(componentTypeIDs, newSignatureHash);
}
ref var newArchetype = ref _world.GetArchetypeReference(newArcID);
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
newArchetype.SetEntity(newChunkIndex, newRowIndex, entity);
// Update location
location.archetypeID = newArcID;
location.chunkIndex = newChunkIndex;
location.rowIndex = newRowIndex;
return ResultStatus.Success;
}
public void Dispose()
{
_entityLocations.Dispose();
}
}

View File

@@ -14,7 +14,7 @@ public unsafe class EntityQuery<T1, T2>
private List<ArchetypeCache> _cache = new(); private List<ArchetypeCache> _cache = new();
public void AddMatchingArchetype(Archetype archetype) internal void AddMatchingArchetype(Archetype archetype)
{ {
// We look up the offsets ONCE when the archetype is registered // We look up the offsets ONCE when the archetype is registered
int off1 = archetype.GetOffset(ComponentTypeID<T1>.value); int off1 = archetype.GetOffset(ComponentTypeID<T1>.value);

View File

@@ -1,11 +1,16 @@
using System.Runtime.CompilerServices; using Ghost.Core;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Runtime.CompilerServices;
namespace Ghost.ArcEntities; namespace Ghost.ArcEntities;
public partial class World public partial class World
{ {
private static List<World> s_worlds = new(4); private static List<World?> s_worlds = new(4);
private static Queue<WorldID> s_freeWorldSlots = new(); private static Queue<Identifier<World>> s_freeWorldSlots = new();
internal static Identifier<Archetype> EmptyArchetypeID => new Identifier<Archetype>(0);
public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count; public static int WorldCount => s_worlds.Count - s_freeWorldSlots.Count;
@@ -15,41 +20,60 @@ public partial class World
{ {
if (s_freeWorldSlots.TryDequeue(out var index)) if (s_freeWorldSlots.TryDequeue(out var index))
{ {
s_worlds[index] = new World(index, entityCapacity); s_worlds[index.value] = new World(index, entityCapacity);
} }
else else
{ {
if (s_worlds.Count >= WorldID.MaxValue) index = new Identifier<World>(s_worlds.Count);
{
throw new InvalidOperationException("Maximum number of worlds reached");
}
index = (WorldID)s_worlds.Count;
s_worlds.Add(new World(index, entityCapacity)); s_worlds.Add(new World(index, entityCapacity));
} }
return s_worlds[index]; return s_worlds[index.value]!;
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static World GetWorld(int index) public static Result<World, ResultStatus> GetWorld(Identifier<World> id)
{ {
return s_worlds[index]; if (id.value < 0 || id.value >= s_worlds.Count)
{
return Result.Create(default(World)!, ResultStatus.NotFound);
}
var world = s_worlds[id.value];
if (world is null)
{
return Result.Create(default(World)!, ResultStatus.NotFound);
}
return Result.Create(world, ResultStatus.Success);
} }
} }
public partial class World : IDisposable, IEquatable<World> public partial class World : IIdentifierType, IDisposable, IEquatable<World>
{ {
private readonly WorldID _id; private readonly Identifier<World> _id;
private bool _isDisposed = false; private UnsafeList<Archetype> _archetypes;
private UnsafeHashMap<int, Identifier<Archetype>> _archetypeLookup; // Signature Hash to Archetype ID
private EntityManager _entityManager;
public WorldID ID => _id; private bool _disposed = false;
private World(WorldID id, int entityCapacity) public Identifier<World> ID => _id;
public EntityManager EntityManager => _entityManager;
private World(Identifier<World> id, int entityCapacity)
{ {
_id = id; _id = id;
_archetypes = new UnsafeList<Archetype>(16, Allocator.Persistent);
_archetypeLookup = new UnsafeHashMap<int, Identifier<Archetype>>(16, Allocator.Persistent);
_entityManager = new EntityManager(this);
// Create the empty archetype
CreateArchetype(ReadOnlySpan<int>.Empty, 0);
} }
~World() ~World()
@@ -57,19 +81,32 @@ public partial class World : IDisposable, IEquatable<World>
Dispose(); Dispose();
} }
internal Identifier<Archetype> CreateArchetype(ReadOnlySpan<int> componentTypeIDs, int signatureHash)
{
var arcID = new Identifier<Archetype>(_archetypes.Count);
_archetypes.Add(new Archetype(arcID, _id, componentTypeIDs));
_archetypeLookup.Add(signatureHash, arcID);
return arcID;
}
internal ref Archetype GetArchetypeReference(Identifier<Archetype> id)
{
return ref _archetypes[id.value];
}
internal Identifier<Archetype> GetArchetypeIDBySignatureHash(int signatureHash)
{
if (_archetypeLookup.TryGetValue(signatureHash, out var arcID))
{
return arcID;
}
return Identifier<Archetype>.Invalid;
}
public bool Equals(World? other) public bool Equals(World? other)
{ {
if (other is null) return other is not null && _id == other._id;
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return _id == other._id;
} }
public override int GetHashCode() public override int GetHashCode()
@@ -94,14 +131,17 @@ public partial class World : IDisposable, IEquatable<World>
public void Dispose() public void Dispose()
{ {
if (_isDisposed) if (_disposed)
{ {
return; return;
} }
_archetypes.Dispose();
_archetypeLookup.Dispose();
s_freeWorldSlots.Enqueue(_id); s_freeWorldSlots.Enqueue(_id);
_isDisposed = true; _disposed = true;
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

View File

@@ -21,7 +21,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.1" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.1" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.2.5" /> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.2.6" />
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.2.6" /> <PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.2.6" />
<PackageReference Include="System.IO.Hashing" Version="10.0.0" /> <PackageReference Include="System.IO.Hashing" Version="10.0.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.5" /> <PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.5" />