using Ghost.Core; using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Collections; using System.Runtime.CompilerServices; namespace Ghost.Entities; internal struct EntityQueryMask : IDisposable, IEquatable { public UnsafeBitSet structuralAll; public UnsafeBitSet structuralAny; public UnsafeBitSet structuralAbsent; public UnsafeBitSet requireEnabled; public UnsafeBitSet requireDisabled; public UnsafeBitSet rejectIfEnabled; public UnsafeBitSet writeAccess; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Matches(ref readonly UnsafeBitSet archetypeSignature) { return (!structuralAll.IsCreated || structuralAll.All(archetypeSignature)) && (!structuralAbsent.IsCreated || structuralAbsent.None(archetypeSignature)) && (!structuralAny.IsCreated || structuralAny.Count == 0 || structuralAny.Any(archetypeSignature)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override readonly int GetHashCode() { var hash = 17; if (structuralAll.IsCreated) hash = hash * 23 + structuralAll.GetHashCode(); if (structuralAbsent.IsCreated) hash = hash * 23 + structuralAbsent.GetHashCode(); if (structuralAny.IsCreated) hash = hash * 23 + structuralAny.GetHashCode(); if (requireEnabled.IsCreated) hash = hash * 23 + requireEnabled.GetHashCode(); if (requireDisabled.IsCreated) hash = hash * 23 + requireDisabled.GetHashCode(); if (rejectIfEnabled.IsCreated) hash = hash * 23 + rejectIfEnabled.GetHashCode(); if (writeAccess.IsCreated) hash = hash * 23 + writeAccess.GetHashCode(); return hash; } public readonly bool Equals(EntityQueryMask other) { return structuralAll.Equals(other.structuralAll) && structuralAny.Equals(other.structuralAny) && structuralAbsent.Equals(other.structuralAbsent) && requireEnabled.Equals(other.requireEnabled) && requireDisabled.Equals(other.requireDisabled) && rejectIfEnabled.Equals(other.rejectIfEnabled) && writeAccess.Equals(other.writeAccess); } public override readonly bool Equals(object? obj) { return obj is EntityQueryMask mask && Equals(mask); } public static bool operator ==(EntityQueryMask left, EntityQueryMask right) { return left.Equals(right); } public static bool operator !=(EntityQueryMask left, EntityQueryMask right) { return !(left == right); } public void Dispose() { structuralAll.Dispose(); structuralAny.Dispose(); structuralAbsent.Dispose(); requireEnabled.Dispose(); requireDisabled.Dispose(); rejectIfEnabled.Dispose(); writeAccess.Dispose(); } } /// /// Provides a read-only view over a chunk of entities and their component data within an archetype. /// /// This does not filter disabled/enabled components. You must handle that manually. public readonly unsafe ref struct ChunkView { // We flatten all the information we need for fast access. private readonly ReadOnlyUnsafeCollection _layouts; private readonly byte* _pChunkData; private readonly int* _pVersion; private readonly int _entityOffset; private readonly int _entityCount; private readonly int _structuralVersion; private readonly int _currentVersion; public readonly int Count => _entityCount; internal ChunkView(ref readonly Archetype archetype, ref readonly Chunk chunk) { _layouts = archetype._layouts.AsReadOnly(); _pChunkData = chunk.GetUnsafePtr(); _entityOffset = archetype.EntityIDsOffset; _entityCount = chunk._count; _pVersion = chunk.GetVersionUnsafePtr(); _structuralVersion = chunk._structuralVersion; _currentVersion = World.GetWorldUncheck(archetype.WorldID).Version; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private Archetype.ComponentMemoryLayout GetLayout(Identifier id) { var layout = _layouts[id.Value]; if (layout.enableBitsOffset == -1) { throw new InvalidOperationException($"Component {id} is not exist in the archetype."); } return layout; } /// /// Determines whether the specified component has changed since the given version. /// /// The identifier of the component to check for changes. /// The version number to compare against the component's current version. Must be greater than or equal to zero. /// true if the component's current version is less than or equal to the specified version; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasChanged(Identifier id, int version) { var layout = GetLayout(id); return version < _pVersion[layout.versionIndex]; } /// /// Determines whether the specified version indicates that the component of space has /// changed since the last recorded version. /// /// The space of component to check for changes. Must be an unmanaged space that implements . /// The version number to compare against the current version of the component. /// true if the component of space T has changed since the specified version; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool HasChanged(int version) where T : unmanaged, IComponent { var layout = GetLayout(ComponentTypeID.Value); return version < _pVersion[layout.versionIndex]; } /// /// Determines whether the chunk's structure has changed since the specified version. /// /// The version number to compare against the chunk's structural version. /// true if the chunk's structure has changed since the specified version; otherwise, false. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool HasStructuralChanged(int version) { return version < _structuralVersion; } /// /// Gets the current version number associated with the specified component identifier. /// /// The identifier of the component for which to retrieve the version number. Must reference a valid component. /// The version number of the specified component. [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly int GetComponentVersion(Identifier id) { return _pVersion[id]; } /// /// Gets the current version number associated with the specified component space. /// /// The component space for which to retrieve the version. Must be an unmanaged space that implements . /// The version number of the component space . [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly int GetComponentVersion() where T : unmanaged, IComponent { return _pVersion[ComponentTypeID.Value]; } /// /// Returns a read-only span containing structuralAll entities stored in the current chunk. /// /// A read-only span of values representing the entities in the chunk. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetEntities() { var pEntity = (Entity*)(_pChunkData + _entityOffset); return new ReadOnlySpan(pEntity, _entityCount); } /// /// Gets a readonly span providing direct access to the component data of space T0 for structuralAll entities in the chunk. /// /// The space of component to access. Must be an unmanaged space that implements . /// A readonly span of space containing the component data for each entity in the chunk. /// Thrown if the specified component space is not present in the archetype. [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetComponentData() where T : unmanaged, IComponent { var layout = GetLayout(ComponentTypeID.Value); var pComponentData = _pChunkData + layout.offset; return new ReadOnlySpan(pComponentData, _entityCount); } /// /// Gets a span providing direct access to the component data of space T0 for structuralAll entities in the chunk. /// /// The space of component to access. Must be an unmanaged space that implements . /// A span of space containing the component data for each entity in the chunk. /// Thrown if the specified component space is not present in the archetype. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetComponentDataRW() where T : unmanaged, IComponent { var compId = ComponentTypeID.Value; var layout = GetLayout(compId); _pVersion[layout.versionIndex] = _currentVersion; var pComponentData = _pChunkData + layout.offset; return new Span(pComponentData, _entityCount); } /// /// Gets a bit set representing the enabled state of each instance of the specified enableable component /// space within the current chunk. /// /// The component space for which to retrieve enablement bits. Must be unmanaged and implement . /// A that provides access to the enablement bits for all instances of the specified component space in the chunk. /// Thrown if the specified component space does not support enablement. [MethodImpl(MethodImplOptions.AggressiveInlining)] public SpanBitSet GetEnableBits() where T : unmanaged, IEnableableComponent { var layout = _layouts[ComponentTypeID.Value]; var maskBase = _pChunkData + layout.enableBitsOffset; return new SpanBitSet(new Span(maskBase, (_entityCount + 31) / 32)); } /// /// Determines whether the specified component of space at the given index is currently enabled. /// /// The space of the component to check. Must be an unmanaged space that implements . /// The zero-based index of the component instance to check within the chunk. /// true if the component at the specified index is enabled; otherwise, false. /// Thrown if the specified component space does not support enable/disable functionality. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsComponentEnabled(int index) where T : unmanaged, IEnableableComponent { var layout = GetLayout(ComponentTypeID.Value); var pMask = _pChunkData + layout.enableBitsOffset; return EntityQuery.CheckBit(pMask, index); } } public unsafe partial struct EntityQuery : IDisposable { /// /// Provides an enumerator for iterating over chunks of entities and their component data that match a set of archetypes within a world. /// public readonly ref struct ChunkIterator { public ref struct Enumerator { private readonly ChunkIterator _iterator; private int _archetypeIndex; private int _chunkIndex; internal Enumerator(ChunkIterator iterator) { _iterator = iterator; _archetypeIndex = 0; _chunkIndex = -1; } public readonly ChunkView Current { get { ref var archetype = ref _iterator._world.ComponentManager.GetArchetypeReference(_iterator._matchingArchetypes[_archetypeIndex]); ref var chunk = ref archetype.GetChunkReference(_chunkIndex); return new ChunkView(in archetype, in chunk); } } public bool MoveNext() { _chunkIndex++; while (_archetypeIndex < _iterator._matchingArchetypes.Count) { ref var archetype = ref _iterator._world.ComponentManager.GetArchetypeReference(_iterator._matchingArchetypes[_archetypeIndex]); if (_chunkIndex < archetype.ChunkCount) { return true; } _chunkIndex = 0; _archetypeIndex++; } return false; } public void Reset() { _archetypeIndex = 0; _chunkIndex = -1; } } private readonly ReadOnlyUnsafeCollection> _matchingArchetypes; private readonly World _world; internal ChunkIterator(ReadOnlyUnsafeCollection> matchingArchetypes, World world) { _matchingArchetypes = matchingArchetypes; _world = world; } public readonly Enumerator GetEnumerator() { return new Enumerator(this); } } internal EntityQueryMask _mask; private UnsafeList> _matchingArchetypes; private readonly Identifier _id; private readonly Identifier _worldID; internal EntityQuery(Identifier id, Identifier worldID, EntityQueryMask mask) { _id = id; _worldID = worldID; _mask = mask; _matchingArchetypes = new UnsafeList>(8, Allocator.Persistent); } // TODO: Fetching layout every time is not optimal. Cache them? private static bool IsEntityValid(byte* chunkBase, int entityIndex, ref readonly Archetype archetype, ref readonly EntityQueryMask mask) { // 1. Check "Require Enabled" (WithAll) var it = mask.requireEnabled.GetIterator(); while (it.Next(out var id)) { // Get the EnableBitmask for this component in this chunk var layoutResult = archetype.GetLayout(id); if (layoutResult.Error != ErrorStatus.None // Not enableable, always true || layoutResult.Value.enableBitsOffset == -1) { continue; } // Check bit if (!CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex)) { return false; } } // 2. Check "Require Disabled" (WithDisabled) it = mask.requireDisabled.GetIterator(); while (it.Next(out var id)) { var layoutResult = archetype.GetLayout(id); if (layoutResult.Error != ErrorStatus.None) { continue; } // If component is not enableable, it is technically "Always Enabled", // so it cannot satisfy "WithDisabled". // Check bit (Must be 0) if (layoutResult.Value.enableBitsOffset == -1 || CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex)) { return false; } } // 3. Check "Reject if Enabled" (The "Soft WithNone") it = mask.rejectIfEnabled.GetIterator(); while (it.Next(out var id)) { var layoutResult = archetype.GetLayout(id); if (layoutResult.Error != ErrorStatus.None) { continue; } // If component is not enableable, it is technically "Always Enabled", // so it cannot satisfy "Reject if Enabled". // Check bit (Must be 0) if (layoutResult.Value.enableBitsOffset == -1 || CheckBit(chunkBase + layoutResult.Value.enableBitsOffset, entityIndex)) { return false; } } return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool CheckBit(byte* maskBase, int index) { var byteIndex = index >> Chunk.BIT_SHIFT; var bitIndex = index & Chunk.BIT_ALIGNMENT_MINUS_ONE; return (maskBase[byteIndex] & (1 << bitIndex)) != 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AddArchetypeIfMatch(ref readonly Archetype archetype) { if (_mask.Matches(in archetype._signature)) { _matchingArchetypes.Add(archetype.ID); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ChunkIterator GetChunkIterator() { var world = World.GetWorld(_worldID).Value; return new ChunkIterator(_matchingArchetypes.AsReadOnly(), world); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly int GetEntityCount() { var total = 0; var r = World.GetWorld(_worldID); if (r.IsFailure) { return 0; } var world = r.Value; for(var i = 0; i < _matchingArchetypes.Count; i++) { var archetypeID = _matchingArchetypes[i]; ref var archetype = ref world.ComponentManager.GetArchetypeReference(archetypeID); for (var j = 0; j < archetype.ChunkCount; j++) { ref var chunk = ref archetype.GetChunkReference(j); total += chunk._count; } } return total; } public void Dispose() { _mask.Dispose(); _matchingArchetypes.Dispose(); } } public ref partial struct QueryBuilder { private readonly Stack.Scope _scope; private UnsafeList> _all; private UnsafeList> _any; private UnsafeList> _absent; private UnsafeList> _none; private UnsafeList> _disabled; private UnsafeList> _present; private UnsafeList> _rw; public QueryBuilder() { _scope = AllocationManager.CreateStackScope(); _all = new UnsafeList>(4, _scope.AllocationHandle); _any = new UnsafeList>(4, _scope.AllocationHandle); _absent = new UnsafeList>(4, _scope.AllocationHandle); _none = new UnsafeList>(4, _scope.AllocationHandle); _disabled = new UnsafeList>(4, _scope.AllocationHandle); _present = new UnsafeList>(4, _scope.AllocationHandle); _rw = new UnsafeList>(4, _scope.AllocationHandle); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void FindMax(UnsafeList> list, ref int max) { foreach (var id in list) { if (id.Value > max) max = id.Value; } } public Identifier Build(World world, Allocator allocator = Allocator.Persistent) { // 1. Calculate max component ID to size the BitSets var maxID = 0; FindMax(_all, ref maxID); FindMax(_any, ref maxID); FindMax(_absent, ref maxID); FindMax(_none, ref maxID); FindMax(_disabled, ref maxID); FindMax(_present, ref maxID); // 2. Create the Mask var mask = new EntityQueryMask { structuralAll = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), structuralAny = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), structuralAbsent = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), requireEnabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), requireDisabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), rejectIfEnabled = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), writeAccess = new UnsafeBitSet(maxID + 1, allocator, AllocationOption.Clear), }; // 3. Fill BitSets foreach (var id in _all) { mask.structuralAll.SetBit(id); // Structure: Must Exist mask.requireEnabled.SetBit(id); // Filter: Must be Enabled } foreach (var id in _disabled) { mask.structuralAll.SetBit(id); // Structure: Must Exist mask.requireDisabled.SetBit(id); // Filter: Must be Disabled } foreach (var id in _none) { if (ComponentRegistry.GetComponentInfo(id).isEnableable) { mask.rejectIfEnabled.SetBit(id); // Filter: Must Not be Enabled (Can be Absent or Disabled) } else { mask.structuralAbsent.SetBit(id); // Structure: Must Not Exist } } foreach (var id in _present) { mask.structuralAll.SetBit(id); } foreach (var id in _absent) { mask.structuralAbsent.SetBit(id); } foreach (var id in _any) { mask.structuralAny.SetBit(id); } foreach (var id in _rw) { mask.writeAccess.SetBit(id); } // 4. Ask World for the Query (Cached) var maskHash = mask.GetHashCode(); var queryID = world.ComponentManager.GetEntityQueryIDByMaskHash(maskHash); if (queryID.IsValid) { // Check if the masks are actually equal (Hash collision?). // Really worth it? It's unlikely to have collisions here. if (world.ComponentManager.GetEntityQueryReference(queryID)._mask.Equals(mask)) { mask.Dispose(); goto Return; } } // NOTE: We do not dispose the mask here, as it is now owned by the EntityQuery. queryID = world.ComponentManager.CreateEntityQuery(mask, maskHash); Return: Dispose(); return queryID; } private readonly void Dispose() { _scope.Dispose(); } }