feat: implement auto-dirty-marking for query iterators

Introduce `TrackAccess` in `World` to record component access
during batching scopes. Update `SelectN` iterators to provide
`RefN` for mutable, tracked access and `ValN` for read-only,
untracked access.
This commit is contained in:
hypercross 2026-07-21 10:40:18 +08:00
parent 5d7eb14911
commit 1e8e4e1b38
6 changed files with 110 additions and 35 deletions

View File

@ -117,7 +117,7 @@ public class CommandTests
var positions = new List<(float X, float Y, int Health)>(); var positions = new List<(float X, float Y, int Health)>();
foreach (var it in world.Select<TestPosition, TestHealth>()) foreach (var it in world.Select<TestPosition, TestHealth>())
{ {
positions.Add((it.Item1.X, it.Item1.Y, it.Item2.Value)); positions.Add((it.Val1.X, it.Val1.Y, it.Val2.Value));
} }
positions.Should().BeEquivalentTo([ positions.Should().BeEquivalentTo([
@ -152,7 +152,7 @@ public class CommandTests
var positions = new List<(float X, float Y)>(); var positions = new List<(float X, float Y)>();
foreach (var it in world.Select<TestPosition>()) foreach (var it in world.Select<TestPosition>())
{ {
positions.Add((it.Item1.X, it.Item1.Y)); positions.Add((it.Val1.X, it.Val1.Y));
} }
positions.Should().BeEquivalentTo([(10, 20)]); positions.Should().BeEquivalentTo([(10, 20)]);

View File

@ -169,9 +169,9 @@ public class EdgeCaseTests
foreach (var it in world.Select<Position, Velocity, Health, Frozen>()) foreach (var it in world.Select<Position, Velocity, Health, Frozen>())
{ {
count++; count++;
it.Item1.X.Should().Be(1); it.Val1.X.Should().Be(1);
it.Item2.Y.Should().Be(4); it.Val2.Y.Should().Be(4);
it.Item3.Value.Should().Be(100); it.Val3.Value.Should().Be(100);
} }
count.Should().Be(1); count.Should().Be(1);
} }

View File

@ -90,7 +90,7 @@ public class QueryTests
world.AddComponent(entity, new Position { X = 0, Y = 0 }); world.AddComponent(entity, new Position { X = 0, Y = 0 });
foreach (var it in world.Select<Position>()) foreach (var it in world.Select<Position>())
it.Item1.X = 42; it.Ref1.X = 42;
ref var pos = ref world.GetComponent<Position>(entity); ref var pos = ref world.GetComponent<Position>(entity);
pos.X.Should().Be(42); pos.X.Should().Be(42);
@ -129,9 +129,9 @@ public class QueryTests
foreach (var it in world.Select<Position, Velocity, Health>()) foreach (var it in world.Select<Position, Velocity, Health>())
{ {
count++; count++;
it.Item1.X.Should().Be(1); it.Val1.X.Should().Be(1);
it.Item2.Y.Should().Be(4); it.Val2.Y.Should().Be(4);
it.Item3.Value.Should().Be(100); it.Val3.Value.Should().Be(100);
} }
count.Should().Be(1); count.Should().Be(1);
@ -166,8 +166,8 @@ public class SystemTests
float dt = tick.DeltaTime; float dt = tick.DeltaTime;
foreach (var it in world.Select<Position, Velocity>()) foreach (var it in world.Select<Position, Velocity>())
{ {
it.Item1.X += it.Item2.X * dt; it.Ref1.X += it.Val2.X * dt;
it.Item1.Y += it.Item2.Y * dt; it.Ref1.Y += it.Val2.Y * dt;
} }
} }
} }

View File

@ -191,7 +191,7 @@ public class RelationshipTests
foreach (var it in world.Select<Relationship<ChildOf, ParentOf>>()) foreach (var it in world.Select<Relationship<ChildOf, ParentOf>>())
{ {
results.Add(it.Entity); results.Add(it.Entity);
it.Item1.Target.Should().Be(parent); it.Val1.Target.Should().Be(parent);
} }
results.Should().BeEquivalentTo([child]); results.Should().BeEquivalentTo([child]);

View File

@ -275,6 +275,17 @@ public class World : IDisposable
return ref _components.Get<T>(entity); return ref _components.Get<T>(entity);
} }
/// <summary>
/// Tracks a component access for auto-dirty-marking without returning
/// a ref. Used by <see cref="SelectN"/> iterators when they expose
/// component refs directly from the sparse set.
/// </summary>
internal void TrackAccess(Entity entity, Type componentType)
{
if (_batchingDepth > 0)
_accessedComponents.Add((entity, componentType));
}
/// <summary> /// <summary>
/// Returns a copy of the component of type <typeparamref name="T"/> /// Returns a copy of the component of type <typeparamref name="T"/>
/// for the given entity. Never auto-marks as modified — use this when /// for the given entity. Never auto-marks as modified — use this when

View File

