feat(core,rendering)!: add cleanup component support, refactor render pipeline
Introduce ICleanupComponent and cleanup archetype logic in ECS. Refactor component versioning to uint. Update IResourceDatabase to use map/unmap pattern. Decouple per-frame render requests from RenderSystem via IRenderPayload. Update render pipeline and extraction system to new API. BREAKING CHANGE: Entity destruction and render pipeline APIs have changed. IResourceDatabase.MapResource signature is updated; all callers must use map/memcpy/unmap. RenderSystem no longer manages per-frame render requests directly.
This commit is contained in:
@@ -109,10 +109,9 @@ internal unsafe struct Chunk : IDisposable
|
||||
public const int BIT_ALIGNMENT_MINUS_ONE = BIT_ALIGNMENT - 1;
|
||||
|
||||
private UnsafeArray<byte> _data;
|
||||
private UnsafeArray<int> _versions;
|
||||
private UnsafeArray<uint> _versions;
|
||||
|
||||
// TODO: Add structual change versioning, similar to DidOrderChange in unity ecs.
|
||||
internal int _structuralVersion;
|
||||
internal uint _structuralVersion;
|
||||
|
||||
internal int _count;
|
||||
internal readonly int _capacity;
|
||||
@@ -123,10 +122,10 @@ internal unsafe struct Chunk : IDisposable
|
||||
internal int _archetypeID;
|
||||
#endif
|
||||
|
||||
public Chunk(int bufferSize, int capacity, int componentCount, int globalVersion)
|
||||
public Chunk(int bufferSize, int capacity, int componentCount, uint globalVersion)
|
||||
{
|
||||
_data = new UnsafeArray<byte>(bufferSize, Allocator.Persistent, AllocationOption.Clear);
|
||||
_versions = new UnsafeArray<int>(componentCount, Allocator.Persistent);
|
||||
_versions = new UnsafeArray<uint>(componentCount, Allocator.Persistent);
|
||||
_capacity = capacity;
|
||||
_count = 0;
|
||||
|
||||
@@ -141,9 +140,9 @@ internal unsafe struct Chunk : IDisposable
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int* GetVersionUnsafePtr()
|
||||
public readonly uint* GetVersionUnsafePtr()
|
||||
{
|
||||
return (int*)_versions.GetUnsafePtr();
|
||||
return (uint*)_versions.GetUnsafePtr();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -175,7 +174,6 @@ internal unsafe struct Archetype : IDisposable
|
||||
internal UnsafeArray<ComponentMemoryLayout> _layouts;
|
||||
internal UnsafeArray<int> _componentIDToLayoutIndex;
|
||||
|
||||
// TODO: Is hash map better?
|
||||
private UnsafeList<Edge> _edgesAdd;
|
||||
private UnsafeList<Edge> _edgesRemove;
|
||||
|
||||
@@ -187,6 +185,9 @@ internal unsafe struct Archetype : IDisposable
|
||||
private int _maxComponentID;
|
||||
private int _entityIdsOffset;
|
||||
|
||||
// -1 means no cleanup component, 0 means haven't computed yet (since 0 is the empty archetype), positive value means the archetype id of the cleanup edge.
|
||||
internal int _cleanupEdge;
|
||||
|
||||
public readonly Identifier<Archetype> ID => _id;
|
||||
public readonly Identifier<World> WorldID => _worldID;
|
||||
|
||||
@@ -235,10 +236,21 @@ internal unsafe struct Archetype : IDisposable
|
||||
var entityAlign = (int)MemoryUtility.AlignOf<Entity>();
|
||||
|
||||
var components = (Span<ComponentInfo>)stackalloc ComponentInfo[componentIds.Length];
|
||||
|
||||
var cleanupCount = 0;
|
||||
for (var i = 0; i < componentIds.Length; i++)
|
||||
{
|
||||
_signature.SetBit(componentIds[i]);
|
||||
components[i] = ComponentRegistry.GetComponentInfo(componentIds[i]);
|
||||
if (components[i].isCleanup)
|
||||
{
|
||||
cleanupCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanupCount == 0)
|
||||
{
|
||||
_cleanupEdge = -1;
|
||||
}
|
||||
|
||||
// Calculate total size per entity to get an initial capacity estimate
|
||||
@@ -456,7 +468,7 @@ internal unsafe struct Archetype : IDisposable
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Error MarkChanged(int chunkIndex, int componentTypeId, int globalVersion)
|
||||
public readonly Error MarkChanged(int chunkIndex, int componentTypeId, uint globalVersion)
|
||||
{
|
||||
var layoutResult = GetLayout(componentTypeId);
|
||||
if (layoutResult.IsFailure)
|
||||
@@ -471,7 +483,7 @@ internal unsafe struct Archetype : IDisposable
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly Result<int, Error> GetVersion(int chunkIndex, int componentTypeId)
|
||||
public readonly Result<uint, Error> GetVersion(int chunkIndex, int componentTypeId)
|
||||
{
|
||||
var layoutResult = GetLayout(componentTypeId);
|
||||
if (layoutResult.Error != Error.None)
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Ghost.Entities;
|
||||
|
||||
public interface IComponent;
|
||||
public interface IEnableableComponent : IComponent;
|
||||
public interface ICleanupComponent : IComponent;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Struct)]
|
||||
public class RequireComponentAttribute<T> : Attribute
|
||||
@@ -24,6 +25,7 @@ internal struct ComponentInfo
|
||||
public int alignment;
|
||||
public bool isEnableable;
|
||||
public bool isSharedWarper;
|
||||
public bool isCleanup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,6 +71,7 @@ internal static class ComponentRegistry
|
||||
alignment = (int)MemoryUtility.AlignOf<T>(),
|
||||
isEnableable = typeof(IEnableableComponent).IsAssignableFrom(type),
|
||||
isSharedWarper = typeof(ISharedWarper).IsAssignableFrom(type),
|
||||
isCleanup = typeof(ICleanupComponent).IsAssignableFrom(type),
|
||||
};
|
||||
|
||||
s_registeredComponents.Add(info);
|
||||
|
||||
@@ -85,6 +85,29 @@ public unsafe partial class EntityManager : IDisposable
|
||||
return Error.NotFound;
|
||||
}
|
||||
|
||||
private static void CopyData(ref Archetype oldArch, int oldChunk, int oldRow,
|
||||
ref Archetype newArch, int newChunk, int newRow)
|
||||
{
|
||||
// Iterate every component space in the OLD archetype
|
||||
for (var i = 0; i < oldArch._layouts.Count; i++)
|
||||
{
|
||||
var layout = oldArch._layouts[i];
|
||||
|
||||
var src = oldArch._chunks[oldChunk].GetUnsafePtr() + layout.offset + (layout.size * oldRow);
|
||||
var r = newArch.GetLayout(layout.componentID);
|
||||
if (r.Error != Error.None)
|
||||
{
|
||||
// New archetype does not have this component, skip it.
|
||||
// This can happen when removing components.
|
||||
continue;
|
||||
}
|
||||
|
||||
var dst = newArch._chunks[newChunk].GetUnsafePtr() + r.Value.offset + (layout.size * newRow);
|
||||
|
||||
MemoryUtility.MemCpy(dst, src, (nuint)layout.size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an entity with no components.
|
||||
/// </summary>
|
||||
@@ -227,29 +250,20 @@ public unsafe partial class EntityManager : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void DestoryManagedEntityIfExists(ref readonly Archetype archetype, EntityLocation location)
|
||||
{
|
||||
var pManagedRef = archetype.GetComponentData(location.chunkIndex, location.rowIndex, ComponentTypeID<ManagedEntityRef>.Value);
|
||||
if (pManagedRef != null)
|
||||
{
|
||||
DestroyManagedEntity(((ManagedEntityRef*)pManagedRef)->entity);
|
||||
}
|
||||
}
|
||||
// private void DestoryManagedEntityIfExists(ref readonly Archetype archetype, EntityLocation location)
|
||||
// {
|
||||
// var pManagedRef = archetype.GetComponentData(location.chunkIndex, location.rowIndex, ComponentTypeID<ManagedEntityRef>.Value);
|
||||
// if (pManagedRef != null)
|
||||
// {
|
||||
// DestroyManagedEntity(((ManagedEntityRef*)pManagedRef)->entity);
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the specified entity.
|
||||
/// </summary>
|
||||
/// <returns>The result status of the operation.</returns>
|
||||
public Error DestroyEntity(Entity entity)
|
||||
private Error DestroyEntity_Internal(Entity entity, EntityLocation location)
|
||||
{
|
||||
if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location))
|
||||
{
|
||||
return Error.NotFound;
|
||||
}
|
||||
|
||||
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||
|
||||
DestoryManagedEntityIfExists(in archetype, location);
|
||||
// DestoryManagedEntityIfExists(in archetype, location);
|
||||
var r = archetype.RemoveEntity(location.chunkIndex, location.rowIndex);
|
||||
if (r != Error.None)
|
||||
{
|
||||
@@ -264,27 +278,100 @@ public unsafe partial class EntityManager : IDisposable
|
||||
return Error.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the specified entity.
|
||||
/// </summary>
|
||||
/// <returns>The result status of the operation.</returns>
|
||||
public Error DestroyEntity(Entity entity)
|
||||
{
|
||||
if (!_entityLocations.TryGetElementAt(entity.ID, entity.Generation, out var location))
|
||||
{
|
||||
return Error.NotFound;
|
||||
}
|
||||
|
||||
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
|
||||
|
||||
if (archetype._cleanupEdge < 0)
|
||||
{
|
||||
return DestroyEntity_Internal(entity, location);
|
||||
}
|
||||
else
|
||||
{
|
||||
Identifier<Archetype> newArcID = default;
|
||||
if (archetype._cleanupEdge == 0)
|
||||
{
|
||||
ref var signature = ref archetype._signature;
|
||||
|
||||
using var scope = AllocationManager.CreateStackScope();
|
||||
using var newSignature = new UnsafeBitSet(signature.Count, scope.AllocationHandle);
|
||||
|
||||
var compCount = 0;
|
||||
var it = signature.GetIterator();
|
||||
while (it.Next(out var componentID))
|
||||
{
|
||||
if (ComponentRegistry.GetComponentInfo(componentID).isCleanup)
|
||||
{
|
||||
newSignature.SetBit(componentID);
|
||||
compCount++;
|
||||
}
|
||||
}
|
||||
|
||||
var newSignatureHash = newSignature.GetHashCode();
|
||||
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
|
||||
if (newArcID.IsInvalid)
|
||||
{
|
||||
// Create new archetype
|
||||
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
|
||||
|
||||
var newIt = newSignature.GetIterator();
|
||||
var i = 0;
|
||||
while (newIt.Next(out var index))
|
||||
{
|
||||
componentTypeIDs[i++] = index;
|
||||
}
|
||||
|
||||
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
|
||||
}
|
||||
|
||||
archetype._cleanupEdge = newArcID;
|
||||
}
|
||||
else
|
||||
{
|
||||
newArcID = archetype._cleanupEdge;
|
||||
}
|
||||
|
||||
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
|
||||
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
|
||||
CopyData(ref archetype, location.chunkIndex, location.rowIndex,
|
||||
ref newArchetype, newChunkIndex, newRowIndex);
|
||||
|
||||
newArchetype.SetEntity(newChunkIndex, newRowIndex, entity);
|
||||
}
|
||||
|
||||
return Error.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroy the specified entities.
|
||||
/// </summary>
|
||||
/// <param name="entities">The entities to destroy.</param>
|
||||
public void DestroyEntities(ReadOnlySpan<Entity> entities)
|
||||
{
|
||||
void RemoveManagedEntity(ReadOnlySpan<int> rowIndicesCache, ref readonly Archetype archetype, int chunkIndex)
|
||||
{
|
||||
for (var j = 0; j < rowIndicesCache.Length; j++)
|
||||
{
|
||||
var rowIndex = rowIndicesCache[j];
|
||||
var location = new EntityLocation
|
||||
{
|
||||
archetypeID = archetype.ID,
|
||||
chunkIndex = chunkIndex,
|
||||
rowIndex = rowIndex
|
||||
};
|
||||
|
||||
DestoryManagedEntityIfExists(in archetype, location);
|
||||
}
|
||||
}
|
||||
// void RemoveManagedEntity(ReadOnlySpan<int> rowIndicesCache, ref readonly Archetype archetype, int chunkIndex)
|
||||
// {
|
||||
// for (var j = 0; j < rowIndicesCache.Length; j++)
|
||||
// {
|
||||
// var rowIndex = rowIndicesCache[j];
|
||||
// var location = new EntityLocation
|
||||
// {
|
||||
// archetypeID = archetype.ID,
|
||||
// chunkIndex = chunkIndex,
|
||||
// rowIndex = rowIndex
|
||||
// };
|
||||
//
|
||||
// DestoryManagedEntityIfExists(in archetype, location);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (entities.Length == 0)
|
||||
{
|
||||
@@ -292,8 +379,8 @@ public unsafe partial class EntityManager : IDisposable
|
||||
}
|
||||
|
||||
using var scope = AllocationManager.CreateStackScope();
|
||||
var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
|
||||
var rowIndicesCache = new UnsafeList<int>(32, scope.AllocationHandle);
|
||||
using var batchDestroy = new UnsafeList<EntityLocation>(entities.Length, scope.AllocationHandle);
|
||||
using var rowIndicesCache = new UnsafeList<int>(32, scope.AllocationHandle);
|
||||
|
||||
// 1. GATHER
|
||||
// Resolve all entities to their locations
|
||||
@@ -335,7 +422,9 @@ public unsafe partial class EntityManager : IDisposable
|
||||
ref var prevArchetype = ref _world.ComponentManager.GetArchetypeReference(prevArchetypeID);
|
||||
|
||||
// Remove Managed Entities first
|
||||
RemoveManagedEntity(rowIndicesCache.AsSpan(), in prevArchetype, prevChunkIndex);
|
||||
// RemoveManagedEntity(rowIndicesCache.AsSpan(), in prevArchetype, prevChunkIndex);
|
||||
|
||||
// TODO: Handle ICleanupComponent here before we remove the entities from the archetype.
|
||||
|
||||
// Execute the hole-filling/swap logic
|
||||
prevArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan());
|
||||
@@ -355,7 +444,7 @@ public unsafe partial class EntityManager : IDisposable
|
||||
{
|
||||
ref var lastArchetype = ref _world.ComponentManager.GetArchetypeReference(prevArchetypeID);
|
||||
|
||||
RemoveManagedEntity(rowIndicesCache.AsSpan(), in lastArchetype, prevChunkIndex);
|
||||
// RemoveManagedEntity(rowIndicesCache.AsSpan(), in lastArchetype, prevChunkIndex);
|
||||
lastArchetype.RemoveEntities(prevChunkIndex, rowIndicesCache.AsSpan());
|
||||
}
|
||||
|
||||
@@ -470,29 +559,6 @@ public unsafe partial class EntityManager : IDisposable
|
||||
return ref *(T*)ptr; // This will return null ref if ptr is null.
|
||||
}
|
||||
|
||||
private static void CopyData(ref Archetype oldArch, int oldChunk, int oldRow,
|
||||
ref Archetype newArch, int newChunk, int newRow)
|
||||
{
|
||||
// Iterate every component space in the OLD archetype
|
||||
for (var i = 0; i < oldArch._layouts.Count; i++)
|
||||
{
|
||||
var layout = oldArch._layouts[i];
|
||||
|
||||
var src = oldArch._chunks[oldChunk].GetUnsafePtr() + layout.offset + (layout.size * oldRow);
|
||||
var r = newArch.GetLayout(layout.componentID);
|
||||
if (r.Error != Error.None)
|
||||
{
|
||||
// New archetype does not have this component, skip it.
|
||||
// This can happen when removing components.
|
||||
continue;
|
||||
}
|
||||
|
||||
var dst = newArch._chunks[newChunk].GetUnsafePtr() + r.Value.offset + (layout.size * newRow);
|
||||
|
||||
MemoryUtility.MemCpy(dst, src, (nuint)layout.size);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a component to the specified entity.
|
||||
/// </summary>
|
||||
@@ -640,6 +706,12 @@ public unsafe partial class EntityManager : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
if (compCount == 0)
|
||||
{
|
||||
// If there is no component left, we destroy the entity directly.
|
||||
return DestroyEntity_Internal(entity, location);
|
||||
}
|
||||
|
||||
// Find or create new archetype
|
||||
var newSignatureHash = newSignature.GetHashCode();
|
||||
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
|
||||
@@ -676,11 +748,11 @@ public unsafe partial class EntityManager : IDisposable
|
||||
return r;
|
||||
}
|
||||
|
||||
var pManagedRef = oldArchetype.GetComponentData(location.chunkIndex, location.rowIndex, ComponentTypeID<ManagedEntityRef>.Value);
|
||||
if (pManagedRef != null)
|
||||
{
|
||||
DestroyManagedEntity(((ManagedEntityRef*)pManagedRef)->entity);
|
||||
}
|
||||
// var pManagedRef = oldArchetype.GetComponentData(location.chunkIndex, location.rowIndex, ComponentTypeID<ManagedEntityRef>.Value);
|
||||
// if (pManagedRef != null)
|
||||
// {
|
||||
// DestroyManagedEntity(((ManagedEntityRef*)pManagedRef)->entity);
|
||||
// }
|
||||
|
||||
// Update location
|
||||
location.archetypeID = newArcID;
|
||||
@@ -833,6 +905,8 @@ public unsafe partial class EntityManager : IDisposable
|
||||
maskBase[byteIndex] &= (byte)~(1 << bitIndex);
|
||||
}
|
||||
|
||||
chunk.GetVersionUnsafePtr()[layoutResult.Value.versionIndex] = _world.Version;
|
||||
|
||||
return Error.None;
|
||||
}
|
||||
|
||||
|
||||
@@ -90,11 +90,11 @@ public readonly unsafe ref struct ChunkView
|
||||
private readonly ReadOnlyUnsafeCollection<Archetype.ComponentMemoryLayout> _layouts;
|
||||
private readonly ReadOnlyUnsafeCollection<int> _layoutIndexLookup;
|
||||
private readonly byte* _pChunkData;
|
||||
private readonly int* _pVersion;
|
||||
private readonly uint* _pVersion;
|
||||
private readonly int _entityOffset;
|
||||
private readonly int _entityCount;
|
||||
private readonly int _structuralVersion;
|
||||
private readonly int _currentVersion;
|
||||
private readonly uint _structuralVersion;
|
||||
private readonly uint _currentVersion;
|
||||
|
||||
public readonly int EntityCount => _entityCount;
|
||||
|
||||
@@ -168,7 +168,7 @@ public readonly unsafe ref struct ChunkView
|
||||
/// <param name="id">The identifier of the component for which to retrieve the version number. Must reference a valid component.</param>
|
||||
/// <returns>The version number of the specified component.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int GetComponentVersion(Identifier<IComponent> id)
|
||||
public readonly uint GetComponentVersion(Identifier<IComponent> id)
|
||||
{
|
||||
return _pVersion[id];
|
||||
}
|
||||
@@ -179,7 +179,7 @@ public readonly unsafe ref struct ChunkView
|
||||
/// <typeparam name="T">The component space for which to retrieve the version. Must be an unmanaged space that implements <see cref="IComponent"/>.</typeparam>
|
||||
/// <returns>The version number of the component space <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int GetComponentVersion<T>()
|
||||
public readonly uint GetComponentVersion<T>()
|
||||
where T : unmanaged, IComponent
|
||||
{
|
||||
return _pVersion[ComponentTypeID<T>.Value];
|
||||
|
||||
@@ -88,7 +88,7 @@ public partial class World : IDisposable, IEquatable<World>
|
||||
|
||||
private readonly Dictionary<Type, object> _services;
|
||||
|
||||
private int _version;
|
||||
private uint _version;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
@@ -119,7 +119,7 @@ public partial class World : IDisposable, IEquatable<World>
|
||||
/// <summary>
|
||||
/// Gets the current version number of the world.
|
||||
/// </summary>
|
||||
public int Version => Interlocked.CompareExchange(ref _version, 0, 0);
|
||||
public uint Version => Interlocked.CompareExchange(ref _version, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the main entity command buffer for this world.
|
||||
@@ -172,7 +172,7 @@ public partial class World : IDisposable, IEquatable<World>
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal int AdvanceVersion()
|
||||
internal uint AdvanceVersion()
|
||||
{
|
||||
return Interlocked.Increment(ref _version);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user