Skip to content

Commit

Permalink
introduced StructuralChangeException thrown when executing a structur…
Browse files Browse the repository at this point in the history
…al change within a query
  • Loading branch information
friflo committed Feb 13, 2025
1 parent eb45fc5 commit 7943095
Show file tree
Hide file tree
Showing 17 changed files with 157 additions and 12 deletions.
6 changes: 6 additions & 0 deletions src/ECS/Archetype/EntityStore.Archetype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ private static Archetype GetArchetypeWithTags(EntityStoreBase store, Archetype a

internal Archetype GetArchetypeAdd(Archetype type, in ComponentTypes addComponents, in Tags addTags)
{
if (internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
var key = searchKey;
key.tags = type.tags;
key.componentTypes = type.componentTypes;
Expand All @@ -150,6 +153,9 @@ key.tags. bitSet.Equals(type.tags. bitSet)) {

internal Archetype GetArchetypeRemove(Archetype type, in ComponentTypes removeComponents, in Tags removeTags)
{
if (internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
var key = searchKey;
key.tags = type.tags;
key.componentTypes = type.componentTypes;
Expand Down
6 changes: 6 additions & 0 deletions src/ECS/Archetype/EntityStore.Mutation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ internal static bool AddTags(
ref int compIndex,
ref int archIndex)
{
if (store.internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
var arch = archetype;
var curTags = arch.tags;
var newTags = new Tags (BitSet.Add(curTags.bitSet, tags.bitSet));
Expand Down Expand Up @@ -140,6 +143,9 @@ internal static bool RemoveTags(
ref int compIndex,
ref int archIndex)
{
if (store.internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
var arch = archetype;
var curTags = arch.tags;
var newTags = new Tags (BitSet.Remove(curTags.bitSet, tags.bitSet));
Expand Down
32 changes: 32 additions & 0 deletions src/ECS/Archetype/EntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public abstract partial class EntityStoreBase
/// <summary>Contains state of <see cref="EntityStoreBase"/> not relevant for application development.</summary>
/// <remarks>Declaring internal state fields in this struct remove noise in debugger.</remarks>
internal struct InternBase {
internal int activeQueryLoops; // 4
//
internal long archetypesCapacity; // 16 - sum of all Archetype capacities
internal double shrinkRatio; // 8
// --- delegates
Expand Down Expand Up @@ -188,6 +190,20 @@ internal static ArgumentException IdOutOfRangeException(EntityStore store, int i
return new ArgumentException($"id: {id}. expect in [0, current max id: {store.nodes.Length - 1}]");
}

/// <summary>
/// Exception is thrown by add / remove component or tag operations potentially
/// calling <see cref="Archetype.MoveEntityTo"/> when within a query loop.
/// </summary>
/*
All methods calling MoveEntityTo() have a runtime exception upfront before calling MoveEntityTo() like:
if (store.internBase.activeQueryLoops > 0) {
throw EntityStoreBase.StructuralChangeWithinQueryLoop();
}
*/
internal static StructuralChangeException StructuralChangeWithinQueryLoop() {
return new StructuralChangeException("within a query loop");
}

/*
internal static ArgumentException AddRelationException(int id, int structIndex) {
var componentType = Static.EntitySchema.components[structIndex];
Expand All @@ -204,6 +220,22 @@ internal static ArgumentException RemoveRelationException(int id, int structInde
#endregion
}

/// <summary>
/// Exception is thrown when executing a <b>structural change</b> within a query loop.<br/>
/// A structural change is adding / removing components or tags.<br/>
/// See <a href="https://friflo.gitbook.io/friflo.engine.ecs/documentation/query#structuralchangeexception">Query > StructuralChangeException.</a>
/// </summary>
/// <remarks>
/// Use <see cref="CommandBuffer"/> to record changes and <see cref="CommandBuffer.Playback"/> them outside the query loop.<br/>
/// <br/>
/// To preserve old behavior use the following workaround.<br/>
/// Exception is not thrown if <see cref="ArchetypeQuery.ThrowOnStructuralChange"/> of the enclosing query is set to <c>false</c>.<br/>
/// </remarks>
public class StructuralChangeException : InvalidOperationException
{
public StructuralChangeException(string message) : base(message) { }
}

public static partial class EntityStoreExtensions
{
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/ECS/Batch/EntityBatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public sealed class EntityBatch
#region internal fields
[Browse(Never)] internal BatchComponent[] batchComponents; // 8
[Browse(Never)] private readonly ComponentType[] componentTypes; // 8
[Browse(Never)] private readonly EntityStoreBase store; // 8 - used only if owner == EntityStore
[Browse(Never)] internal readonly EntityStoreBase store; // 8 - used only if owner == EntityStore
[Browse(Never)] internal int entityId; // 4 - used only if owner == EntityStore
[Browse(Never)] internal BatchOwner owner; // 4
[Browse(Never)] internal Tags tagsAdd; // 32
Expand Down
3 changes: 3 additions & 0 deletions src/ECS/Batch/EntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ internal EntityBatch GetBatch(int entityId)

internal void ApplyBatchTo(EntityBatch batch, int entityId)
{
if (internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
ref var node = ref ((EntityStore)this).nodes[entityId];
var archetype = node.archetype;
var compIndex = node.compIndex;
Expand Down
3 changes: 3 additions & 0 deletions src/ECS/CommandBuffer/CommandBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ public void Clear()
/// </exception>
public void Playback()
{
if (intern.store.internBase.activeQueryLoops > 0) {
throw EntityStoreBase.StructuralChangeWithinQueryLoop();
}
if (!intern.hasCommands) {
// early out if command buffer is still empty
if (!intern.reuseBuffer) {
Expand Down
6 changes: 6 additions & 0 deletions src/ECS/Entity/Mutation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public readonly partial struct Entity
/// <returns>true - component is newly added to the entity.<br/> false - component is updated.</returns>
public bool AddComponent<T>(in T component) where T : struct, IComponent
{
if (store.internBase.activeQueryLoops > 0) {
throw EntityStoreBase.StructuralChangeWithinQueryLoop();
}
int id = Id;
var localStore = store;
ref var node = ref localStore.nodes[id];
Expand Down Expand Up @@ -70,6 +73,9 @@ public bool AddComponent<T>(in T component) where T : struct, IComponent
/// </remarks>
public bool RemoveComponent<T>() where T : struct, IComponent
{
if (store.internBase.activeQueryLoops > 0) {
throw EntityStoreBase.StructuralChangeWithinQueryLoop();
}
var id = Id;
int structIndex = StructInfo<T>.Index;
var localStore = store;
Expand Down
3 changes: 3 additions & 0 deletions src/ECS/Entity/Store/DataEntities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ internal Entity DataEntityToEntity(DataEntity dataEntity, out string error, Comp
if (dataEntity == null) {
throw new ArgumentNullException(nameof(dataEntity));
}
if (internBase.activeQueryLoops > 0) {
throw StructuralChangeWithinQueryLoop();
}
Entity entity;
if (intern.pidType == PidType.UsePidAsId) {
entity = CreateFromDataEntityUsePidAsId(dataEntity);
Expand Down
18 changes: 17 additions & 1 deletion src/ECS/Query/ArchetypeQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ public class ArchetypeQuery
[Browse(Never)]
public ref readonly ComponentTypes ComponentTypes => ref components;

/// <summary>
/// If true (default) a <see cref="StructuralChangeException"/> is thrown when executing a <b>structural change</b> within a query loop.<br/>
/// See <a href="https://friflo.gitbook.io/friflo.engine.ecs/documentation/query#structuralchangeexception">Query > StructuralChangeException.</a>
/// </summary>
/// <remarks>
/// A structural change is adding / removing components or tags.<br/>
/// Use <see cref="CommandBuffer"/> to record changes and <see cref="CommandBuffer.Playback"/> them outside the query loop.
/// </remarks>
[Browse(Never)]
public bool ThrowOnStructuralChange { get => checkChange; set => checkChange = value; }


public override string ToString() => GetString();
#endregion
Expand All @@ -72,7 +83,8 @@ public class ArchetypeQuery

#region private / internal fields
// --- non blittable types
[Browse(Never)] private readonly EntityStoreBase store; // 8
[Browse(Never)] internal bool checkChange; // 1
[Browse(Never)] internal readonly EntityStoreBase store; // 8
[Browse(Never)] private Archetype[] archetypes; // 8 current list of matching archetypes, can grow
[Browse(Never)] private int[] chunkPositions; // 8 indexes of chunk entities matching a value condition
[Browse(Never)] private EventFilter eventFilter; // 8 used to filter component/tag add/remove events
Expand Down Expand Up @@ -208,6 +220,7 @@ internal ArchetypeQuery(EntityStoreBase store, in SignatureIndexes indexes, Quer
signatureIndexes= indexes;
Filter = filter ?? new QueryFilter();
relationQuery = relationType;
checkChange = true;
}

/// <summary>
Expand All @@ -221,6 +234,7 @@ internal ArchetypeQuery(EntityStoreBase store, in ComponentTypes componentTypes,
chunkPositions = Array.Empty<int>();
components = componentTypes;
Filter = filter ?? new QueryFilter();
checkChange = true;
}

/// <summary> Called by <see cref="EntityStore.GetEntities"/> </summary>
Expand All @@ -230,6 +244,7 @@ internal ArchetypeQuery(EntityStoreBase store)
archetypes = Array.Empty<Archetype>();
chunkPositions = Array.Empty<int>();
Filter = new QueryFilter(default).FreezeFilter();
checkChange = true;
}

/// <summary> Called by <see cref="Archetype.GetEntities"/> </summary>
Expand All @@ -240,6 +255,7 @@ internal ArchetypeQuery(Archetype archetype)
archetypes = new [] { archetype };
components = archetype.componentTypes;
Filter = new QueryFilter(archetype.tags).FreezeFilter();
checkChange = true;
}

private ReadOnlySpan<Archetype> GetArchetypesSpan() {
Expand Down
11 changes: 10 additions & 1 deletion src/ECS/Query/Arg.1/Query.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ public struct ChunkEnumerator<T1> : IEnumerator<Chunks<T1>>
{
private readonly int structIndex1; // 4
//
private readonly EntityStoreBase store; // 8
private readonly Archetypes archetypes; // 16
//
private int archetypePos; // 4
Expand All @@ -95,6 +96,10 @@ internal ChunkEnumerator(ArchetypeQuery<T1> query)
structIndex1 = query.signatureIndexes.T1;
archetypes = query.GetArchetypes();
archetypePos = -1;
if (query.checkChange) {
store = query.store;
store.internBase.activeQueryLoops++;
}
}

/// <summary>return Current by reference to avoid struct copy and enable mutation in library</summary>
Expand Down Expand Up @@ -151,5 +156,9 @@ public bool MoveNext()
}

// --- IDisposable
public void Dispose() { }
public void Dispose() {
if (store != null) {
store.internBase.activeQueryLoops--;
}
}
}
11 changes: 10 additions & 1 deletion src/ECS/Query/Arg.2/Query.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public struct ChunkEnumerator<T1, T2> : IEnumerator<Chunks<T1,T2>>
private readonly int structIndex1; // 4
private readonly int structIndex2; // 4
//
private readonly EntityStoreBase store; // 8
private readonly Archetypes archetypes; // 16
//
private int archetypePos; // 4
Expand All @@ -104,6 +105,10 @@ internal ChunkEnumerator(ArchetypeQuery<T1, T2> query)
structIndex2 = query.signatureIndexes.T2;
archetypes = query.GetArchetypes();
archetypePos = -1;
if (query.checkChange) {
store = query.store;
store.internBase.activeQueryLoops++;
}
}

/// <summary>return Current by reference to avoid struct copy and enable mutation in library</summary>
Expand Down Expand Up @@ -162,5 +167,9 @@ public bool MoveNext()
}

// --- IDisposable
public void Dispose() { }
public void Dispose() {
if (store != null) {
store.internBase.activeQueryLoops--;
}
}
}
11 changes: 10 additions & 1 deletion src/ECS/Query/Arg.3/Query.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public struct ChunkEnumerator<T1, T2, T3> : IEnumerator<Chunks<T1, T2, T3>>
private readonly int structIndex2; // 4
private readonly int structIndex3; // 4
//
private readonly EntityStoreBase store; // 8
private readonly Archetypes archetypes; // 16
//
private int archetypePos; // 4
Expand All @@ -113,6 +114,10 @@ internal ChunkEnumerator(ArchetypeQuery<T1, T2, T3> query)
structIndex3 = query.signatureIndexes.T3;
archetypes = query.GetArchetypes();
archetypePos = -1;
if (query.checkChange) {
store = query.store;
store.internBase.activeQueryLoops++;
}
}

/// <summary>return Current by reference to avoid struct copy and enable mutation in library</summary>
Expand Down Expand Up @@ -173,5 +178,9 @@ public bool MoveNext()
}

// --- IDisposable
public void Dispose() { }
public void Dispose() {
if (store != null) {
store.internBase.activeQueryLoops--;
}
}
}
11 changes: 10 additions & 1 deletion src/ECS/Query/Arg.4/Query.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public struct ChunkEnumerator<T1, T2, T3, T4> : IEnumerator<Chunks<T1, T2, T3, T
private readonly int structIndex3; // 4
private readonly int structIndex4; // 4
//
private readonly EntityStoreBase store; // 8
private readonly Archetypes archetypes; // 16
//
private int archetypePos; // 4
Expand All @@ -122,6 +123,10 @@ internal ChunkEnumerator(ArchetypeQuery<T1, T2, T3, T4> query)
structIndex4 = query.signatureIndexes.T4;
archetypes = query.GetArchetypes();
archetypePos = -1;
if (query.checkChange) {
store = query.store;
store.internBase.activeQueryLoops++;
}
}

/// <summary>return Current by reference to avoid struct copy and enable mutation in library</summary>
Expand Down Expand Up @@ -184,5 +189,9 @@ public bool MoveNext()
}

// --- IDisposable
public void Dispose() { }
public void Dispose() {
if (store != null) {
store.internBase.activeQueryLoops--;
}
}
}
11 changes: 10 additions & 1 deletion src/ECS/Query/Arg.5/Query.Chunks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public struct ChunkEnumerator<T1, T2, T3, T4, T5> : IEnumerator<Chunks<T1, T2, T
private readonly int structIndex4; // 4
private readonly int structIndex5; // 4
//
private readonly EntityStoreBase store; // 8
private readonly Archetypes archetypes; // 16
//
private int archetypePos; // 4
Expand All @@ -130,6 +131,10 @@ internal ChunkEnumerator(ArchetypeQuery<T1, T2, T3, T4, T5> query)
structIndex5 = query.signatureIndexes.T5;
archetypes = query.GetArchetypes();
archetypePos = -1;
if (query.checkChange) {
store = query.store;
store.internBase.activeQueryLoops++;
}
}

/// <summary>return Current by reference to avoid struct copy and enable mutation in library</summary>
Expand Down Expand Up @@ -194,5 +199,9 @@ public bool MoveNext()
}

// --- IDisposable
public void Dispose() { }
public void Dispose() {
if (store != null) {
store.internBase.activeQueryLoops--;
}
}
}
Loading

0 comments on commit 7943095

Please sign in to comment.