@ -126,8 +126,25 @@ public ref struct Select1<T1> where T1 : struct
/// <summary>The current enumerator (required for foreach duck-typing).</summary> /// <summary>The current enumerator (required for foreach duck-typing).</summary>
public Select1<T1> Current => this; public Select1<T1> Current => this;
/// <summary>Read-write reference to the current entity's component of type <typeparamref name="T1"/>.</summary> /// <summary>
public ref T1 Item1 => ref _set!.Get(Entity); /// Mutable reference to the current entity's component of type <typeparamref name="T1"/>.
/// Access is tracked for auto-dirty-marking during a batching scope.
/// </summary>
public ref T1 Ref1
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
_world.TrackAccess(Entity, typeof(T1));
return ref _set!.Get(Entity);
}
}
/// <summary>
/// Read-only reference to the current entity's component of type <typeparamref name="T1"/>.
/// Access is NOT tracked for auto-dirty-marking.
/// </summary>
public ref readonly T1 Val1 => ref _set!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -193,8 +210,29 @@ public ref struct Select2<T1, T2> where T1 : struct where T2 : struct
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
public Select2<T1, T2> Current => this; public Select2<T1, T2> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity); public ref T1 Ref1
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
_world.TrackAccess(Entity, typeof(T1));
return ref _set1!.Get(Entity);
}
}
public ref T2 Ref2
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
_world.TrackAccess(Entity, typeof(T2));
return ref _set2!.Get(Entity);
}
}
public ref readonly T1 Val1 => ref _set1!.Get(Entity);
public ref readonly T2 Val2 => ref _set2!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -273,9 +311,14 @@ public ref struct Select3<T1, T2, T3>
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
public Select3<T1, T2, T3> Current => this; public Select3<T1, T2, T3> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity); public ref T1 Ref1 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T1)); return ref _set1!.Get(Entity); } }
public ref T3 Item3 => ref _set3!.Get(Entity); public ref T2 Ref2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T2)); return ref _set2!.Get(Entity); } }
public ref T3 Ref3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T3)); return ref _set3!.Get(Entity); } }
public ref readonly T1 Val1 => ref _set1!.Get(Entity);
public ref readonly T2 Val2 => ref _set2!.Get(Entity);
public ref readonly T3 Val3 => ref _set3!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -365,10 +408,16 @@ public ref struct Select4<T1, T2, T3, T4>
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
public Select4<T1, T2, T3, T4> Current => this; public Select4<T1, T2, T3, T4> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity); public ref T1 Ref1 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T1)); return ref _set1!.Get(Entity); } }
public ref T3 Item3 => ref _set3!.Get(Entity); public ref T2 Ref2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T2)); return ref _set2!.Get(Entity); } }
public ref T4 Item4 => ref _set4!.Get(Entity); public ref T3 Ref3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T3)); return ref _set3!.Get(Entity); } }
public ref T4 Ref4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T4)); return ref _set4!.Get(Entity); } }
public ref readonly T1 Val1 => ref _set1!.Get(Entity);
public ref readonly T2 Val2 => ref _set2!.Get(Entity);
public ref readonly T3 Val3 => ref _set3!.Get(Entity);
public ref readonly T4 Val4 => ref _set4!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -471,11 +520,18 @@ public ref struct Select5<T1, T2, T3, T4, T5>
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
public Select5<T1, T2, T3, T4, T5> Current => this; public Select5<T1, T2, T3, T4, T5> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity); public ref T1 Ref1 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T1)); return ref _set1!.Get(Entity); } }
public ref T3 Item3 => ref _set3!.Get(Entity); public ref T2 Ref2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T2)); return ref _set2!.Get(Entity); } }
public ref T4 Item4 => ref _set4!.Get(Entity); public ref T3 Ref3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T3)); return ref _set3!.Get(Entity); } }
public ref T5 Item5 => ref _set5!.Get(Entity); public ref T4 Ref4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T4)); return ref _set4!.Get(Entity); } }
public ref T5 Ref5 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T5)); return ref _set5!.Get(Entity); } }
public ref readonly T1 Val1 => ref _set1!.Get(Entity);
public ref readonly T2 Val2 => ref _set2!.Get(Entity);
public ref readonly T3 Val3 => ref _set3!.Get(Entity);
public ref readonly T4 Val4 => ref _set4!.Get(Entity);
public ref readonly T5 Val5 => ref _set5!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -593,12 +649,20 @@ public ref struct Select6<T1, T2, T3, T4, T5, T6>
public Entity Entity { get; private set; } public Entity Entity { get; private set; }
public Select6<T1, T2, T3, T4, T5, T6> Current => this; public Select6<T1, T2, T3, T4, T5, T6> Current => this;
public ref T1 Item1 => ref _set1!.Get(Entity);
public ref T2 Item2 => ref _set2!.Get(Entity); public ref T1 Ref1 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T1)); return ref _set1!.Get(Entity); } }
public ref T3 Item3 => ref _set3!.Get(Entity); public ref T2 Ref2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T2)); return ref _set2!.Get(Entity); } }
public ref T4 Item4 => ref _set4!.Get(Entity); public ref T3 Ref3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T3)); return ref _set3!.Get(Entity); } }
public ref T5 Item5 => ref _set5!.Get(Entity); public ref T4 Ref4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T4)); return ref _set4!.Get(Entity); } }
public ref T6 Item6 => ref _set6!.Get(Entity); public ref T5 Ref5 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T5)); return ref _set5!.Get(Entity); } }
public ref T6 Ref6 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { _world.TrackAccess(Entity, typeof(T6)); return ref _set6!.Get(Entity); } }
public ref readonly T1 Val1 => ref _set1!.Get(Entity);
public ref readonly T2 Val2 => ref _set2!.Get(Entity);
public ref readonly T3 Val3 => ref _set3!.Get(Entity);
public ref readonly T4 Val4 => ref _set4!.Get(Entity);
public ref readonly T5 Val5 => ref _set5!.Get(Entity);
public ref readonly T6 Val6 => ref _set6!.Get(Entity);
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() public bool MoveNext()
@ -679,4 +743,4 @@ public ref struct Select6<T1, T2, T3, T4, T5, T6>
} }
return true; return true;
} }
} }