From 3c376ebe489dbfccc6b686cfd2da5a97492c0ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Fri, 22 Aug 2025 18:36:34 -0300 Subject: [PATCH 01/13] Add chunk-level dirty flag support --- src/Arch/Arch.csproj | 12 ++-- src/Arch/Core/Archetype.cs | 2 +- src/Arch/Core/Chunk.cs | 117 +++++++++++++++++++++++++++++++--- src/Arch/Core/Utils/BitSet.cs | 73 +++++++++++++++++++-- 4 files changed, 185 insertions(+), 19 deletions(-) diff --git a/src/Arch/Arch.csproj b/src/Arch/Arch.csproj index 35b5ce92..20186674 100644 --- a/src/Arch/Arch.csproj +++ b/src/Arch/Arch.csproj @@ -41,29 +41,29 @@ - TRACE; + TRACE;DIRTY_FLAGS; false AnyCPU - TRACE;PURE_ECS; + TRACE;PURE_ECS;DIRTY_FLAGS; - TRACE;EVENTS; + TRACE;EVENTS;DIRTY_FLAGS; - TRACE + TRACE;DIRTY_FLAGS; - TRACE;EVENTS; + TRACE;EVENTS;DIRTY_FLAGS; - TRACE;PURE_ECS + TRACE;PURE_ECS;DIRTY_FLAGS; diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index 931df944..afececd9 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -436,7 +436,7 @@ internal int Add(Entity entity, out Chunk chunk, out Slot slot) // TODO: Store ref var currentChunk = ref GetChunk(count); // Fill chunk - if (currentChunk.IsEmpty) + if (!currentChunk.IsFull) { slot = new Slot(currentChunk.Add(entity), count); chunk = currentChunk; diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index 7ce1a838..42e44e0b 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -1,12 +1,9 @@ using System.Buffers; using System.Diagnostics.Contracts; -using System.Drawing; using Arch.Core.Events; -using Arch.Core.Extensions; using Arch.Core.Extensions.Internal; using Arch.Core.Utils; using Arch.LowLevel; -using Collections.Pooled; using CommunityToolkit.HighPerformance; using Array = System.Array; @@ -175,6 +172,15 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span var type = types[index]; Components[index] = ArrayRegistry.GetArray(type, Capacity); } + +#if DIRTY_FLAGS + DirtyFlags = new BitSet[types.Length]; + + for (var i = 0; i < types.Length; i++) + { + DirtyFlags[i] = new BitSet(Capacity); + } +#endif } @@ -216,11 +222,6 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span /// public readonly bool IsFull { [Pure] get => Count >= Capacity; } - /// - /// Checks whether this instance is full or not. - /// - public readonly bool IsEmpty { [Pure] get => Count < Capacity; } - /// /// Inserts an entity into the . /// This won't fire an event for . @@ -666,3 +667,103 @@ internal int Transfer(int index, ref Chunk chunk) return lastEntity.Id; } } + +#if DIRTY_FLAGS + +public partial struct Chunk +{ + public readonly BitSet[] DirtyFlags { [Pure] get; } + + /// + /// Checks whether any component in this chunk has been marked dirty. + /// + /// True if it has, otherwise false + public bool IsDirty() + { + for (int i = 0; i < Components.Length; i++) + { + if (DirtyFlags.DangerousGetReferenceAt(i).IsAnyBitSet()) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether any component of the given type has been marked dirty. + /// + /// The component type to check + /// True if it has, otherwise false + public bool IsDirty(ComponentType type) + { + var compIndex = Index(type); + return DirtyFlags.DangerousGetReferenceAt(compIndex).IsAnyBitSet(); + } + + /// + /// Checks whether the component at the given index has been marked dirty. + /// + /// True if it has, otherwise false + public bool IsDirty(int index, ComponentType type) + { + var compIndex = Index(type); + return DirtyFlags.DangerousGetReferenceAt(compIndex).IsSet(index); + } + + /// + /// Set the dirty flags of the specified component type for all indices in this chunk. + /// + /// The type. + public void SetDirty(ComponentType type) + { + var index = Index(type); + DirtyFlags.DangerousGetReferenceAt(index).SetAll(); + } + + /// + /// Set the dirty flags of the specified component type for all indices in this chunk. + /// + /// The type. + /// The index. + public void SetDirty(int index, ComponentType type) + { + var compIndex = Index(type); + DirtyFlags.DangerousGetReferenceAt(compIndex).SetBit(index); + } + + /// + /// Clears all dirty flags in this chunk. + /// + public void ClearDirty() + { + foreach (var flags in DirtyFlags) + { + flags.ClearAll(); + } + } + + /// + /// Clears all dirty flags for the specified component type. + /// + /// The type. + public void ClearDirty(ComponentType type) + { + var index = Index(type); + DirtyFlags.DangerousGetReferenceAt(index).ClearAll(); + } + + /// + /// Clears the dirty flag for the specified component type at the specified index. + /// + /// The type. + /// The index. + public void ClearDirty(int index, ComponentType type) + { + var compIndex = Index(type); + DirtyFlags.DangerousGetReferenceAt(compIndex).ClearBit(index); + } +} + +#endif diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs index a82c593a..45a4e634 100644 --- a/src/Arch/Core/Utils/BitSet.cs +++ b/src/Arch/Core/Utils/BitSet.cs @@ -24,7 +24,7 @@ public sealed class BitSet public static int RequiredLength(int id) { -#if NET7_0 +#if NET7_0_OR_GREATER return (id >> 5) + int.Sign(id & BitSize); #else return (int)Math.Ceiling((float)id / BitSize); @@ -36,17 +36,15 @@ public static int RequiredLength(int id) /// private uint[] _bits; - /// TODO: Update on ClearBit, however clearbit is only used in tests so its fine for now. /// /// The highest bit set. /// private int _highestBit; - /// TODO: Update on ClearBit, probably remove in favor? /// /// The maximum -index current in use. /// - private int _max; + private int _max; // TODO: probably remove in favor? /// /// Initializes a new instance of the class. @@ -56,6 +54,21 @@ public BitSet() _bits = new uint[_padding]; } + /// + /// Initializes a new instance of the class with the required capacity. + /// + /// The initial capacity. + public BitSet(int capacity) + { + // Calculate the number of uint blocks needed for 'capacity' bits + // Each block holds BitSize + 1 bits (usually 32) + int requiredBlocks = (capacity + BitSize) / (BitSize + 1); + + // Round up to nearest multiple of _padding + int size = (requiredBlocks + _padding - 1) / _padding * _padding; + _bits = new uint[size]; + } + /// /// Initializes a new instance of the class. /// @@ -104,6 +117,15 @@ public bool IsSet(int index) return (_bits[b] & (1 << (index & BitSize))) != 0; } + /// + /// Checks whether any bit is set. + /// + /// True if it is, otherwise false + public bool IsAnyBitSet() + { + return _highestBit >= 0; + } + /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. @@ -136,6 +158,47 @@ public void ClearBit(int index) } _bits[b] &= ~(1u << (index & BitSize)); + + // Update _highestBit and _max only if we cleared the previous highest bit + if (index != _highestBit) + { + return; + } + + for (int i = _bits.Length - 1; i >= 0; i--) + { + uint val = _bits[i]; + if (val != 0) + { + +#if NET7_0_OR_GREATER + + // Compute highest set bit using LeadingZeroCount + int highestInBlock = BitSize - BitOperations.LeadingZeroCount(val) + 1; + _highestBit = (i << IndexSize) + (highestInBlock - 1); + _max = (_highestBit / (BitSize + 1)) + 1; + return; + +#else + // Find the highest set bit in this block + int bitPos = BitSize; // usually 31 + while (bitPos >= 0) + { + if ((val & (1u << bitPos)) != 0) + { + _highestBit = (i << IndexSize) + bitPos; + _max = (_highestBit / (BitSize + 1)) + 1; + return; + } + bitPos--; + } +#endif + } + } + + // No bits left + _highestBit = -1; + _max = 0; } /// @@ -159,6 +222,8 @@ public void SetAll() public void ClearAll() { Array.Clear(_bits, 0, _bits.Length); + _highestBit = -1; + _max = 0; } /// From 70a75bf25452542990ed848a6fc5578a1c750b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 25 Aug 2025 12:20:32 -0300 Subject: [PATCH 02/13] Add high-level (archetype, world and extension) dirty flag support --- src/Arch/Core/Archetype.cs | 65 ++++++++++++++++++++ src/Arch/Core/Chunk.cs | 65 ++++++++++---------- src/Arch/Core/Extensions/EntityExtensions.cs | 43 +++++++++++++ src/Arch/Core/World.cs | 63 +++++++++++++++++++ 4 files changed, 204 insertions(+), 32 deletions(-) diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index afececd9..9ef27875 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -958,3 +958,68 @@ internal static void CopyComponents(Archetype source, ref Slot fromSlot, Archety Chunk.CopyComponents(ref oldChunk, fromSlot.Index, ref sourceSignature, ref newChunk, toSlot.Index, 1); } } + + +#if DIRTY_FLAGS + +public sealed partial class Archetype +{ + /// + /// Flags the component of an at a given as dirty. + /// + /// The at which the component of an is to be marked dirty. + /// The component type. + internal void SetDirty(ref Slot slot, ComponentType componentType) + { + ref var chunk = ref GetChunk(slot.ChunkIndex); + chunk.SetDirty(slot.Index, componentType); + } + + /// + /// Clears the dirty flag of the component of an at a given . + /// + /// The at which the component of an is to be cleared. + /// The component type. + internal void ClearDirty(ref Slot slot, ComponentType componentType) + { + ref var chunk = ref GetChunk(slot.ChunkIndex); + chunk.ClearDirty(slot.Index, componentType); + } + + /// + /// Clears the dirty flag for all components of an at a given . + /// + /// The slot. + internal void ClearDirty(ref Slot slot) + { + ref var chunk = ref GetChunk(slot.ChunkIndex); + chunk.ClearDirty(slot.Index); + } + + /// + /// Clears all the dirty flags in this Archetype. + /// + internal void ClearAllDirty() + { + for (var i = 0; i < Chunks.Count; i++) + { + ref var chunk = ref Chunks[i]; + chunk.ClearAllDirty(); + } + } + + /// + /// Clears all the dirty flags for the specified component type in this Archetype. + /// + /// The type. + internal void ClearDirty(ComponentType type) + { + for (var i = 0; i < Chunks.Count; i++) + { + ref var chunk = ref Chunks[i]; + chunk.ClearAllDirty(type); + } + } +} + +#endif diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index 42e44e0b..502e49b6 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -675,37 +675,22 @@ public partial struct Chunk public readonly BitSet[] DirtyFlags { [Pure] get; } /// - /// Checks whether any component in this chunk has been marked dirty. + /// Checks whether any component of the given type has been flagged dirty. /// - /// True if it has, otherwise false - public bool IsDirty() - { - for (int i = 0; i < Components.Length; i++) - { - if (DirtyFlags.DangerousGetReferenceAt(i).IsAnyBitSet()) - { - return true; - } - } - - return false; - } - - /// - /// Checks whether any component of the given type has been marked dirty. - /// - /// The component type to check - /// True if it has, otherwise false - public bool IsDirty(ComponentType type) + /// The component type. + /// True if the component is dirty, false otherwise. + public bool IsAnyDirty(ComponentType type) { var compIndex = Index(type); return DirtyFlags.DangerousGetReferenceAt(compIndex).IsAnyBitSet(); } /// - /// Checks whether the component at the given index has been marked dirty. + /// Checks whether the component at the given index has been flagged dirty. /// - /// True if it has, otherwise false + /// The index. + /// The component type. + /// True if the component is dirty, false otherwise. public bool IsDirty(int index, ComponentType type) { var compIndex = Index(type); @@ -713,42 +698,45 @@ public bool IsDirty(int index, ComponentType type) } /// - /// Set the dirty flags of the specified component type for all indices in this chunk. + /// Flags the specified component type as dirty, for all indices in this chunk. /// /// The type. - public void SetDirty(ComponentType type) + public void SetAllDirty(ComponentType type) { + // TODO add a single per-component flag to avoid setting and clearing all the bits var index = Index(type); DirtyFlags.DangerousGetReferenceAt(index).SetAll(); } /// - /// Set the dirty flags of the specified component type for all indices in this chunk. + /// Flags the component at the given index as dirty. /// - /// The type. /// The index. + /// The component type. public void SetDirty(int index, ComponentType type) { + // TODO add a single per-component flag to avoid setting and clearing all the bits var compIndex = Index(type); DirtyFlags.DangerousGetReferenceAt(compIndex).SetBit(index); } /// - /// Clears all dirty flags in this chunk. + /// Clears all the dirty flags in this Chunk. /// - public void ClearDirty() + public void ClearAllDirty() { - foreach (var flags in DirtyFlags) + for (var i = 0; i < DirtyFlags.Length; i++) { + var flags = DirtyFlags.DangerousGetReferenceAt(i); flags.ClearAll(); } } /// - /// Clears all dirty flags for the specified component type. + /// Clears all the dirty flags for the specified component type in this Chunk. /// /// The type. - public void ClearDirty(ComponentType type) + public void ClearAllDirty(ComponentType type) { var index = Index(type); DirtyFlags.DangerousGetReferenceAt(index).ClearAll(); @@ -764,6 +752,19 @@ public void ClearDirty(int index, ComponentType type) var compIndex = Index(type); DirtyFlags.DangerousGetReferenceAt(compIndex).ClearBit(index); } + + /// + /// Clears the dirty flag for all components at the specified index. + /// + /// The index. + public void ClearDirty(int index) + { + for (var i = 0; i < DirtyFlags.Length; i++) + { + var flags = DirtyFlags.DangerousGetReferenceAt(i); + flags.ClearBit(index); + } + } } #endif diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs index cc890c4e..e824dd4f 100644 --- a/src/Arch/Core/Extensions/EntityExtensions.cs +++ b/src/Arch/Core/Extensions/EntityExtensions.cs @@ -359,3 +359,46 @@ public static void RemoveRange(this in Entity entity, Span types) #endif } + +public static partial class EntityExtensions +{ + +#if DIRTY_FLAGS && !PURE_ECS + + /// + public static void SetDirty(this Entity entity) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + world.SetDirty(entity); + } + + /// + public static void SetDirty(this Entity entity, ComponentType componentType) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + world.SetDirty(entity, componentType); + } + + /// + public static void ClearDirty(this Entity entity) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + world.ClearDirty(entity); + } + + /// + public static void ClearDirty(this Entity entity, ComponentType componentType) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + world.ClearDirty(entity, componentType); + } + + /// + public static void ClearDirty(this Entity entity) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + world.ClearDirty(entity); + } + +#endif +} diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index 5af4c0ba..6590a0bd 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -1745,3 +1745,66 @@ public Signature GetSignature(Entity entity) } #endregion + +#if DIRTY_FLAGS + +public partial class World +{ + /// + /// Flags the component of type an as dirty. + /// + /// The component type. + /// The . + public void SetDirty(Entity entity) + { + var componentType = Component.ComponentType; + var entityData = EntityInfo.GetEntityData(entity.Id); + entityData.Archetype.SetDirty(ref entityData.Slot, componentType); + } + + /// + /// Flags the component of type an as dirty. + /// + /// The . + /// The component . + public void SetDirty(Entity entity, ComponentType type) + { + var entityData = EntityInfo.GetEntityData(entity.Id); + entityData.Archetype.SetDirty(ref entityData.Slot, type); + } + + /// + /// Clears the dirty flag of the component of an . + /// + /// The component type. + /// The . + public void ClearDirty(Entity entity) + { + var componentType = Component.ComponentType; + var entityData = EntityInfo.GetEntityData(entity.Id); + entityData.Archetype.ClearDirty(ref entityData.Slot, componentType); + } + + /// + /// Clears the dirty flag of the component of an . + /// + /// The . + /// The component . + public void ClearDirty(Entity entity, ComponentType type) + { + var entityData = EntityInfo.GetEntityData(entity.Id); + entityData.Archetype.ClearDirty(ref entityData.Slot, type); + } + + /// + /// Clears the dirty flag for all components of an . + /// + /// The . + public void ClearDirty(Entity entity) + { + var entityData = EntityInfo.GetEntityData(entity.Id); + entityData.Archetype.ClearDirty(ref entityData.Slot); + } +} + +#endif From b69eeeea5a2f637c0c46c323496c596c98c2957b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 25 Aug 2025 13:51:04 -0300 Subject: [PATCH 03/13] Add dirty flag support to queries (WIP) --- src/Arch/Arch.csproj | 5 + src/Arch/Core/Enumerators.cs | 3 +- src/Arch/Core/Query.cs | 72 ++++++- .../Templates/QueryDescription.WithDirty.cs | 204 ++++++++++++++++++ .../Templates/QueryDescription.WithDirty.tt | 33 +++ 5 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 src/Arch/Templates/QueryDescription.WithDirty.cs create mode 100644 src/Arch/Templates/QueryDescription.WithDirty.tt diff --git a/src/Arch/Arch.csproj b/src/Arch/Arch.csproj index 20186674..49c95c1e 100644 --- a/src/Arch/Arch.csproj +++ b/src/Arch/Arch.csproj @@ -590,6 +590,11 @@ True World.CreateBulk.tt + + True + True + QueryDescription.WithDirty.tt + diff --git a/src/Arch/Core/Enumerators.cs b/src/Arch/Core/Enumerators.cs index 6e45292f..f63f87a0 100644 --- a/src/Arch/Core/Enumerators.cs +++ b/src/Arch/Core/Enumerators.cs @@ -171,7 +171,7 @@ public QueryArchetypeEnumerator GetEnumerator() /// represents an enumerator with which one can iterate over all non empty 's that matches the given . /// [SkipLocalsInit] -public ref struct QueryChunkEnumerator +public ref partial struct QueryChunkEnumerator { private QueryArchetypeEnumerator _archetypeEnumerator; private int _index; @@ -538,4 +538,3 @@ public RangeEnumerator GetEnumerator() return new RangeEnumerator(_threads, _size); } } - diff --git a/src/Arch/Core/Query.cs b/src/Arch/Core/Query.cs index feed4a1e..c9d7f558 100644 --- a/src/Arch/Core/Query.cs +++ b/src/Arch/Core/Query.cs @@ -487,6 +487,9 @@ public override int GetHashCode() hash = (hash * 23) + Any.GetHashCode(); hash = (hash * 23) + None.GetHashCode(); hash = (hash * 23) + Exclusive.GetHashCode(); +#if DIRTY_FLAGS + hash = (hash * 23) + Dirty.GetHashCode(); +#endif _hashCode = hash; return hash; } @@ -561,6 +564,7 @@ internal Query(Archetypes allArchetypes, QueryDescription description) _any = description.Any; _none = description.None; _exclusive = description.Exclusive; + _dirty = description.Dirty; // Handle exclusive. if (description.Exclusive.Count != 0) @@ -578,7 +582,10 @@ internal Query(Archetypes allArchetypes, QueryDescription description) /// True if it matches, otherwise false. public bool Matches(BitSet bitset) { - return _isExclusive ? _exclusive.Exclusive(bitset) : _all.All(bitset) && _any.Any(bitset) && _none.None(bitset); + return _isExclusive + ? _exclusive.Exclusive(bitset) + : _all.All(bitset) && _any.Any(bitset) && _none.None(bitset); + ; } /// @@ -667,10 +674,13 @@ public override int GetHashCode() { unchecked { - var hashCode = _any is not null ? _any.GetHashCode() : 0; - hashCode = (hashCode * 397) ^ (_all is not null ? _all.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (_none is not null ? _none.GetHashCode() : 0); + var hashCode = _any?.GetHashCode() ?? 0; + hashCode = (hashCode * 397) ^ (_all?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (_none?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (_exclusive?.GetHashCode() ?? 0); +#if DIRTY_FLAGS + hashCode = (hashCode * 397) ^ (_dirty?.GetHashCode() ?? 0); +#endif hashCode = (hashCode * 397) ^ _queryDescription.GetHashCode(); return hashCode; @@ -699,3 +709,57 @@ public override int GetHashCode() return !left.Equals(right); } } + + +// Dirty flags support +#if DIRTY_FLAGS + +public partial struct QueryDescription +{ + /// + /// A of all components that an should have and which should be flagged as dirty. + /// If the content of the array is subsequently changed, a should be carried out. + /// + public Signature Dirty { get; private set; } = Signature.Null; + + /// + /// Initializes a new instance of the struct. + /// + /// An array of all components that an should have mandatory. + /// An array of all components of which an should have at least one. + /// An array of all components of which an should not have any. + /// All components that an should have mandatory. + /// All components that an should have and which should be flagged as dirty. + + public QueryDescription(Signature? all = null, Signature? any = null, Signature? none = null, Signature? exclusive = null, Signature? dirty = null) + { + All = all ?? All; + Any = any ?? Any; + None = none ?? None; + Exclusive = exclusive ?? Exclusive; + Dirty = dirty ?? Dirty; + + _hashCode = -1; + _hashCode = GetHashCode(); + } + + /// + /// All components that an should have and which should be flagged as dirty. + /// + /// The generic type. + /// The same instance for chained operations. + [UnscopedRef] + public ref QueryDescription WithDirty() + { + Exclusive = Component.Signature; + Build(); + return ref this; + } +} + +public partial class Query +{ + private readonly BitSet _dirty; +} + +#endif diff --git a/src/Arch/Templates/QueryDescription.WithDirty.cs b/src/Arch/Templates/QueryDescription.WithDirty.cs new file mode 100644 index 00000000..54c4cdf8 --- /dev/null +++ b/src/Arch/Templates/QueryDescription.WithDirty.cs @@ -0,0 +1,204 @@ + + +using System; +using System.Diagnostics.Contracts; +using Arch.Core; +using Arch.Core.Utils; + +namespace Arch.Core; +public partial struct QueryDescription +{ + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + + [UnscopedRef] + public ref QueryDescription WithAll() + { + All = Component.Signature; + Build(); + return ref this; + } + } + diff --git a/src/Arch/Templates/QueryDescription.WithDirty.tt b/src/Arch/Templates/QueryDescription.WithDirty.tt new file mode 100644 index 00000000..0fe18b4c --- /dev/null +++ b/src/Arch/Templates/QueryDescription.WithDirty.tt @@ -0,0 +1,33 @@ +<#@ template language="C#" #> +<#@ output extension=".cs" #> +<#@ import namespace="System.Text" #> +<#@ include file="Helpers.ttinclude" #> + +using System; +using System.Diagnostics.Contracts; +using Arch.Core; +using Arch.Core.Utils; + +namespace Arch.Core; +public partial struct QueryDescription +{ +#if DIRTY_FLAGS + <# + for (var index = 2; index <= Amount; index++) + { + var generics = AppendGenerics(index); + #> + + [UnscopedRef] + public ref QueryDescription WithDirty<<#= generics #>>() + { + All = Component<<#= generics #>>.Signature; + Build(); + return ref this; + } + <# + } + #> +#endif +} + From b9caf7bce7fd829b2b082717ccd8158053e6ac47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Tue, 26 Aug 2025 20:13:57 -0300 Subject: [PATCH 04/13] Add missing "IsDirty" methods to archetype, world and extensions --- src/Arch/Core/Archetype.cs | 12 ++++++++++ src/Arch/Core/Extensions/EntityExtensions.cs | 14 ++++++++++++ src/Arch/Core/World.cs | 24 ++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index 9ef27875..21cb89d1 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -964,6 +964,18 @@ internal static void CopyComponents(Archetype source, ref Slot fromSlot, Archety public sealed partial class Archetype { + /// + /// Checks whether the component of an at a given has been flagged dirty. + /// + /// The at which the component of an is to be checked. + /// The component type. + /// True if the component is dirty, false otherwise. + public bool IsDirty(ref Slot slot, ComponentType componentType) + { + ref var chunk = ref GetChunk(slot.ChunkIndex); + return chunk.IsDirty(slot.Index, componentType); + } + /// /// Flags the component of an at a given as dirty. /// diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs index e824dd4f..f0b0ae5a 100644 --- a/src/Arch/Core/Extensions/EntityExtensions.cs +++ b/src/Arch/Core/Extensions/EntityExtensions.cs @@ -365,6 +365,20 @@ public static partial class EntityExtensions #if DIRTY_FLAGS && !PURE_ECS + /// + public static bool IsDirty(this Entity entity) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + return world.IsDirty(entity); + } + + /// + public static bool IsDirty(this Entity entity, ComponentType type) + { + var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); + return world.IsDirty(entity, type); + } + /// public static void SetDirty(this Entity entity) { diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index 6590a0bd..1262a463 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -1750,6 +1750,30 @@ public Signature GetSignature(Entity entity) public partial class World { + + /// + /// Checks whether the component of an has been flagged dirty. + /// + /// The component type. + /// The . + public bool IsDirty(Entity entity) + { + var componentType = Component.ComponentType; + var entityData = EntityInfo.GetEntityData(entity.Id); + return entityData.Archetype.IsDirty(ref entityData.Slot, componentType); + } + + /// + /// Checks whether the component of an has been flagged dirty. + /// + /// The . + /// The component . + public bool IsDirty(Entity entity, ComponentType type) + { + var entityData = EntityInfo.GetEntityData(entity.Id); + return entityData.Archetype.IsDirty(ref entityData.Slot, type); + } + /// /// Flags the component of type an as dirty. /// From d07cb03660baf5e35ab98f71acec2e6758810cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Fri, 5 Sep 2025 14:03:32 -0300 Subject: [PATCH 05/13] Fix query constructor and builder template errors --- src/Arch/Core/Query.cs | 4 + .../Templates/QueryDescription.WithDirty.cs | 149 +++++++++--------- .../Templates/QueryDescription.WithDirty.tt | 6 +- 3 files changed, 83 insertions(+), 76 deletions(-) diff --git a/src/Arch/Core/Query.cs b/src/Arch/Core/Query.cs index c9d7f558..aa6fbbb7 100644 --- a/src/Arch/Core/Query.cs +++ b/src/Arch/Core/Query.cs @@ -356,6 +356,8 @@ public QueryDescription() _hashCode = -1; } +#if !DIRTY_FLAGS + /// /// Initializes a new instance of the struct. /// @@ -374,6 +376,8 @@ public QueryDescription(Signature? all = null, Signature? any = null, Signature? _hashCode = GetHashCode(); } +#endif + /// /// Builds this instance by calculating a new . /// Is actually only needed if the passed arrays are changed afterwards. diff --git a/src/Arch/Templates/QueryDescription.WithDirty.cs b/src/Arch/Templates/QueryDescription.WithDirty.cs index 54c4cdf8..d3055951 100644 --- a/src/Arch/Templates/QueryDescription.WithDirty.cs +++ b/src/Arch/Templates/QueryDescription.WithDirty.cs @@ -1,4 +1,4 @@ - +#if DIRTY_FLAGS using System; using System.Diagnostics.Contracts; @@ -8,197 +8,198 @@ namespace Arch.Core; public partial struct QueryDescription { - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - + [UnscopedRef] - public ref QueryDescription WithAll() + public ref QueryDescription WithDirty() { - All = Component.Signature; + Dirty = Component.Signature; Build(); return ref this; } - } +} +#endif diff --git a/src/Arch/Templates/QueryDescription.WithDirty.tt b/src/Arch/Templates/QueryDescription.WithDirty.tt index 0fe18b4c..54075c40 100644 --- a/src/Arch/Templates/QueryDescription.WithDirty.tt +++ b/src/Arch/Templates/QueryDescription.WithDirty.tt @@ -3,6 +3,8 @@ <#@ import namespace="System.Text" #> <#@ include file="Helpers.ttinclude" #> +#if DIRTY_FLAGS + using System; using System.Diagnostics.Contracts; using Arch.Core; @@ -11,7 +13,6 @@ using Arch.Core.Utils; namespace Arch.Core; public partial struct QueryDescription { -#if DIRTY_FLAGS <# for (var index = 2; index <= Amount; index++) { @@ -28,6 +29,7 @@ public partial struct QueryDescription <# } #> -#endif } +#endif + From dd88c31f29306f587a34aa735fafe7803e62f365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Sat, 6 Sep 2025 15:13:04 -0300 Subject: [PATCH 06/13] Revert changes to BitSet _max decrement, will not be needed --- src/Arch/Core/Utils/BitSet.cs | 56 ++--------------------------------- 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs index 45a4e634..797c0877 100644 --- a/src/Arch/Core/Utils/BitSet.cs +++ b/src/Arch/Core/Utils/BitSet.cs @@ -36,15 +36,17 @@ public static int RequiredLength(int id) /// private uint[] _bits; + /// TODO: Update on ClearBit, however clearbit is only used in tests so its fine for now. /// /// The highest bit set. /// private int _highestBit; + /// TODO: Update on ClearBit, probably remove in favor? /// /// The maximum -index current in use. /// - private int _max; // TODO: probably remove in favor? + private int _max; /// /// Initializes a new instance of the class. @@ -117,15 +119,6 @@ public bool IsSet(int index) return (_bits[b] & (1 << (index & BitSize))) != 0; } - /// - /// Checks whether any bit is set. - /// - /// True if it is, otherwise false - public bool IsAnyBitSet() - { - return _highestBit >= 0; - } - /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. @@ -158,47 +151,6 @@ public void ClearBit(int index) } _bits[b] &= ~(1u << (index & BitSize)); - - // Update _highestBit and _max only if we cleared the previous highest bit - if (index != _highestBit) - { - return; - } - - for (int i = _bits.Length - 1; i >= 0; i--) - { - uint val = _bits[i]; - if (val != 0) - { - -#if NET7_0_OR_GREATER - - // Compute highest set bit using LeadingZeroCount - int highestInBlock = BitSize - BitOperations.LeadingZeroCount(val) + 1; - _highestBit = (i << IndexSize) + (highestInBlock - 1); - _max = (_highestBit / (BitSize + 1)) + 1; - return; - -#else - // Find the highest set bit in this block - int bitPos = BitSize; // usually 31 - while (bitPos >= 0) - { - if ((val & (1u << bitPos)) != 0) - { - _highestBit = (i << IndexSize) + bitPos; - _max = (_highestBit / (BitSize + 1)) + 1; - return; - } - bitPos--; - } -#endif - } - } - - // No bits left - _highestBit = -1; - _max = 0; } /// @@ -222,8 +174,6 @@ public void SetAll() public void ClearAll() { Array.Clear(_bits, 0, _bits.Length); - _highestBit = -1; - _max = 0; } /// From a3d936548740542152de9c92659de67a0f1bc463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Sat, 6 Sep 2025 15:29:52 -0300 Subject: [PATCH 07/13] Rename "dirty" to "changed" and implement a shortcut any-changed bitset for each component in a chunk --- src/Arch/Arch.csproj | 16 +-- src/Arch/Core/Archetype.cs | 42 ++++---- src/Arch/Core/Chunk.cs | 81 +++++++-------- src/Arch/Core/Extensions/EntityExtensions.cs | 44 ++++----- src/Arch/Core/Query.cs | 32 +++--- src/Arch/Core/World.cs | 44 ++++----- .../Templates/QueryDescription.WithDirty.cs | 98 +++++++++---------- .../Templates/QueryDescription.WithDirty.tt | 4 +- 8 files changed, 177 insertions(+), 184 deletions(-) diff --git a/src/Arch/Arch.csproj b/src/Arch/Arch.csproj index 49c95c1e..a61a66b4 100644 --- a/src/Arch/Arch.csproj +++ b/src/Arch/Arch.csproj @@ -41,29 +41,29 @@ - TRACE;DIRTY_FLAGS; + TRACE;CHANGED_FLAGS; false AnyCPU - TRACE;PURE_ECS;DIRTY_FLAGS; + TRACE;PURE_ECS;CHANGED_FLAGS; - TRACE;EVENTS;DIRTY_FLAGS; + TRACE;EVENTS;CHANGED_FLAGS; - TRACE;DIRTY_FLAGS; + TRACE;CHANGED_FLAGS; - TRACE;EVENTS;DIRTY_FLAGS; + TRACE;EVENTS;CHANGED_FLAGS; - TRACE;PURE_ECS;DIRTY_FLAGS; + TRACE;PURE_ECS;CHANGED_FLAGS; @@ -590,10 +590,10 @@ True World.CreateBulk.tt - + True True - QueryDescription.WithDirty.tt + QueryDescription.WithChanged.tt diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index 21cb89d1..a32f1825 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -960,76 +960,76 @@ internal static void CopyComponents(Archetype source, ref Slot fromSlot, Archety } -#if DIRTY_FLAGS +#if CHANGED_FLAGS public sealed partial class Archetype { /// - /// Checks whether the component of an at a given has been flagged dirty. + /// Checks whether the component of an at a given has been flagged changed. /// /// The at which the component of an is to be checked. /// The component type. - /// True if the component is dirty, false otherwise. - public bool IsDirty(ref Slot slot, ComponentType componentType) + /// True if the component is changed, false otherwise. + public bool IsChanged(ref Slot slot, ComponentType componentType) { ref var chunk = ref GetChunk(slot.ChunkIndex); - return chunk.IsDirty(slot.Index, componentType); + return chunk.IsChanged(slot.Index, componentType); } /// - /// Flags the component of an at a given as dirty. + /// Flags the component of an at a given as changed. /// - /// The at which the component of an is to be marked dirty. + /// The at which the component of an is to be marked changed. /// The component type. - internal void SetDirty(ref Slot slot, ComponentType componentType) + internal void SetChanged(ref Slot slot, ComponentType componentType) { ref var chunk = ref GetChunk(slot.ChunkIndex); - chunk.SetDirty(slot.Index, componentType); + chunk.SetChanged(slot.Index, componentType); } /// - /// Clears the dirty flag of the component of an at a given . + /// Clears the changed flag of the component of an at a given . /// /// The at which the component of an is to be cleared. /// The component type. - internal void ClearDirty(ref Slot slot, ComponentType componentType) + internal void ClearChanged(ref Slot slot, ComponentType componentType) { ref var chunk = ref GetChunk(slot.ChunkIndex); - chunk.ClearDirty(slot.Index, componentType); + chunk.ClearChanged(slot.Index, componentType); } /// - /// Clears the dirty flag for all components of an at a given . + /// Clears the changed flag for all components of an at a given . /// /// The slot. - internal void ClearDirty(ref Slot slot) + internal void ClearChanged(ref Slot slot) { ref var chunk = ref GetChunk(slot.ChunkIndex); - chunk.ClearDirty(slot.Index); + chunk.ClearChanged(slot.Index); } /// - /// Clears all the dirty flags in this Archetype. + /// Clears all the changed flags in this Archetype. /// - internal void ClearAllDirty() + internal void ClearAllChanged() { for (var i = 0; i < Chunks.Count; i++) { ref var chunk = ref Chunks[i]; - chunk.ClearAllDirty(); + chunk.ClearAllChanged(); } } /// - /// Clears all the dirty flags for the specified component type in this Archetype. + /// Clears all the changed flags for the specified component type in this Archetype. /// /// The type. - internal void ClearDirty(ComponentType type) + internal void ClearChanged(ComponentType type) { for (var i = 0; i < Chunks.Count; i++) { ref var chunk = ref Chunks[i]; - chunk.ClearAllDirty(type); + chunk.ClearAllChanged(type); } } } diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index 502e49b6..d60e3eaf 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -173,12 +173,13 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span Components[index] = ArrayRegistry.GetArray(type, Capacity); } -#if DIRTY_FLAGS - DirtyFlags = new BitSet[types.Length]; +#if CHANGED_FLAGS + AnyChangedFlags = new BitSet(types.Length); + ChangedFlags = new BitSet[types.Length]; for (var i = 0; i < types.Length; i++) { - DirtyFlags[i] = new BitSet(Capacity); + ChangedFlags[i] = new BitSet(Capacity); } #endif } @@ -668,100 +669,92 @@ internal int Transfer(int index, ref Chunk chunk) } } -#if DIRTY_FLAGS +#if CHANGED_FLAGS public partial struct Chunk { - public readonly BitSet[] DirtyFlags { [Pure] get; } + //Shortcut to avoid checking per-entity flags in the chunk iterator + public readonly BitSet AnyChangedFlags; + public readonly BitSet[] ChangedFlags; - /// - /// Checks whether any component of the given type has been flagged dirty. + /// Checks whether any component of the given type has been flagged changed. /// /// The component type. - /// True if the component is dirty, false otherwise. - public bool IsAnyDirty(ComponentType type) + /// True if the component is changed, false otherwise. + public bool IsAnyChanged(ComponentType type) { var compIndex = Index(type); - return DirtyFlags.DangerousGetReferenceAt(compIndex).IsAnyBitSet(); + return AnyChangedFlags.IsSet(compIndex); } /// - /// Checks whether the component at the given index has been flagged dirty. + /// Checks whether the component at the given index has been flagged changed. /// /// The index. /// The component type. - /// True if the component is dirty, false otherwise. - public bool IsDirty(int index, ComponentType type) + /// True if the component is changed, false otherwise. + public bool IsChanged(int index, ComponentType type) { var compIndex = Index(type); - return DirtyFlags.DangerousGetReferenceAt(compIndex).IsSet(index); - } - - /// - /// Flags the specified component type as dirty, for all indices in this chunk. - /// - /// The type. - public void SetAllDirty(ComponentType type) - { - // TODO add a single per-component flag to avoid setting and clearing all the bits - var index = Index(type); - DirtyFlags.DangerousGetReferenceAt(index).SetAll(); + return ChangedFlags.DangerousGetReferenceAt(compIndex).IsSet(index); } /// - /// Flags the component at the given index as dirty. + /// Flags the component at the given index as changed. /// /// The index. /// The component type. - public void SetDirty(int index, ComponentType type) + public void SetChanged(int index, ComponentType type) { - // TODO add a single per-component flag to avoid setting and clearing all the bits var compIndex = Index(type); - DirtyFlags.DangerousGetReferenceAt(compIndex).SetBit(index); + AnyChangedFlags.SetBit(compIndex); + ChangedFlags.DangerousGetReferenceAt(compIndex).SetBit(index); } /// - /// Clears all the dirty flags in this Chunk. + /// Clears all the changed flags in this Chunk. /// - public void ClearAllDirty() + public void ClearAllChanged() { - for (var i = 0; i < DirtyFlags.Length; i++) + AnyChangedFlags.ClearAll(); + for (var i = 0; i < ChangedFlags.Length; i++) { - var flags = DirtyFlags.DangerousGetReferenceAt(i); + var flags = ChangedFlags.DangerousGetReferenceAt(i); flags.ClearAll(); } } /// - /// Clears all the dirty flags for the specified component type in this Chunk. + /// Clears all the changed flags for the specified component type in this Chunk. /// /// The type. - public void ClearAllDirty(ComponentType type) + public void ClearAllChanged(ComponentType type) { - var index = Index(type); - DirtyFlags.DangerousGetReferenceAt(index).ClearAll(); + var compIndex = Index(type); + AnyChangedFlags.ClearBit(compIndex); + ChangedFlags.DangerousGetReferenceAt(compIndex).ClearAll(); } /// - /// Clears the dirty flag for the specified component type at the specified index. + /// Clears the changed flag for the specified component type at the specified index. /// /// The type. /// The index. - public void ClearDirty(int index, ComponentType type) + public void ClearChanged(int index, ComponentType type) { var compIndex = Index(type); - DirtyFlags.DangerousGetReferenceAt(compIndex).ClearBit(index); + ChangedFlags.DangerousGetReferenceAt(compIndex).ClearBit(index); } /// - /// Clears the dirty flag for all components at the specified index. + /// Clears the changed flag for all components at the specified index. /// /// The index. - public void ClearDirty(int index) + public void ClearChanged(int index) { - for (var i = 0; i < DirtyFlags.Length; i++) + for (var i = 0; i < ChangedFlags.Length; i++) { - var flags = DirtyFlags.DangerousGetReferenceAt(i); + var flags = ChangedFlags.DangerousGetReferenceAt(i); flags.ClearBit(index); } } diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs index f0b0ae5a..ede3e629 100644 --- a/src/Arch/Core/Extensions/EntityExtensions.cs +++ b/src/Arch/Core/Extensions/EntityExtensions.cs @@ -363,55 +363,55 @@ public static void RemoveRange(this in Entity entity, Span types) public static partial class EntityExtensions { -#if DIRTY_FLAGS && !PURE_ECS +#if CHANGED_FLAGS && !PURE_ECS - /// - public static bool IsDirty(this Entity entity) + /// + public static bool IsChanged(this Entity entity) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - return world.IsDirty(entity); + return world.IsChanged(entity); } - /// - public static bool IsDirty(this Entity entity, ComponentType type) + /// + public static bool IsChanged(this Entity entity, ComponentType type) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - return world.IsDirty(entity, type); + return world.IsChanged(entity, type); } - /// - public static void SetDirty(this Entity entity) + /// + public static void SetChanged(this Entity entity) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.SetDirty(entity); + world.SetChanged(entity); } - /// - public static void SetDirty(this Entity entity, ComponentType componentType) + /// + public static void SetChanged(this Entity entity, ComponentType componentType) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.SetDirty(entity, componentType); + world.SetChanged(entity, componentType); } - /// - public static void ClearDirty(this Entity entity) + /// + public static void ClearChanged(this Entity entity) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.ClearDirty(entity); + world.ClearChanged(entity); } - /// - public static void ClearDirty(this Entity entity, ComponentType componentType) + /// + public static void ClearChanged(this Entity entity, ComponentType componentType) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.ClearDirty(entity, componentType); + world.ClearChanged(entity, componentType); } - /// - public static void ClearDirty(this Entity entity) + /// + public static void ClearChanged(this Entity entity) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.ClearDirty(entity); + world.ClearChanged(entity); } #endif diff --git a/src/Arch/Core/Query.cs b/src/Arch/Core/Query.cs index aa6fbbb7..8c9d51c0 100644 --- a/src/Arch/Core/Query.cs +++ b/src/Arch/Core/Query.cs @@ -356,7 +356,7 @@ public QueryDescription() _hashCode = -1; } -#if !DIRTY_FLAGS +#if !CHANGED_FLAGS /// /// Initializes a new instance of the struct. @@ -491,8 +491,8 @@ public override int GetHashCode() hash = (hash * 23) + Any.GetHashCode(); hash = (hash * 23) + None.GetHashCode(); hash = (hash * 23) + Exclusive.GetHashCode(); -#if DIRTY_FLAGS - hash = (hash * 23) + Dirty.GetHashCode(); +#if CHANGED_FLAGS + hash = (hash * 23) + Changed.GetHashCode(); #endif _hashCode = hash; return hash; @@ -568,7 +568,7 @@ internal Query(Archetypes allArchetypes, QueryDescription description) _any = description.Any; _none = description.None; _exclusive = description.Exclusive; - _dirty = description.Dirty; + _changed = description.Changed; // Handle exclusive. if (description.Exclusive.Count != 0) @@ -682,8 +682,8 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ (_all?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (_none?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (_exclusive?.GetHashCode() ?? 0); -#if DIRTY_FLAGS - hashCode = (hashCode * 397) ^ (_dirty?.GetHashCode() ?? 0); +#if CHANGED_FLAGS + hashCode = (hashCode * 397) ^ (_changed?.GetHashCode() ?? 0); #endif hashCode = (hashCode * 397) ^ _queryDescription.GetHashCode(); @@ -715,16 +715,16 @@ public override int GetHashCode() } -// Dirty flags support -#if DIRTY_FLAGS +// Changed flags support +#if CHANGED_FLAGS public partial struct QueryDescription { /// - /// A of all components that an should have and which should be flagged as dirty. + /// A of all components that an should have and which should be flagged as changed. /// If the content of the array is subsequently changed, a should be carried out. /// - public Signature Dirty { get; private set; } = Signature.Null; + public Signature Changed { get; private set; } = Signature.Null; /// /// Initializes a new instance of the struct. @@ -733,27 +733,27 @@ public partial struct QueryDescription /// An array of all components of which an should have at least one. /// An array of all components of which an should not have any. /// All components that an should have mandatory. - /// All components that an should have and which should be flagged as dirty. + /// All components that an should have and which should be flagged as changed. - public QueryDescription(Signature? all = null, Signature? any = null, Signature? none = null, Signature? exclusive = null, Signature? dirty = null) + public QueryDescription(Signature? all = null, Signature? any = null, Signature? none = null, Signature? exclusive = null, Signature? changed = null) { All = all ?? All; Any = any ?? Any; None = none ?? None; Exclusive = exclusive ?? Exclusive; - Dirty = dirty ?? Dirty; + Changed = changed ?? Changed; _hashCode = -1; _hashCode = GetHashCode(); } /// - /// All components that an should have and which should be flagged as dirty. + /// All components that an should have and which should be flagged as changed. /// /// The generic type. /// The same instance for chained operations. [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { Exclusive = Component.Signature; Build(); @@ -763,7 +763,7 @@ public ref QueryDescription WithDirty() public partial class Query { - private readonly BitSet _dirty; + private readonly BitSet _changed; } #endif diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index 1262a463..ec4b2b8e 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -1746,88 +1746,88 @@ public Signature GetSignature(Entity entity) #endregion -#if DIRTY_FLAGS +#if CHANGED_FLAGS public partial class World { /// - /// Checks whether the component of an has been flagged dirty. + /// Checks whether the component of an has been flagged changed. /// /// The component type. /// The . - public bool IsDirty(Entity entity) + public bool IsChanged(Entity entity) { var componentType = Component.ComponentType; var entityData = EntityInfo.GetEntityData(entity.Id); - return entityData.Archetype.IsDirty(ref entityData.Slot, componentType); + return entityData.Archetype.IsChanged(ref entityData.Slot, componentType); } /// - /// Checks whether the component of an has been flagged dirty. + /// Checks whether the component of an has been flagged changed. /// /// The . /// The component . - public bool IsDirty(Entity entity, ComponentType type) + public bool IsChanged(Entity entity, ComponentType type) { var entityData = EntityInfo.GetEntityData(entity.Id); - return entityData.Archetype.IsDirty(ref entityData.Slot, type); + return entityData.Archetype.IsChanged(ref entityData.Slot, type); } /// - /// Flags the component of type an as dirty. + /// Flags the component of type an as changed. /// /// The component type. /// The . - public void SetDirty(Entity entity) + public void SetChanged(Entity entity) { var componentType = Component.ComponentType; var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.SetDirty(ref entityData.Slot, componentType); + entityData.Archetype.SetChanged(ref entityData.Slot, componentType); } /// - /// Flags the component of type an as dirty. + /// Flags the component of type an as changed. /// /// The . /// The component . - public void SetDirty(Entity entity, ComponentType type) + public void SetChanged(Entity entity, ComponentType type) { var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.SetDirty(ref entityData.Slot, type); + entityData.Archetype.SetChanged(ref entityData.Slot, type); } /// - /// Clears the dirty flag of the component of an . + /// Clears the changed flag of the component of an . /// /// The component type. /// The . - public void ClearDirty(Entity entity) + public void ClearChanged(Entity entity) { var componentType = Component.ComponentType; var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.ClearDirty(ref entityData.Slot, componentType); + entityData.Archetype.ClearChanged(ref entityData.Slot, componentType); } /// - /// Clears the dirty flag of the component of an . + /// Clears the changed flag of the component of an . /// /// The . /// The component . - public void ClearDirty(Entity entity, ComponentType type) + public void ClearChanged(Entity entity, ComponentType type) { var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.ClearDirty(ref entityData.Slot, type); + entityData.Archetype.ClearChanged(ref entityData.Slot, type); } /// - /// Clears the dirty flag for all components of an . + /// Clears the changed flag for all components of an . /// /// The . - public void ClearDirty(Entity entity) + public void ClearChanged(Entity entity) { var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.ClearDirty(ref entityData.Slot); + entityData.Archetype.ClearChanged(ref entityData.Slot); } } diff --git a/src/Arch/Templates/QueryDescription.WithDirty.cs b/src/Arch/Templates/QueryDescription.WithDirty.cs index d3055951..c40f9737 100644 --- a/src/Arch/Templates/QueryDescription.WithDirty.cs +++ b/src/Arch/Templates/QueryDescription.WithDirty.cs @@ -1,4 +1,4 @@ -#if DIRTY_FLAGS +#if CHANGED_FLAGS using System; using System.Diagnostics.Contracts; @@ -10,193 +10,193 @@ public partial struct QueryDescription { [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } [UnscopedRef] - public ref QueryDescription WithDirty() + public ref QueryDescription WithChanged() { - Dirty = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } diff --git a/src/Arch/Templates/QueryDescription.WithDirty.tt b/src/Arch/Templates/QueryDescription.WithDirty.tt index 54075c40..6b07f2e3 100644 --- a/src/Arch/Templates/QueryDescription.WithDirty.tt +++ b/src/Arch/Templates/QueryDescription.WithDirty.tt @@ -3,7 +3,7 @@ <#@ import namespace="System.Text" #> <#@ include file="Helpers.ttinclude" #> -#if DIRTY_FLAGS +#if CHANGED_FLAGS using System; using System.Diagnostics.Contracts; @@ -20,7 +20,7 @@ public partial struct QueryDescription #> [UnscopedRef] - public ref QueryDescription WithDirty<<#= generics #>>() + public ref QueryDescription WithChanged<<#= generics #>>() { All = Component<<#= generics #>>.Signature; Build(); From 1d112fcb738451ea584ff9a5bf74ac9a8cb0e598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Sat, 6 Sep 2025 15:47:25 -0300 Subject: [PATCH 08/13] Add early-return optimization to bitsets --- src/Arch.Tests/BitSetTest.cs | 2 +- src/Arch/Core/Utils/BitSet.cs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Arch.Tests/BitSetTest.cs b/src/Arch.Tests/BitSetTest.cs index 548025f8..8af11e14 100644 --- a/src/Arch.Tests/BitSetTest.cs +++ b/src/Arch.Tests/BitSetTest.cs @@ -51,7 +51,7 @@ public void HashSimilarity() [Test] public void BitsetSetAll() { - var bitSet = new BitSet(); + var bitSet = new BitSet(5); bitSet.SetAll(); var count = 0; diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs index 797c0877..8dcaebd6 100644 --- a/src/Arch/Core/Utils/BitSet.cs +++ b/src/Arch/Core/Utils/BitSet.cs @@ -184,6 +184,16 @@ public void ClearAll() [SkipLocalsInit] public bool All(BitSet other) { + if (Length == 0) + { + return true; + } + + if (other.Length == 0) + { + return false; + } + var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < _padding) { @@ -245,6 +255,11 @@ public bool All(BitSet other) /// True if they match, false if not. public bool Any(BitSet other) { + if (Length == 0 || other.Length == 0) + { + return false; + } + var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < _padding) { @@ -306,6 +321,11 @@ public bool Any(BitSet other) /// True if none match, false if not. public bool None(BitSet other) { + if (Length == 0 || other.Length == 0) + { + return true; + } + var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < _padding) { @@ -348,8 +368,17 @@ public bool None(BitSet other) /// True if they match, false if not. public bool Exclusive(BitSet other) { - var min = Math.Min(Math.Min(Length, other.Length), _max); + if (Length == 0 && other.Length == 0) + { + return true; + } + if (Length == 0 || other.Length == 0) + { + return false; + } + + var min = Math.Min(Math.Min(Length, other.Length), _max); if (!Vector.IsHardwareAccelerated || min < _padding) { var bits = _bits.AsSpan(); From 11bcf349bd65d21f45866288facbd35bb6de7354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Sat, 6 Sep 2025 16:03:14 -0300 Subject: [PATCH 09/13] Rename "SetChanged" to "MarkChanged" and add unit tests --- src/Arch/Core/Archetype.cs | 4 ++-- src/Arch/Core/Chunk.cs | 2 +- src/Arch/Core/Extensions/EntityExtensions.cs | 12 ++++++------ src/Arch/Core/World.cs | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index a32f1825..5a9444d4 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -981,10 +981,10 @@ public bool IsChanged(ref Slot slot, ComponentType componentType) /// /// The at which the component of an is to be marked changed. /// The component type. - internal void SetChanged(ref Slot slot, ComponentType componentType) + internal void MarkChanged(ref Slot slot, ComponentType componentType) { ref var chunk = ref GetChunk(slot.ChunkIndex); - chunk.SetChanged(slot.Index, componentType); + chunk.MarkChanged(slot.Index, componentType); } /// diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index d60e3eaf..0e855b25 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -704,7 +704,7 @@ public bool IsChanged(int index, ComponentType type) /// /// The index. /// The component type. - public void SetChanged(int index, ComponentType type) + public void MarkChanged(int index, ComponentType type) { var compIndex = Index(type); AnyChangedFlags.SetBit(compIndex); diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs index ede3e629..e258fe8e 100644 --- a/src/Arch/Core/Extensions/EntityExtensions.cs +++ b/src/Arch/Core/Extensions/EntityExtensions.cs @@ -379,18 +379,18 @@ public static bool IsChanged(this Entity entity, ComponentType type) return world.IsChanged(entity, type); } - /// - public static void SetChanged(this Entity entity) + /// + public static void Markchanged(this Entity entity) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.SetChanged(entity); + world.Markchanged(entity); } - /// - public static void SetChanged(this Entity entity, ComponentType componentType) + /// + public static void Markchanged(this Entity entity, ComponentType componentType) { var world = World.Worlds.DangerousGetReferenceAt(entity.WorldId); - world.SetChanged(entity, componentType); + world.Markchanged(entity, componentType); } /// diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index ec4b2b8e..0721daad 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -1779,11 +1779,11 @@ public bool IsChanged(Entity entity, ComponentType type) /// /// The component type. /// The . - public void SetChanged(Entity entity) + public void MarkChanged(Entity entity) { var componentType = Component.ComponentType; var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.SetChanged(ref entityData.Slot, componentType); + entityData.Archetype.MarkChanged(ref entityData.Slot, componentType); } /// @@ -1791,10 +1791,10 @@ public void SetChanged(Entity entity) /// /// The . /// The component . - public void SetChanged(Entity entity, ComponentType type) + public void MarkChanged(Entity entity, ComponentType type) { var entityData = EntityInfo.GetEntityData(entity.Id); - entityData.Archetype.SetChanged(ref entityData.Slot, type); + entityData.Archetype.MarkChanged(ref entityData.Slot, type); } /// From c2f9e0a7eaf7ff515c1d8a7ec34953e90c33cc7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 8 Sep 2025 14:57:09 -0300 Subject: [PATCH 10/13] Add change checks against multiple components and change changed flags implementation to row-first (bitset per entity) to allow checking against bitmasks per entity. --- src/Arch/Core/Chunk.cs | 71 +++++++++---------- .../Internal/ComponentTypeExtensions.cs | 26 ++++--- src/Arch/Core/Utils/BitSet.cs | 2 +- 3 files changed, 53 insertions(+), 46 deletions(-) diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index 0e855b25..9b474070 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -174,12 +174,11 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span } #if CHANGED_FLAGS - AnyChangedFlags = new BitSet(types.Length); - ChangedFlags = new BitSet[types.Length]; - - for (var i = 0; i < types.Length; i++) + _changedFlags = new BitSet[Capacity + 1]; // Last index contains the "any" bitset + var typeCapacity = ComponentTypeExtensions.GetMaxValue(types); + for (var i = 0; i < Capacity; i++) { - ChangedFlags[i] = new BitSet(Capacity); + _changedFlags[i] = new BitSet(typeCapacity); } #endif } @@ -673,18 +672,26 @@ internal int Transfer(int index, ref Chunk chunk) public partial struct Chunk { - //Shortcut to avoid checking per-entity flags in the chunk iterator - public readonly BitSet AnyChangedFlags; - public readonly BitSet[] ChangedFlags; + private readonly BitSet[] _changedFlags; + /// /// Checks whether any component of the given type has been flagged changed. /// /// The component type. /// True if the component is changed, false otherwise. public bool IsAnyChanged(ComponentType type) { - var compIndex = Index(type); - return AnyChangedFlags.IsSet(compIndex); + return _changedFlags.DangerousGetReferenceAt(Capacity).IsSet(type.Id); + } + + /// + /// Checks whether any component of the given types has been flagged changed. + /// + /// A representing the component types. + /// True if any of the components have changed, false otherwise. + public bool IsAnyChanged(BitSet types) + { + return _changedFlags.DangerousGetReferenceAt(Capacity).Any(types); } /// @@ -695,8 +702,18 @@ public bool IsAnyChanged(ComponentType type) /// True if the component is changed, false otherwise. public bool IsChanged(int index, ComponentType type) { - var compIndex = Index(type); - return ChangedFlags.DangerousGetReferenceAt(compIndex).IsSet(index); + return _changedFlags.DangerousGetReferenceAt(index).IsSet(type.Id); + } + + /// + /// Checks whether any of the components at the given index has been flagged changed. + /// + /// The index. + /// A representing the component types. + /// True if the component is changed, false otherwise. + public bool IsChanged(int index, BitSet types) + { + return _changedFlags.DangerousGetReferenceAt(index).Any(types); } /// @@ -706,9 +723,8 @@ public bool IsChanged(int index, ComponentType type) /// The component type. public void MarkChanged(int index, ComponentType type) { - var compIndex = Index(type); - AnyChangedFlags.SetBit(compIndex); - ChangedFlags.DangerousGetReferenceAt(compIndex).SetBit(index); + _changedFlags.DangerousGetReferenceAt(index).SetBit(type.Id); + _changedFlags.DangerousGetReferenceAt(Capacity).SetBit(type.Id); } /// @@ -716,25 +732,13 @@ public void MarkChanged(int index, ComponentType type) /// public void ClearAllChanged() { - AnyChangedFlags.ClearAll(); - for (var i = 0; i < ChangedFlags.Length; i++) + for (var i = 0; i < _changedFlags.Length; i++) { - var flags = ChangedFlags.DangerousGetReferenceAt(i); + var flags = _changedFlags.DangerousGetReferenceAt(i); flags.ClearAll(); } } - /// - /// Clears all the changed flags for the specified component type in this Chunk. - /// - /// The type. - public void ClearAllChanged(ComponentType type) - { - var compIndex = Index(type); - AnyChangedFlags.ClearBit(compIndex); - ChangedFlags.DangerousGetReferenceAt(compIndex).ClearAll(); - } - /// /// Clears the changed flag for the specified component type at the specified index. /// @@ -742,8 +746,7 @@ public void ClearAllChanged(ComponentType type) /// The index. public void ClearChanged(int index, ComponentType type) { - var compIndex = Index(type); - ChangedFlags.DangerousGetReferenceAt(compIndex).ClearBit(index); + _changedFlags.DangerousGetReferenceAt(index).ClearBit(type.Id); } /// @@ -752,11 +755,7 @@ public void ClearChanged(int index, ComponentType type) /// The index. public void ClearChanged(int index) { - for (var i = 0; i < ChangedFlags.Length; i++) - { - var flags = ChangedFlags.DangerousGetReferenceAt(i); - flags.ClearBit(index); - } + _changedFlags.DangerousGetReferenceAt(index).ClearAll(); } } diff --git a/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs b/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs index 6f3c3215..c0a6b1fd 100644 --- a/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs +++ b/src/Arch/Core/Extensions/Internal/ComponentTypeExtensions.cs @@ -38,15 +38,7 @@ internal static int ToByteSize(this Span types) internal static int[] ToLookupArray(this Span types) { // Get maximum component ID. - var max = 0; - foreach (var type in types) - { - var componentId = type.Id; - if (componentId >= max) - { - max = componentId; - } - } + var max = GetMaxValue(types); // Create lookup table where the component ID points to the component index. var array = new int[max + 1]; @@ -61,4 +53,20 @@ internal static int[] ToLookupArray(this Span types) return array; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int GetMaxValue(Span types) + { + var max = 0; + foreach (var type in types) + { + var componentId = type.Id; + if (componentId >= max) + { + max = componentId; + } + } + + return max; + } } diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs index 8dcaebd6..e03f5ffc 100644 --- a/src/Arch/Core/Utils/BitSet.cs +++ b/src/Arch/Core/Utils/BitSet.cs @@ -53,7 +53,7 @@ public static int RequiredLength(int id) /// public BitSet() { - _bits = new uint[_padding]; + _bits = []; } /// From cddf8243a8c8d74807e8b68ed6e088fc4875e18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 8 Sep 2025 15:00:30 -0300 Subject: [PATCH 11/13] Small cleanup of archetype class --- src/Arch/Core/Archetype.cs | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index 5a9444d4..e9087ca3 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -1,14 +1,9 @@ -using System.Buffers; using System.Diagnostics.Contracts; -using Arch.Core.Extensions; using Arch.Core.Extensions.Internal; using Arch.Core.Utils; -using Arch.LowLevel; using Arch.LowLevel.Jagged; using Collections.Pooled; using CommunityToolkit.HighPerformance; -using Array = System.Array; -using System.Runtime.InteropServices; namespace Arch.Core; @@ -339,10 +334,7 @@ internal int[] LookupArray /// The number of 's within the array. /// public int ChunkCount { - get - { - return Chunks.Count; - } + get => Chunks.Count; } /// @@ -350,10 +342,7 @@ public int ChunkCount { /// The total capacity. /// public int ChunkCapacity { - get - { - return Chunks.Capacity; - } + get => Chunks.Capacity; } /// @@ -1019,19 +1008,6 @@ internal void ClearAllChanged() chunk.ClearAllChanged(); } } - - /// - /// Clears all the changed flags for the specified component type in this Archetype. - /// - /// The type. - internal void ClearChanged(ComponentType type) - { - for (var i = 0; i < Chunks.Count; i++) - { - ref var chunk = ref Chunks[i]; - chunk.ClearAllChanged(type); - } - } } #endif From 113d2c8445398865c1b2ea98470b470868f3a8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 8 Sep 2025 15:00:50 -0300 Subject: [PATCH 12/13] Add changed query tests --- src/Arch.Tests/ChangedQueryTest.cs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/Arch.Tests/ChangedQueryTest.cs diff --git a/src/Arch.Tests/ChangedQueryTest.cs b/src/Arch.Tests/ChangedQueryTest.cs new file mode 100644 index 00000000..d9e94d0a --- /dev/null +++ b/src/Arch.Tests/ChangedQueryTest.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using System.Collections.Generic; +using System.Linq; +using Arch.Core; + +namespace Arch.Tests; + +[TestFixture] +public class ChangedQueryTest +{ + private struct A; + private struct B; + private struct C; + + private World _world; + private Entity _e1, _e2, _e3, _e4, _e5, _e6; + private Entity[] _nothing = []; + + [SetUp] + public void Setup() + { + _world = World.Create(); + + _e1 = _world.Create(); + + _e2 = _world.Create(); + _world.MarkChanged(_e2); + + _e3 = _world.Create(); + _world.MarkChanged(_e3); + + _e4 = _world.Create(); + _world.MarkChanged(_e4); + + // E5 = (C*) + _e5 = _world.Create(); + _world.MarkChanged(_e5); + + // E6 = () + _e6 = _world.Create(); + } + + [Test] + public void WithChangedOnly() + { + var query = new QueryDescription().WithChanged(); + AssertMatches(query, _e2, _e3, _e4); + } + + [Test] + public void WithAnyAndChanged() + { + var query = new QueryDescription().WithAny().WithChanged(); + AssertMatches(query, _e3, _e5); + } + + [Test] + public void WithDisjointAnyAndChanged() + { + var query = new QueryDescription().WithAny().WithChanged(); + AssertMatches(query, _e2, _e3); + } + + [Test] + public void WithNoneAndChanged() + { + var query = new QueryDescription().WithNone().WithChanged(); + AssertMatches(query); + } + + [Test] + public void WithDisjointNoneAndChanged() + { + var query = new QueryDescription().WithNone().WithChanged(); + AssertMatches(query, _e5); + } + + [Test] + public void WithExclusiveAndChanged() + { + var query = new QueryDescription().WithExclusive().WithChanged(); + AssertMatches(query, _e2); + } + + [Test] + public void WithDisjointExclusiveAndChanged() + { + var query = new QueryDescription().WithExclusive().WithChanged(); + AssertMatches(query, _nothing); + } + + [Test] + public void WithDisjointAnyNoneAndChanged() + { + var query = new QueryDescription().WithAny().WithChanged().WithNone(); + AssertMatches(query, _e2); + } + + private void AssertMatches(QueryDescription query, params Entity[] expected) + { + var count = _world.CountEntities(query); + var entities = new Entity[count]; + _world.GetEntities(query, entities); + CollectionAssert.AreEquivalent(expected, entities); + } +} From 027d25abb9c2de3d968a5fa8fa4bcc0dfcc931f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquin=20Mu=C3=B1iz?= Date: Mon, 8 Sep 2025 20:52:19 -0300 Subject: [PATCH 13/13] Implement new Enumerator to go over all entities of a query while skipping non-dirty elements --- src/Arch.Tests/ChangedQueryTest.cs | 51 ++--- src/Arch/Core/Chunk.cs | 12 +- src/Arch/Core/Query.cs | 67 ++++-- src/Arch/Core/QueryEnumerators.cs | 282 ++++++++++++++++++++++++++ src/Arch/Core/Utils/AssertionUtils.cs | 10 + src/Arch/Core/Utils/BitSet.cs | 7 +- src/Arch/Core/World.cs | 87 ++++---- 7 files changed, 437 insertions(+), 79 deletions(-) create mode 100644 src/Arch/Core/QueryEnumerators.cs create mode 100644 src/Arch/Core/Utils/AssertionUtils.cs diff --git a/src/Arch.Tests/ChangedQueryTest.cs b/src/Arch.Tests/ChangedQueryTest.cs index d9e94d0a..3af50818 100644 --- a/src/Arch.Tests/ChangedQueryTest.cs +++ b/src/Arch.Tests/ChangedQueryTest.cs @@ -1,6 +1,3 @@ -using NUnit.Framework; -using System.Collections.Generic; -using System.Linq; using Arch.Core; namespace Arch.Tests; @@ -13,73 +10,71 @@ private struct B; private struct C; private World _world; - private Entity _e1, _e2, _e3, _e4, _e5, _e6; + private Entity _e0, _e1, _e2, _e3, _e4, _e5; private Entity[] _nothing = []; - [SetUp] + [OneTimeSetUp] public void Setup() { _world = World.Create(); + _e0 = _world.Create(); + _e1 = _world.Create(); + _world.MarkChanged(_e1); - _e2 = _world.Create(); + _e2 = _world.Create(); _world.MarkChanged(_e2); - _e3 = _world.Create(); + _e3 = _world.Create(); _world.MarkChanged(_e3); - _e4 = _world.Create(); - _world.MarkChanged(_e4); - - // E5 = (C*) - _e5 = _world.Create(); - _world.MarkChanged(_e5); + _e4 = _world.Create(); + _world.MarkChanged(_e4); - // E6 = () - _e6 = _world.Create(); + _e5 = _world.Create(); } [Test] public void WithChangedOnly() { var query = new QueryDescription().WithChanged(); - AssertMatches(query, _e2, _e3, _e4); + AssertMatches(query, _e1, _e2, _e3); } [Test] public void WithAnyAndChanged() { var query = new QueryDescription().WithAny().WithChanged(); - AssertMatches(query, _e3, _e5); + AssertMatches(query, _e4); } [Test] public void WithDisjointAnyAndChanged() { var query = new QueryDescription().WithAny().WithChanged(); - AssertMatches(query, _e2, _e3); + AssertMatches(query, _e1, _e2, _e3); } [Test] public void WithNoneAndChanged() { var query = new QueryDescription().WithNone().WithChanged(); - AssertMatches(query); + AssertMatches(query, _nothing); } [Test] public void WithDisjointNoneAndChanged() { var query = new QueryDescription().WithNone().WithChanged(); - AssertMatches(query, _e5); + AssertMatches(query, _e4); } [Test] public void WithExclusiveAndChanged() { var query = new QueryDescription().WithExclusive().WithChanged(); - AssertMatches(query, _e2); + AssertMatches(query, _e1); } [Test] @@ -93,14 +88,20 @@ public void WithDisjointExclusiveAndChanged() public void WithDisjointAnyNoneAndChanged() { var query = new QueryDescription().WithAny().WithChanged().WithNone(); - AssertMatches(query, _e2); + AssertMatches(query, _e1, _e3); } private void AssertMatches(QueryDescription query, params Entity[] expected) { - var count = _world.CountEntities(query); - var entities = new Entity[count]; - _world.GetEntities(query, entities); + var entities = new Entity[10]; + var total = _world.GetEntities(query, entities); + entities = entities[..total]; CollectionAssert.AreEquivalent(expected, entities); } + + [OneTimeTearDown] + public void TearDown() + { + _world.Dispose(); + } } diff --git a/src/Arch/Core/Chunk.cs b/src/Arch/Core/Chunk.cs index 9b474070..15b430ed 100644 --- a/src/Arch/Core/Chunk.cs +++ b/src/Arch/Core/Chunk.cs @@ -176,7 +176,7 @@ internal Chunk(int capacity, int[] componentIdToArrayIndex, Span #if CHANGED_FLAGS _changedFlags = new BitSet[Capacity + 1]; // Last index contains the "any" bitset var typeCapacity = ComponentTypeExtensions.GetMaxValue(types); - for (var i = 0; i < Capacity; i++) + for (var i = 0; i <= Capacity; i++) { _changedFlags[i] = new BitSet(typeCapacity); } @@ -679,6 +679,7 @@ public partial struct Chunk /// /// The component type. /// True if the component is changed, false otherwise. + [Pure] public bool IsAnyChanged(ComponentType type) { return _changedFlags.DangerousGetReferenceAt(Capacity).IsSet(type.Id); @@ -689,9 +690,11 @@ public bool IsAnyChanged(ComponentType type) /// /// A representing the component types. /// True if any of the components have changed, false otherwise. + [Pure] public bool IsAnyChanged(BitSet types) { - return _changedFlags.DangerousGetReferenceAt(Capacity).Any(types); + var changed = _changedFlags.DangerousGetReferenceAt(Capacity); + return types.Any(changed); } /// @@ -700,6 +703,7 @@ public bool IsAnyChanged(BitSet types) /// The index. /// The component type. /// True if the component is changed, false otherwise. + [Pure] public bool IsChanged(int index, ComponentType type) { return _changedFlags.DangerousGetReferenceAt(index).IsSet(type.Id); @@ -711,9 +715,11 @@ public bool IsChanged(int index, ComponentType type) /// The index. /// A representing the component types. /// True if the component is changed, false otherwise. + [Pure] public bool IsChanged(int index, BitSet types) { - return _changedFlags.DangerousGetReferenceAt(index).Any(types); + var changed = _changedFlags.DangerousGetReferenceAt(index); + return types.Any(changed); } /// diff --git a/src/Arch/Core/Query.cs b/src/Arch/Core/Query.cs index 8c9d51c0..9ce2ef52 100644 --- a/src/Arch/Core/Query.cs +++ b/src/Arch/Core/Query.cs @@ -1,7 +1,5 @@ -using Arch.Core.Extensions; using Arch.Core.Extensions.Internal; using Arch.Core.Utils; -using Collections.Pooled; using CommunityToolkit.HighPerformance; namespace Arch.Core; @@ -542,6 +540,11 @@ public partial class Query : IEquatable private readonly BitSet _none; private readonly BitSet _exclusive; +#if CHANGED_FLAGS + public readonly bool HasChangedFilter; + public readonly BitSet Changed; +#endif + private readonly bool _isExclusive; /// @@ -565,10 +568,25 @@ internal Query(Archetypes allArchetypes, QueryDescription description) // Convert to `BitSet`s. _all = description.All; - _any = description.Any; _none = description.None; _exclusive = description.Exclusive; - _changed = description.Changed; + +#if CHANGED_FLAGS + if (description.Changed.Count > 0) + { + Changed = description.Changed; + _any = Signature.Add(description.Any, description.Changed); + HasChangedFilter = true; + } + else + { + Changed = new BitSet(); + _any = description.Any; + HasChangedFilter = false; + } +#else + _any = description.Any; +#endif // Handle exclusive. if (description.Exclusive.Count != 0) @@ -589,7 +607,6 @@ public bool Matches(BitSet bitset) return _isExclusive ? _exclusive.Exclusive(bitset) : _all.All(bitset) && _any.Any(bitset) && _none.None(bitset); - ; } /// @@ -610,8 +627,7 @@ private void Match() _matchingArchetypes.Clear(); foreach (var archetype in allArchetypes) { - var matches = Matches(archetype.BitSet); - if (matches) + if (Matches(archetype.BitSet)) { _matchingArchetypes.Add(archetype); } @@ -650,6 +666,18 @@ public QueryChunkEnumerator GetEnumerator() return new QueryChunkEnumerator(_matchingArchetypes.AsSpan()); } + public QueryEntityEnumerator GetEntityEnumerator() + { + Match(); + return new QueryEntityEnumerator(this); + } + + public QueryComponentEnumerator GetComponentEnumerator() + { + Match(); + return new QueryComponentEnumerator(this); + } + /// /// Checks this for equality with another. /// @@ -657,7 +685,14 @@ public QueryChunkEnumerator GetEnumerator() /// True if they are equal, false if not. public bool Equals(Query other) { - return Equals(_any, other._any) && Equals(_all, other._all) && Equals(_none, other._none) && Equals(_exclusive, other._exclusive) && _queryDescription.Equals(other._queryDescription); + return Equals(_any, other._any) && + Equals(_all, other._all) && + Equals(_none, other._none) && +#if CHANGED_FLAGS + Equals(Changed, other.Changed) && +#endif + Equals(_exclusive, other._exclusive) && + _queryDescription.Equals(other._queryDescription); } /// @@ -683,7 +718,7 @@ public override int GetHashCode() hashCode = (hashCode * 397) ^ (_none?.GetHashCode() ?? 0); hashCode = (hashCode * 397) ^ (_exclusive?.GetHashCode() ?? 0); #if CHANGED_FLAGS - hashCode = (hashCode * 397) ^ (_changed?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Changed?.GetHashCode() ?? 0); #endif hashCode = (hashCode * 397) ^ _queryDescription.GetHashCode(); @@ -741,7 +776,12 @@ public QueryDescription(Signature? all = null, Signature? any = null, Signature? Any = any ?? Any; None = none ?? None; Exclusive = exclusive ?? Exclusive; - Changed = changed ?? Changed; + + if (changed != null) + { + Changed = changed.Value; + Any = Signature.Add(Any, Changed); + } _hashCode = -1; _hashCode = GetHashCode(); @@ -755,15 +795,10 @@ public QueryDescription(Signature? all = null, Signature? any = null, Signature? [UnscopedRef] public ref QueryDescription WithChanged() { - Exclusive = Component.Signature; + Changed = Component.Signature; Build(); return ref this; } } -public partial class Query -{ - private readonly BitSet _changed; -} - #endif diff --git a/src/Arch/Core/QueryEnumerators.cs b/src/Arch/Core/QueryEnumerators.cs new file mode 100644 index 00000000..9951ca56 --- /dev/null +++ b/src/Arch/Core/QueryEnumerators.cs @@ -0,0 +1,282 @@ +using Arch.Core.Utils; +using CommunityToolkit.HighPerformance; + +namespace Arch.Core; + +[SkipLocalsInit] +public ref struct QueryEntityEnumerator +{ +#if CHANGED_FLAGS + private readonly bool _checkChanged; + private readonly BitSet _changedMask; +#endif + + private QueryChunkEnumerator _chunkEnumerator; + private Ref _entityPtr; + private Ref _chunk; + private int _entityIndex = -1; + + public QueryEntityEnumerator(Query query) + { +#if CHANGED_FLAGS + _checkChanged = query.HasChangedFilter; + _changedMask = query.Changed; +#endif + _chunkEnumerator = query.GetEnumerator(); + } + + public readonly ref Entity Current + { + get => ref Unsafe.Add(ref _entityPtr.Value, _entityIndex); + } + + public bool MoveNext() + { +#if CHANGED_FLAGS + if (_checkChanged) + { + while (true) + { + // Move to the next entity in the current chunk + if (MoveNextChangedEntity()) + { + return true; // Return true if the next entity is valid + } + + // Move to the next chunk in the query + if (!MoveNextChangedChunk()) + { + return false; // Return false if there's no more chunks + } + } + } +#endif + + while (true) + { + // Move to the next entity in the current chunk + if (MoveNextEntity()) + { + return true; // Return true if the next entity is valid + } + + // Move to the next chunk in the query + if (!MoveNextChunk()) + { + return false; // Return false if there's no more chunks + } + } + } + + public QueryEntityEnumerator GetEnumerator() + { + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChunk() + { + while (_chunkEnumerator.MoveNext()) + { + ref var chunk = ref _chunkEnumerator.Current; + + // Skip empty chunks + if (chunk.Count <= 0) + { + continue; + } + + _chunk = new Ref(ref chunk); + _entityIndex = chunk.Count; + _entityPtr = new Ref(ref chunk.Entity(0)); + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextEntity() + { + unchecked + { + return --_entityIndex >= 0; + } + } + +#if CHANGED_FLAGS + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChangedChunk() + { + while (_chunkEnumerator.MoveNext()) + { + ref var chunk = ref _chunkEnumerator.Current; + + // Skip empty and non-changed chunks + if (chunk.Count <= 0 || !chunk.IsAnyChanged(_changedMask)) + { + continue; + } + + _chunk = new Ref(ref chunk); + _entityIndex = chunk.Count; + _entityPtr = new Ref(ref chunk.Entity(0)); + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChangedEntity() + { + unchecked + { + // Skip non-changed entities + return --_entityIndex >= 0 && _chunk.Value.IsChanged(_entityIndex, _changedMask); + } + } + +#endif +} + +[SkipLocalsInit] +public ref struct QueryComponentEnumerator +{ +#if CHANGED_FLAGS + private readonly bool _checkChanged; + private readonly BitSet _changedMask; +#endif + + private QueryChunkEnumerator _chunkEnumerator; + private Ref _cmpPtr; + private Ref _chunkPtr; + private int _entityIndex = -1; + + public QueryComponentEnumerator(Query query) + { +#if CHANGED_FLAGS + _checkChanged = query.HasChangedFilter; + _changedMask = query.Changed; +#endif + _chunkEnumerator = query.GetEnumerator(); + } + + public readonly ref T Current + { + get => ref Unsafe.Add(ref _cmpPtr.Value, _entityIndex); + } + + public bool MoveNext() + { +#if CHANGED_FLAGS + if (_checkChanged) + { + while (true) + { + // Move to the next entity in the current chunk + if (MoveNextChangedEntity()) + { + return true; // Return true if the next entity is valid + } + + // Move to the next chunk in the query + if (!MoveNextChangedChunk()) + { + return false; // Return false if there's no more chunks + } + } + } +#endif + + while (true) + { + // Move to the next entity in the current chunk + if (MoveNextEntity()) + { + return true; // Return true if the next entity is valid + } + + // Move to the next chunk in the query + if (!MoveNextChunk()) + { + return false; // Return false if there's no more chunks + } + } + } + + public QueryComponentEnumerator GetEnumerator() + { + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChunk() + { + while (_chunkEnumerator.MoveNext()) + { + ref var chunk = ref _chunkEnumerator.Current; + + // Skip empty chunks + if (chunk.Count <= 0) + { + continue; + } + + _chunkPtr = new Ref(ref chunk); + _entityIndex = chunk.Count; + _cmpPtr = new Ref(ref chunk.GetFirst()); + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextEntity() + { + unchecked + { + return --_entityIndex >= 0; + } + } + +#if CHANGED_FLAGS + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChangedChunk() + { + while (_chunkEnumerator.MoveNext()) + { + ref var chunk = ref _chunkEnumerator.Current; + + // Skip empty and non-changed chunks + if (chunk.Count <= 0 || !chunk.IsAnyChanged(_changedMask)) + { + continue; + } + + _chunkPtr = new Ref(ref chunk); + _entityIndex = chunk.Count; + _cmpPtr = new Ref(ref chunk.GetFirst()); + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNextChangedEntity() + { + unchecked + { + // Skip non-changed entities + return --_entityIndex >= 0 && _chunkPtr.Value.IsChanged(_entityIndex, _changedMask); + } + } + +#endif +} + + diff --git a/src/Arch/Core/Utils/AssertionUtils.cs b/src/Arch/Core/Utils/AssertionUtils.cs new file mode 100644 index 00000000..a5f19123 --- /dev/null +++ b/src/Arch/Core/Utils/AssertionUtils.cs @@ -0,0 +1,10 @@ +namespace Arch.Core.Utils; + +public static class AssertionUtils +{ + [Conditional("DEBUG"), Conditional("CHANGED_FLAGS")] + public static void AssertNoChangedFilter(QueryDescription queryDescription, [CallerMemberName] string? callerName = null) + { + Debug.Assert(queryDescription.Changed.Count == 0, $"The method {callerName} does not support queries with dirty filters."); + } +} diff --git a/src/Arch/Core/Utils/BitSet.cs b/src/Arch/Core/Utils/BitSet.cs index e03f5ffc..e39f7c9f 100644 --- a/src/Arch/Core/Utils/BitSet.cs +++ b/src/Arch/Core/Utils/BitSet.cs @@ -255,7 +255,12 @@ public bool All(BitSet other) /// True if they match, false if not. public bool Any(BitSet other) { - if (Length == 0 || other.Length == 0) + if (Length == 0) + { + return true; + } + + if (other.Length == 0) { return false; } diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index 0721daad..1c7b1fe5 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -407,6 +407,8 @@ public Query Query(in QueryDescription queryDescription) [Pure] public int CountEntities(in QueryDescription queryDescription) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + var counter = 0; var query = Query(in queryDescription); foreach (var archetype in query.GetArchetypeIterator()) @@ -424,20 +426,19 @@ public int CountEntities(in QueryDescription queryDescription) /// The which specifies the components or s for which to search. /// The receiving the found s. /// The start index inside the . Default is 0. - public void GetEntities(in QueryDescription queryDescription, Span list, int start = 0) + /// The amount of entities matched by the query. + public int GetEntities(in QueryDescription queryDescription, Span list, int start = 0) { var index = 0; var query = Query(in queryDescription); - foreach (ref var chunk in query) + + foreach (ref var entity in query.GetEntityEnumerator()) { - ref var entityFirstElement = ref chunk.Entity(0); - foreach (var entityIndex in chunk) - { - var entity = Unsafe.Add(ref entityFirstElement, entityIndex); - list[start + index] = entity; - index++; - } + list[start + index] = entity; + index++; } + + return index; } /// @@ -446,8 +447,10 @@ public void GetEntities(in QueryDescription queryDescription, Span list, /// The which specifies the components for which to search. /// The receiving s containing s with the matching components. /// The start index inside the . Default is 0. - public void GetArchetypes(in QueryDescription queryDescription, Span archetypes, int start = 0) + public int GetArchetypes(in QueryDescription queryDescription, Span archetypes, int start = 0) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + var index = 0; var query = Query(in queryDescription); foreach (var archetype in query.GetArchetypeIterator()) @@ -455,6 +458,8 @@ public void GetArchetypes(in QueryDescription queryDescription, Span archetypes[start + index] = archetype; index++; } + + return index; } /// @@ -463,15 +468,37 @@ public void GetArchetypes(in QueryDescription queryDescription, Span /// The which specifies which components are searched for. /// The receiving s containing s with the matching components. /// The start index inside the . Default is 0. - public void GetChunks(in QueryDescription queryDescription, Span chunks, int start = 0) + public int GetChunks(in QueryDescription queryDescription, Span chunks, int start = 0) { var index = 0; var query = Query(in queryDescription); + +#if CHANGED_FLAGS + if (query.HasChangedFilter) + { + var changedMask = query.Changed; + foreach (ref var chunk in query) + { + if (!chunk.IsAnyChanged(changedMask)) + { + continue; + } + + chunks[start + index] = chunk; + index++; + } + + return index; + } +#endif + foreach (ref var chunk in query) { chunks[start + index] = chunk; index++; } + + return index; } /// @@ -735,14 +762,9 @@ public partial class World public void Query(in QueryDescription queryDescription, ForEach forEntity) { var query = Query(in queryDescription); - foreach (ref var chunk in query) + foreach (ref var entity in query.GetEntityEnumerator()) { - ref var entityLastElement = ref chunk.Entity(0); - foreach (var entityIndex in chunk) - { - var entity = Unsafe.Add(ref entityLastElement, entityIndex); - forEntity(entity); - } + forEntity(entity); } } @@ -755,16 +777,10 @@ public void Query(in QueryDescription queryDescription, ForEach forEntity) public void InlineQuery(in QueryDescription queryDescription) where T : struct, IForEach { var t = new T(); - var query = Query(in queryDescription); - foreach (ref var chunk in query) + foreach (ref var entity in query.GetEntityEnumerator()) { - ref var entityFirstElement = ref chunk.Entity(0); - foreach (var entityIndex in chunk) - { - var entity = Unsafe.Add(ref entityFirstElement, entityIndex); - t.Update(entity); - } + t.Update(entity); } } @@ -778,14 +794,9 @@ public void InlineQuery(in QueryDescription queryDescription) where T : struc public void InlineQuery(in QueryDescription queryDescription, ref T iForEach) where T : struct, IForEach { var query = Query(in queryDescription); - foreach (ref var chunk in query) + foreach (ref var entity in query.GetEntityEnumerator()) { - ref var entityFirstElement = ref chunk.Entity(0); - foreach (var entityIndex in chunk) - { - var entity = Unsafe.Add(ref entityFirstElement, entityIndex); - iForEach.Update(entity); - } + iForEach.Update(entity); } } } @@ -808,6 +819,8 @@ public partial class World [StructuralChange] public void Destroy(in QueryDescription queryDescription) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + var query = Query(in queryDescription); foreach (var archetype in query.GetArchetypeIterator()) { @@ -847,6 +860,8 @@ public void Destroy(in QueryDescription queryDescription) /// The value of the component to set. public void Set(in QueryDescription queryDescription, in T? value = default) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + var query = Query(in queryDescription); foreach (ref var chunk in query) { @@ -876,13 +891,15 @@ public void Set(in QueryDescription queryDescription, in T? value = default) [StructuralChange] public void Add(in QueryDescription queryDescription, in T? component = default) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + // BitSet to stack/span bitset, size big enough to contain ALL registered components. Span stack = stackalloc uint[BitSet.RequiredLength(ComponentRegistry.Size)]; var query = Query(in queryDescription); foreach (var archetype in query.GetArchetypeIterator()) { - // Archetype with T shouldnt be skipped to prevent undefined behaviour. + // Archetype with T shouldn't be skipped to prevent undefined behaviour. if (archetype.EntityCount == 0 || archetype.Has()) { continue; @@ -933,6 +950,8 @@ public void Add(in QueryDescription queryDescription, in T? component = defau [StructuralChange] public void Remove(in QueryDescription queryDescription) { + AssertionUtils.AssertNoChangedFilter(queryDescription); + // BitSet to stack/span bitset, size big enough to contain ALL registered components. Span stack = stackalloc uint[BitSet.RequiredLength(ComponentRegistry.Size)];