diff --git a/Ghost.Entities/Archetype.cs b/Ghost.Entities/Archetype.cs index 1c83975..df712bb 100644 --- a/Ghost.Entities/Archetype.cs +++ b/Ghost.Entities/Archetype.cs @@ -111,6 +111,8 @@ internal unsafe struct Chunk : IDisposable private UnsafeArray _data; private UnsafeArray _versions; + // TODO: Add structual change versioning, similar to DidOrderChange in unity ecs. + internal int _count; internal readonly int _capacity; @@ -145,9 +147,7 @@ internal unsafe struct Chunk : IDisposable public void Dispose() { _data.Dispose(); - Console.WriteLine($"Disposing chunk data"); _versions.Dispose(); - Console.WriteLine($"Disposing chunk versions"); } } @@ -487,8 +487,8 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable var pLastEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * lastIndex); var pRowEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * rowIndex); - var wrold = World.GetWorldUncheck(_worldID); - var result = wrold.EntityManager.UpdateEntityLocation(*(Entity*)pLastEntity, _id, chunkIndex, rowIndex); + var world = World.GetWorldUncheck(_worldID); + var result = world.EntityManager.UpdateEntityLocation(*(Entity*)pLastEntity, _id, chunkIndex, rowIndex); if (result != ErrorStatus.None) { return result; @@ -512,6 +512,111 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable return ErrorStatus.None; } + public ErrorStatus RemoveEntities(int chunkIndex, ReadOnlySpan sortedIndicesToRemove) + { + if (chunkIndex < 0 || chunkIndex >= _chunks.Count) + { + return ErrorStatus.InvalidArgument; + } + + if (sortedIndicesToRemove.Length == 0) + { + return ErrorStatus.None; + } + + ref var chunk = ref _chunks[chunkIndex]; + + int oldCount = chunk._count; + int removeCount = sortedIndicesToRemove.Length; + int newCount = oldCount - removeCount; // The boundary between "Keep" and "Drop" + + var chunkBase = chunk.GetUnsafePtr(); + var world = World.GetWorldUncheck(_worldID); // Typo fixed from 'wrold' + + // Pointers for the swap logic + // 1. 'holePtr' tracks which index in the sorted list we are processing + int holePtr = 0; + + // 2. 'candidateIndex' starts at the end of the OLD array and moves backward + int candidateIndex = oldCount - 1; + + // 3. 'removalTailPtr' tracks removals at the end of the array to skip them + int removalTailPtr = sortedIndicesToRemove.Length - 1; + + // Iterate through the holes that are strictly INSIDE the new valid range + while (holePtr < removeCount) + { + int holeIndex = sortedIndicesToRemove[holePtr]; + + // If the current hole is beyond the new count, it's in the "Drop Zone". + // Since the list is sorted, all subsequent holes are also in the drop zone. + // We are done filling holes. + if (holeIndex >= newCount) + break; + + // --- Find a Valid Filler --- + // We look for an entity at the end of the array that IS NOT scheduled for removal. + while (candidateIndex >= newCount) + { + // Check if the current candidate is actually marked for removal + bool isCandidateRemoved = false; + + // Because sortedIndices is sorted, we check the end of the list + // to see if the candidateIndex matches a removal request. + if (removalTailPtr >= 0 && sortedIndicesToRemove[removalTailPtr] == candidateIndex) + { + isCandidateRemoved = true; + removalTailPtr--; // Consume this removal + } + + if (!isCandidateRemoved) + { + // Found a valid filler! + break; + } + + // This candidate was also removed, so skip it and keep looking left + candidateIndex--; + } + + // --- Perform The Swap --- + // Move 'candidateIndex' (Filler) into 'holeIndex' (Hole) + + var pFillerEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * candidateIndex); + var pHoleEntity = chunkBase + _entityIdsOffset + (sizeof(Entity) * holeIndex); + + // 1. Update the Map (Critical Step) + // We tell the world: "The entity that WAS at 'candidateIndex' is now at 'holeIndex'" + var result = world.EntityManager.UpdateEntityLocation(*(Entity*)pFillerEntity, _id, chunkIndex, holeIndex); + if (result != ErrorStatus.None) + { + return result; + } + + // 2. Overwrite Entity ID + MemoryUtility.MemCpy(pHoleEntity, pFillerEntity, (nuint)sizeof(Entity)); + + // 3. Overwrite Components + for (var i = 0; i < _layouts.Count; i++) + { + var layout = _layouts[i]; + var pRow = chunkBase + layout.offset + (layout.size * holeIndex); + var pLast = chunkBase + layout.offset + (layout.size * candidateIndex); + MemoryUtility.MemCpy(pRow, pLast, (nuint)layout.size); + } + + // Prepare for next hole + holePtr++; + candidateIndex--; + } + + // Finally, simply truncate the count + chunk._count = newCount; + + // (Optional) If you have Versioning, mark the components as changed here. + return ErrorStatus.None; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool HasComponent(Identifier componentID) { @@ -577,12 +682,9 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable { if (_chunks.IsCreated) { - var i= 0; foreach (ref var chunk in _chunks) { - Console.WriteLine($"Disposing chunk {i} of archetype {_id}"); chunk.Dispose(); - i++; } } diff --git a/Ghost.Entities/EntityManager.cs b/Ghost.Entities/EntityManager.cs index 09c060a..e0bc3eb 100644 --- a/Ghost.Entities/EntityManager.cs +++ b/Ghost.Entities/EntityManager.cs @@ -16,11 +16,28 @@ namespace Ghost.Entities; /// public unsafe partial class EntityManager : IDisposable { - private struct EntityLocation + private struct EntityLocation : IComparable { public int archetypeID; public int chunkIndex; public int rowIndex; + + public readonly int CompareTo(EntityLocation other) + { + var archComp = chunkIndex.CompareTo(other.chunkIndex); + if (archComp != 0) + { + return archComp; + } + + var chunkComp = chunkIndex.CompareTo(other.chunkIndex); + if (chunkComp != 0) + { + return chunkComp; + } + + return rowIndex.CompareTo(other.rowIndex); + } } private readonly World _world; @@ -234,9 +251,79 @@ public unsafe partial class EntityManager : IDisposable /// The entities to destroy. public void DestroyEntities(ReadOnlySpan entities) { - for (var i = 0; i < entities.Length; i++) + if (entities.Length == 0) { - DestroyEntity(entities[i]); + return; + } + + using var scope = AllocationManager.CreateStackScope(); + var batchDestroy = new UnsafeList(entities.Length, scope.AllocationHandle); + var rowIndicesCache = new UnsafeList(32, scope.AllocationHandle); + + // 1. GATHER + // Resolve all entities to their locations + for (int i = 0; i < entities.Length; i++) + { + var entity = entities[i]; + if (_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location)) + { + batchDestroy.Add(location); + } + } + + if (batchDestroy.Count == 0) + { + return; + } + + // 2. SORT + // Sorting groups them by chunk automatically + batchDestroy.AsSpan().Sort(); + + // 3. SWEEP + // Iterate through the sorted list and batch process each chunk + var firstLoc = batchDestroy[0]; + var prevArchetypeID = firstLoc.archetypeID; + var prevChunkIndex = firstLoc.chunkIndex; + + for (int i = 0; i < batchDestroy.Count; i++) + { + var loc = batchDestroy[i]; + + // Check if we have crossed a boundary (Different Chunk OR Different Archetype) + bool isNewBatch = (loc.chunkIndex != prevChunkIndex) || (loc.archetypeID != prevArchetypeID); + + if (isNewBatch) + { + // FLUSH PREVIOUS BATCH + // We must retrieve the Archetype of the *Previous* batch, not the current 'loc' + ref var prevArchetype = ref _world.GetArchetypeReference(prevArchetypeID); + + // Execute the hole-filling/swap logic + prevArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan()); + + // RESET + rowIndicesCache.Clear(); + prevArchetypeID = loc.archetypeID; + prevChunkIndex = loc.chunkIndex; + } + + rowIndicesCache.Add(loc.rowIndex); + } + + // 4. FINAL FLUSH + // Process the stragglers remaining in the cache + if (rowIndicesCache.Count > 0) + { + ref var lastArchetype = ref _world.GetArchetypeReference(prevArchetypeID); + lastArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan()); + } + + // 5. Remove from Entity Locations + for (int i = 0; i < entities.Length; i++) + { + var entity = entities[i]; + _entityLocations.Remove(entity.ID, entity.Generation); } } diff --git a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs index b641b76..f59a81c 100644 --- a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs +++ b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.gen.cs @@ -20,7 +20,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -210,7 +209,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -412,7 +410,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -624,7 +621,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -846,7 +842,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -1078,7 +1073,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -1320,7 +1314,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; @@ -1572,7 +1565,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs; diff --git a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt index bde0b8e..6d693df 100644 --- a/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt +++ b/Ghost.Entities/Templates/EntityQuery.ComponentIterator.tt @@ -55,7 +55,6 @@ public unsafe partial struct EntityQuery private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly EntityQueryMask _mask; private readonly World _world; - private readonly int _currentVersion; private readonly Stack.Scope _scope; private UnsafeList _changedComponentIDs;