Support enableable components and query enhancements

- Upgraded `Misaki.HighPerformance.LowLevel` to v1.2.8.
- Added `IEquatable` to `Handle<T>` and `Identifier<T>`.
- Improved `Result` extensions with `[CallerArgumentExpression]`.
- Introduced `SetEnabled` in `EntityManager` to toggle components.
- Refactored `Chunk` and `Archetype` for enableable components.
- Added `EntityQueryMask` for filtering enabled/disabled components.
- Enhanced `QueryBuilder` with new filtering methods (`WithAll`, etc.).
- Improved `EntityQuery.ForEach` with entity validation.
This commit is contained in:
2025-12-05 22:38:11 +09:00
parent 224b2b2dd5
commit 30c1d99959
16 changed files with 1203 additions and 448 deletions

View File

@@ -9,10 +9,13 @@ namespace Ghost.Entities;
internal unsafe struct Chunk : IDisposable
{
public const int CHUNK_SIZE = 16384; // 16 KB
public const int BIT_ALIGNMENT = 8;
public const int BIT_SHIFT = 3; // log2(BIT_ALIGNMENT)
public const int BIT_ALIGNMENT_MINUS_ONE = BIT_ALIGNMENT - 1;
private UnsafeArray<byte> _data;
private int _count;
private int _capacity;
private readonly int _capacity;
public int Count
{
@@ -41,21 +44,22 @@ internal unsafe struct Chunk : IDisposable
}
}
internal struct Edge
{
public Identifier<IComponent> componentID;
public int targetArchetype; // can't use Identifier<Archetype> because cycle causer
}
internal struct ComponentMemoryLayout
{
public int offset;
public int size;
public Identifier<IComponent> componentID;
}
internal unsafe struct Archetype : IIdentifierType, IDisposable
{
internal struct ComponentMemoryLayout
{
public int componentID;
public int size;
public int offset;
public int enableBitsOffset; // TODO: Support enableable component
}
private struct Edge
{
public int componentID;
public int targetArchetype; // can't use Identifier<Archetype> because cycle causer
}
private readonly Identifier<Archetype> _id;
private readonly Identifier<World> _worldID;
@@ -63,7 +67,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
internal UnsafeList<Chunk> _chunks;
internal UnsafeArray<ComponentMemoryLayout> _layouts;
private UnsafeArray<int> _componentIDToOffset;
private UnsafeArray<int> _componentIDToLayoutIndex;
// TODO: Is hash map better?
private UnsafeList<Edge> _edgesAdd;
private UnsafeList<Edge> _edgesRemove;
@@ -84,16 +88,16 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
_id = id;
_worldID = worldID;
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
if (componentIds.IsEmpty)
{
_signature = new UnsafeBitSet(1, Allocator.Persistent, AllocationOption.Clear);
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
_hash = 0;
_signature.ClearAll();
_entityCapacity = Chunk.CHUNK_SIZE / sizeof(Entity);
return;
@@ -109,28 +113,23 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
}
_signature = new UnsafeBitSet(highestComponentID + 1, Allocator.Persistent, AllocationOption.Clear);
_chunks = new UnsafeList<Chunk>(4, Allocator.Persistent);
_edgesAdd = new UnsafeList<Edge>(4, Allocator.Persistent);
_edgesRemove = new UnsafeList<Edge>(4, Allocator.Persistent);
_hash = _signature.GetHashCode();
var pComponents = stackalloc ComponentInfo[componentIds.Length];
for (var i = 0; i < componentIds.Length; i++)
{
_signature.SetBit(componentIds[i]);
pComponents[i] = ComponentRegister.GetComponentInfo(componentIds[i]);
}
CalculateLayout(new Span<ComponentInfo>(pComponents, componentIds.Length));
CalculateLayout(componentIds);
}
private void CalculateLayout(Span<ComponentInfo> components)
private void CalculateLayout(ReadOnlySpan<Identifier<IComponent>> componentIds)
{
var entitySize = sizeof(Entity);
var entityAlign = (int)MemoryUtility.AlignOf<Entity>();
var components = (Span<ComponentInfo>)stackalloc ComponentInfo[componentIds.Length];
for (var i = 0; i < componentIds.Length; i++)
{
_signature.SetBit(componentIds[i]);
components[i] = ComponentRegister.GetComponentInfo(componentIds[i]);
}
// Calculate total size per entity to get an initial capacity estimate
var bytesPerEntity = entitySize;
var maxComponentID = 0;
@@ -147,12 +146,13 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
_maxComponentID = maxComponentID;
_entityCapacity = Chunk.CHUNK_SIZE / bytesPerEntity;
_layouts = new UnsafeArray<ComponentMemoryLayout>(components.Length, Allocator.Persistent);
_componentIDToOffset = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent);
_componentIDToLayoutIndex = new UnsafeArray<int>(_maxComponentID + 1, Allocator.Persistent);
_componentIDToOffset.AsSpan().Fill(-1);
_componentIDToLayoutIndex.AsSpan().Fill(-1);
components.Sort((a, b) => b.alignment.CompareTo(a.alignment));
var tempOffsets = stackalloc int[components.Length];
var tempBitmaskOffsets = stackalloc int[components.Length];
while (_entityCapacity > 0)
{
@@ -173,6 +173,19 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
tempOffsets[i] = currentOffset;
currentOffset += _entityCapacity * size;
var bitmaskOffset = -1;
if (components[i].isEnableable)
{
var bitmaskSize = (_entityCapacity + Chunk.BIT_ALIGNMENT_MINUS_ONE) / Chunk.BIT_ALIGNMENT;
// Reserve space for the bitmask (1 bit per entity)
currentOffset = (currentOffset + Chunk.BIT_ALIGNMENT_MINUS_ONE) & ~Chunk.BIT_ALIGNMENT_MINUS_ONE; // Align
bitmaskOffset = currentOffset;
currentOffset += bitmaskSize;
}
tempBitmaskOffsets[i] = bitmaskOffset;
if (currentOffset > Chunk.CHUNK_SIZE)
{
fits = false;
@@ -188,10 +201,11 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
{
offset = tempOffsets[i],
size = components[i].size,
componentID = components[i].id
componentID = components[i].id,
enableBitsOffset = tempBitmaskOffsets[i],
};
_componentIDToOffset[components[i].id] = tempOffsets[i];
_componentIDToLayoutIndex[components[i].id] = i;
}
return;
@@ -219,6 +233,18 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
// Need to allocate a new chunk
var newChunk = new Chunk(Chunk.CHUNK_SIZE, _entityCapacity);
// Set all enable to true by default for enableable components
for (var i = 0; i < _layouts.Count; i++)
{
var layout = _layouts[i];
if (layout.enableBitsOffset != -1)
{
var pChunk = newChunk.GetUnsafePtr();
var pBits = pChunk + layout.enableBitsOffset;
MemoryUtility.MemSet(pBits, 0xFF, (nuint)((_entityCapacity + Chunk.BIT_ALIGNMENT_MINUS_ONE) / Chunk.BIT_ALIGNMENT));
}
}
rowIndex = 0;
newChunk.Count++;
chunkIndex = _chunks.Count;
@@ -237,9 +263,15 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void SetComponentData(int chunkIndex, int rowIndex, Identifier<IComponent> componentID, void* pComponent)
public readonly ResultStatus SetComponentData(int chunkIndex, int rowIndex, Identifier<IComponent> componentID, void* pComponent)
{
var offset = _componentIDToOffset[componentID];
var r = GetLayout(componentID);
if (r.Status != ResultStatus.Success)
{
return r.Status;
}
var offset = r.Value.offset;
var chunk = _chunks[chunkIndex];
var chunkBase = chunk.GetUnsafePtr();
@@ -247,19 +279,27 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
var dst = chunkBase + offset + (size * rowIndex);
MemoryUtility.MemCpy(pComponent, dst, (nuint)size);
return ResultStatus.Success;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void* GetComponentDataPtr(int chunkIndex, int rowIndex, Identifier<IComponent> componentID)
public readonly ResultStatus GetComponentDataPtr(int chunkIndex, int rowIndex, Identifier<IComponent> componentID, void** ppv)
{
var offset = _componentIDToOffset[componentID];
var r = GetLayout(componentID);
if (r.Status != ResultStatus.Success)
{
return r.Status;
}
var offset = r.Value.offset;
var chunk = _chunks[chunkIndex];
var chunkBase = chunk.GetUnsafePtr();
var size = ComponentRegister.GetComponentInfo(componentID).size;
var dst = chunkBase + offset + (size * rowIndex);
*ppv = chunkBase + offset + (size * rowIndex);
return dst;
return ResultStatus.Success;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -269,14 +309,20 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int GetOffset(int componentId)
public readonly Result<ComponentMemoryLayout, ResultStatus> GetLayout(int componentID)
{
if (componentId >= _componentIDToOffset.Count)
if (componentID >= _componentIDToLayoutIndex.Count)
{
return -1;
return Result.Create(default(ComponentMemoryLayout), ResultStatus.InvalidArgument);
}
return _componentIDToOffset[componentId];
var layoutIndex = _componentIDToLayoutIndex[componentID];
if (layoutIndex == -1)
{
return Result.Create(default(ComponentMemoryLayout), ResultStatus.NotFound);
}
return Result.Create(_layouts[layoutIndex], ResultStatus.Success);
}
public ResultStatus RemoveEntity(int chunkIndex, int rowIndex)
@@ -382,26 +428,6 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
return Identifier<Archetype>.Invalid;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<T> GetComponentArray<T>(int chunkIndex)
where T : unmanaged, IComponent
{
var id = ComponentTypeID<T>.value;
if (id >= _componentIDToOffset.Count)
{
return default;
}
var offset = _componentIDToOffset[id];
if (offset == -1)
{
return default;
}
var chunk = _chunks[chunkIndex];
return new Span<T>((T*)((byte*)chunk.GetUnsafePtr() + offset), chunk.Count);
}
public override readonly int GetHashCode()
{
return _hash;
@@ -419,7 +445,7 @@ internal unsafe struct Archetype : IIdentifierType, IDisposable
_signature.Dispose();
_chunks.Dispose();
_componentIDToOffset.Dispose();
_componentIDToLayoutIndex.Dispose();
_layouts.Dispose();
_edgesAdd.Dispose();
_edgesRemove.Dispose();