diff --git a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs index 4b605c7..6bf9b33 100644 --- a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs +++ b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs @@ -1,8 +1,6 @@ using Hexecs.Actors.Pipelines; -using Hexecs.Benchmarks.Map.Common.Positions; using Hexecs.Benchmarks.Map.Terrains.Assets; using Hexecs.Benchmarks.Map.Terrains.ValueTypes; -using Hexecs.Benchmarks.Map.Utils; using Hexecs.Pipelines; namespace Hexecs.Benchmarks.Map.Terrains.Commands.Generate; diff --git a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs index 4670121..f560637 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCheckComponentExistsBenchmark.cs @@ -1,5 +1,4 @@ -using System.Runtime.CompilerServices; -using Friflo.Engine.ECS; +using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; using World = Hexecs.Worlds.World; @@ -8,47 +7,49 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // // | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | // |----------------- |------- |----------:|------:|----------:|------------:| -// | Hexecs_Is | 10000 | 20.42 us | 0.96 | - | NA | -// | Hexecs_Has | 10000 | 21.19 us | 1.00 | - | NA | -// | Hexecs_Reference | 10000 | 24.30 us | 1.15 | - | NA | -// | FriFlo_Has | 10000 | 40.28 us | 1.90 | - | NA | -// | DefaultEcs_Has | 10000 | 73.24 us | 3.46 | - | NA | +// | Hexecs_Is | 10000 | 14.55 us | 0.88 | - | NA | +// | Hexecs_Reference | 10000 | 15.92 us | 0.96 | - | NA | +// | Hexecs_Has | 10000 | 16.58 us | 1.00 | - | NA | +// | FriFlo_Has | 10000 | 40.36 us | 2.43 | - | NA | +// | DefaultEcs_Has | 10000 | 72.01 us | 4.34 | - | NA | // | | | | | | | -// | Hexecs_Is | 100000 | 204.98 us | 0.94 | - | NA | -// | Hexecs_Has | 100000 | 219.12 us | 1.00 | - | NA | -// | Hexecs_Reference | 100000 | 251.83 us | 1.15 | - | NA | -// | FriFlo_Has | 100000 | 409.48 us | 1.87 | - | NA | -// | DefaultEcs_Has | 100000 | 712.00 us | 3.25 | - | NA | +// | Hexecs_Is | 100000 | 149.57 us | 0.93 | - | NA | +// | Hexecs_Has | 100000 | 161.46 us | 1.00 | - | NA | +// | Hexecs_Reference | 100000 | 163.02 us | 1.01 | - | NA | +// | FriFlo_Has | 100000 | 409.32 us | 2.54 | - | NA | +// | DefaultEcs_Has | 100000 | 730.64 us | 4.53 | - | NA | // // ------------------------------------------------------------------------------------ // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 +// .NET SDK 10.0.102 // [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Mean | Ratio | Allocated | Alloc Ratio | -// |----------------- |----------:|------:|----------:|------------:| -// | Hexecs_Is | 12.76 us | 0.93 | - | NA | -// | Hexecs_Has | 13.79 us | 1.00 | - | NA | -// | Hexecs_Reference | 15.44 us | 1.12 | - | NA | -// | DefaultEcs_Has | 25.32 us | 1.84 | - | NA | -// | | | | | | -// | Hexecs_Is | 127.64 us | 0.92 | - | NA | -// | Hexecs_Has | 139.17 us | 1.00 | - | NA | -// | Hexecs_Reference | 155.12 us | 1.11 | - | NA | -// | DefaultEcs_Has | 255.36 us | 1.83 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------- |------- |-----------:|------:|----------:|------------:| +// | Hexecs_Is | 10000 | 9.916 us | 0.90 | - | NA | +// | Hexecs_Has | 10000 | 10.960 us | 1.00 | - | NA | +// | Hexecs_Reference | 10000 | 11.301 us | 1.03 | - | NA | +// | FriFlo_Has | 10000 | 16.540 us | 1.51 | - | NA | +// | DefaultEcs_Has | 10000 | 25.877 us | 2.36 | - | NA | +// | | | | | | | +// | Hexecs_Is | 100000 | 98.966 us | 0.90 | - | NA | +// | Hexecs_Has | 100000 | 110.039 us | 1.00 | - | NA | +// | Hexecs_Reference | 100000 | 112.981 us | 1.03 | - | NA | +// | FriFlo_Has | 100000 | 159.346 us | 1.45 | - | NA | +// | DefaultEcs_Has | 100000 | 256.135 us | 2.33 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs index 38f1345..76ee82f 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorCreateAddComponentsDestroyBenchmark.cs @@ -8,46 +8,49 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | -// |---------------------------- |------- |-------------:|------:|-------:|-----------:|------------:| -// | FriFlo_CreateAddDestroy | 1000 | 153.5 us | 0.29 | - | - | NA | -// | DefaultEcs_CreateAddDestroy | 1000 | 401.1 us | 0.77 | 1.4648 | 32000 B | NA | -// | Hexecs_CreateAddDestroy | 1000 | 523.6 us | 1.00 | - | - | NA | -// | | | | | | | | -// | FriFlo_CreateAddDestroy | 100000 | 16,519.0 us | 0.25 | - | 40 B | 1.00 | -// | Hexecs_CreateAddDestroy | 100000 | 65,924.0 us | 1.00 | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 100000 | 105,603.9 us | 1.60 | - | 3200040 B | 80,001.00 | -// | | | | | | | | -// | FriFlo_CreateAddDestroy | 500000 | 85,496.9 us | 0.18 | - | 40 B | 1.00 | -// | Hexecs_CreateAddDestroy | 500000 | 474,476.8 us | 1.00 | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 500000 | 539,368.3 us | 1.14 | - | 16000040 B | 400,001.00 | +// | Method | Count | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +// |---------------------------- |------- |-------------:|------:|---------:|-----------:|------------:| +// | FriFlo_CreateAddDestroy | 1000 | 157.5 us | 0.32 | - | - | NA | +// | DefaultEcs_CreateAddDestroy | 1000 | 400.8 us | 0.83 | 1.4648 | 32000 B | NA | +// | Hexecs_CreateAddDestroy | 1000 | 485.5 us | 1.00 | - | - | NA | +// | | | | | | | | +// | FriFlo_CreateAddDestroy | 100000 | 16,503.2 us | 0.26 | - | 40 B | 1.00 | +// | Hexecs_CreateAddDestroy | 100000 | 62,708.1 us | 1.00 | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 100000 | 89,760.2 us | 1.43 | 166.6667 | 3200040 B | 80,001.00 | +// | | | | | | | | +// | FriFlo_CreateAddDestroy | 500000 | 84,276.5 us | 0.15 | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 500000 | 457,366.0 us | 0.83 | - | 16000040 B | 400,001.00 | +// | Hexecs_CreateAddDestroy | 500000 | 551,567.4 us | 1.00 | - | 40 B | 1.00 | // -// ------------------------------------------------------------------------------------ +// --------------------------------------------------------------------------------------------------------- // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 +// .NET SDK 10.0.102 // [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | -// |---------------------------- |------- |-------------:|------:|----------:|---------:|-----------:|------------:| -// | Hexecs_CreateAddDestroy | 1000 | 176.0 us | 1.00 | - | - | - | NA | -// | DefaultEcs_CreateAddDestroy | 1000 | 207.3 us | 1.18 | 3.6621 | - | 32000 B | NA | -// | | | | | | | | | -// | Hexecs_CreateAddDestroy | 100000 | 19,069.0 us | 1.00 | - | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 100000 | 22,847.4 us | 1.20 | 375.0000 | 156.2500 | 3200040 B | 80,001.00 | -// | | | | | | | | | -// | Hexecs_CreateAddDestroy | 500000 | 113,318.9 us | 1.00 | - | - | 40 B | 1.00 | -// | DefaultEcs_CreateAddDestroy | 500000 | 121,507.3 us | 1.07 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | +// | Method | Count | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +// |---------------------------- |------- |--------------:|------:|----------:|---------:|-----------:|------------:| +// | FriFlo_CreateAddDestroy | 1000 | 70.04 us | 0.45 | - | - | - | NA | +// | Hexecs_CreateAddDestroy | 1000 | 156.05 us | 1.00 | - | - | - | NA | +// | DefaultEcs_CreateAddDestroy | 1000 | 206.26 us | 1.32 | 3.6621 | - | 32000 B | NA | +// | | | | | | | | | +// | FriFlo_CreateAddDestroy | 100000 | 7,036.37 us | 0.41 | - | - | 40 B | 1.00 | +// | Hexecs_CreateAddDestroy | 100000 | 16,996.78 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 100000 | 22,336.53 us | 1.31 | 375.0000 | 156.2500 | 3200040 B | 80,001.00 | +// | | | | | | | | | +// | FriFlo_CreateAddDestroy | 500000 | 33,983.08 us | 0.36 | - | - | 40 B | 1.00 | +// | Hexecs_CreateAddDestroy | 500000 | 93,505.58 us | 1.00 | - | - | 40 B | 1.00 | +// | DefaultEcs_CreateAddDestroy | 500000 | 122,641.64 us | 1.31 | 1800.0000 | 800.0000 | 16000040 B | 400,001.00 | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs index faaec4a..5243d5e 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter2EnumerationBenchmark.cs @@ -1,38 +1,54 @@ -using Hexecs.Benchmarks.Mocks.ActorComponents; +using Friflo.Engine.ECS; +using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 - +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Mean | Ratio | Allocated | Alloc Ratio | -// |----------- |---------:|------:|----------:|------------:| -// | DefaultEcs | 165.8 us | 0.70 | - | NA | -// | Hexecs | 236.0 us | 1.00 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------------- |------- |----------:|------:|----------:|------------:| +// | FriFlo_Chunks | 10000 | 11.12 us | 0.46 | - | NA | +// | DefaultEcs | 10000 | 15.74 us | 0.65 | - | NA | +// | Hexecs_ComponentAccess | 10000 | 17.25 us | 0.71 | - | NA | +// | FriFlo | 10000 | 23.40 us | 0.96 | 88 B | NA | +// | Hexecs | 10000 | 24.34 us | 1.00 | - | NA | +// | | | | | | | +// | FriFlo_Chunks | 100000 | 109.50 us | 0.47 | - | NA | +// | DefaultEcs | 100000 | 159.41 us | 0.69 | - | NA | +// | Hexecs_ComponentAccess | 100000 | 174.74 us | 0.76 | - | NA | +// | Hexecs | 100000 | 230.64 us | 1.00 | - | NA | +// | FriFlo | 100000 | 232.40 us | 1.01 | 88 B | NA | // // ------------------------------------------------------------------------------------ // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 +// .NET SDK 10.0.102 // [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | -// |----------- |------- |-----------:|------:|----------:|------------:| -// | DefaultEcs | 10000 | 9.140 us | 0.88 | - | NA | -// | Hexecs | 10000 | 10.444 us | 1.00 | - | NA | -// | | | | | | | -// | DefaultEcs | 100000 | 89.176 us | 0.88 | - | NA | -// | Hexecs | 100000 | 101.793 us | 1.00 | - | NA | +// | Method | Count | Mean | Ratio | Gen0 | Allocated | Alloc Ratio | +// |----------------------- |------- |----------:|------:|-------:|----------:|------------:| +// | FriFlo | 10000 | 6.614 us | 0.66 | 0.0076 | 88 B | NA | +// | FriFlo_Chunks | 10000 | 6.716 us | 0.67 | - | - | NA | +// | Hexecs_ComponentAccess | 10000 | 8.878 us | 0.88 | - | - | NA | +// | DefaultEcs | 10000 | 9.770 us | 0.97 | - | - | NA | +// | Hexecs | 10000 | 10.078 us | 1.00 | - | - | NA | +// | | | | | | | | +// | FriFlo | 100000 | 65.792 us | 0.70 | - | 88 B | NA | +// | FriFlo_Chunks | 100000 | 67.258 us | 0.72 | - | - | NA | +// | Hexecs_ComponentAccess | 100000 | 84.009 us | 0.90 | - | - | NA | +// | DefaultEcs | 100000 | 92.408 us | 0.98 | - | - | NA | +// | Hexecs | 100000 | 93.853 us | 1.00 | - | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -45,12 +61,16 @@ public class ActorFilter2EnumerationBenchmark { [Params(10_000, 100_000)] public int Count; + private ActorContext _context = null!; private ActorFilter _filter = null!; private World _world = null!; private DefaultEcs.World _defaultWorld = null!; private DefaultEcs.EntitySet _defaultEntitySet = null!; + private EntityStore _frifloWorld = null!; + private ArchetypeQuery _frifloQuery = null!; + [Benchmark(Baseline = true)] public int Hexecs() { @@ -64,6 +84,23 @@ public int Hexecs() return result; } + [Benchmark] + public int Hexecs_ComponentAccess() + { + var result = 0; + + var attacks = _context.GetComponents(); + var defences = _context.GetComponents(); + + foreach (var actorId in _filter.Keys) + { + result += attacks[actorId].Value + + defences[actorId].Value; + } + + return result; + } + [Benchmark] public int DefaultEcs() { @@ -80,6 +117,40 @@ public int DefaultEcs() return result; } + [Benchmark] + public int FriFlo() + { + var result = 0; + + _frifloQuery.ForEachEntity((ref attack, ref defence, _) => + { + result += attack.Value + + defence.Value; + }); + + return result; + } + + [Benchmark] + public int FriFlo_Chunks() + { + var result = 0; + + foreach (var queryChunk in _frifloQuery.Chunks) + { + var attacks = queryChunk.Chunk1; + var defences = queryChunk.Chunk2; + + for (var i = 0; i < queryChunk.Length; i++) + { + result += attacks[i].Value + + defences[i].Value; + } + } + + return result; + } + [GlobalCleanup] public void Cleanup() { @@ -94,21 +165,28 @@ public void Cleanup() public void Setup() { _defaultWorld = new DefaultEcs.World(); + _frifloWorld = new EntityStore(); _world = new WorldBuilder().Build(); + _context = _world.Actors; _defaultEntitySet = _defaultWorld.GetEntities().With().With().AsSet(); _filter = _world.Actors.Filter(); + _frifloQuery = _frifloWorld.Query(); var context = _world.Actors; for (var i = 0; i < Count; i++) { + var attack = new Attack { Value = i }; + var actor = context.CreateActor(); - actor.Add(new Attack()); + actor.Add(in attack); actor.Add(new Defence()); var defaultEntity = _defaultWorld.CreateEntity(); - defaultEntity.Set(); + defaultEntity.Set(in attack); defaultEntity.Set(); + + _frifloWorld.CreateEntity(attack, new Defence(), new Speed()); } } } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs index a8903e4..0d66d63 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorFilter3EnumerationBenchmark.cs @@ -7,41 +7,49 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | -// |-------------- |------- |----------:|------:|----------:|------------:| -// | FriFlo_Chunks | 10000 | 16.28 us | 0.63 | - | NA | -// | Hexecs | 10000 | 25.99 us | 1.00 | - | NA | -// | FriFlo | 10000 | 26.76 us | 1.03 | 88 B | NA | -// | DefaultEcs | 10000 | 29.78 us | 1.15 | - | NA | -// | | | | | | | -// | FriFlo_Chunks | 100000 | 156.83 us | 0.58 | - | NA | -// | FriFlo | 100000 | 263.76 us | 0.98 | 88 B | NA | -// | Hexecs | 100000 | 268.25 us | 1.00 | - | NA | -// | DefaultEcs | 100000 | 289.78 us | 1.08 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------------- |------- |----------:|------:|----------:|------------:| +// | FriFlo_Chunks | 10000 | 16.10 us | 0.55 | - | NA | +// | Hexecs_ComponentAccess | 10000 | 25.10 us | 0.86 | - | NA | +// | FriFlo | 10000 | 25.66 us | 0.87 | 88 B | NA | +// | Hexecs | 10000 | 29.33 us | 1.00 | - | NA | +// | DefaultEcs | 10000 | 29.34 us | 1.00 | - | NA | +// | | | | | | | +// | FriFlo_Chunks | 100000 | 158.62 us | 0.50 | - | NA | +// | FriFlo | 100000 | 253.14 us | 0.79 | 88 B | NA | +// | Hexecs_ComponentAccess | 100000 | 285.76 us | 0.89 | - | NA | +// | DefaultEcs | 100000 | 287.17 us | 0.90 | - | NA | +// | Hexecs | 100000 | 320.26 us | 1.00 | - | NA | // // ------------------------------------------------------------------------------------ // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 +// .NET SDK 10.0.102 // [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a // // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | -// |----------- |------- |----------:|------:|----------:|------------:| -// | DefaultEcs | 10000 | 13.52 us | 0.95 | - | NA | -// | Hexecs | 10000 | 14.28 us | 1.00 | - | NA | -// | | | | | | | -// | DefaultEcs | 100000 | 125.76 us | 0.90 | - | NA | -// | Hexecs | 100000 | 139.85 us | 1.00 | - | NA | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------------- |------- |-----------:|------:|----------:|------------:| +// | FriFlo | 10000 | 9.383 us | 0.69 | 88 B | NA | +// | FriFlo_Chunks | 10000 | 9.538 us | 0.70 | - | NA | +// | Hexecs_ComponentAccess | 10000 | 12.621 us | 0.93 | - | NA | +// | DefaultEcs | 10000 | 13.351 us | 0.99 | - | NA | +// | Hexecs | 10000 | 13.534 us | 1.00 | - | NA | +// | | | | | | | +// | FriFlo | 100000 | 91.925 us | 0.65 | 88 B | NA | +// | FriFlo_Chunks | 100000 | 94.962 us | 0.67 | - | NA | +// | Hexecs_ComponentAccess | 100000 | 131.812 us | 0.93 | - | NA | +// | Hexecs | 100000 | 142.264 us | 1.00 | - | NA | +// | DefaultEcs | 100000 | 158.585 us | 1.11 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] @@ -54,6 +62,7 @@ public class ActorFilter3EnumerationBenchmark { [Params(10_000, 100_000)] public int Count; + private ActorContext _context = null!; private ActorFilter _filter = null!; private World _world = null!; @@ -77,6 +86,25 @@ public int Hexecs() return result; } + [Benchmark] + public int Hexecs_ComponentAccess() + { + var result = 0; + + var attacks = _context.GetComponents(); + var defences = _context.GetComponents(); + var speeds = _context.GetComponents(); + + foreach (var actorId in _filter.Keys) + { + result += attacks[actorId].Value + + defences[actorId].Value + + speeds[actorId].Value; + } + + return result; + } + [Benchmark] public int DefaultEcs() { @@ -148,6 +176,7 @@ public void Setup() _defaultWorld = new DefaultEcs.World(); _frifloWorld = new EntityStore(); _world = new WorldBuilder().Build(); + _context = _world.Actors; _defaultEntitySet = _defaultWorld.GetEntities().With().With().With().AsSet(); _filter = _world.Actors.Filter(); diff --git a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs index 98e5b5f..9494530 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorHierarchyBenchmark.cs @@ -1,30 +1,71 @@ using Hexecs.Worlds; -using System.Buffers; +using Friflo.Engine.ECS; namespace Hexecs.Benchmarks.Actors; +// BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) +// Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------- |------ |-------------:|------:|----------:|------------:| +// | Hexecs_Hierarchy | 100 | 333.4 us | 1.00 | - | NA | +// | Friflo_Hierarchy | 100 | 709.2 us | 2.13 | - | NA | +// | | | | | | | +// | Hexecs_Hierarchy | 1000 | 34,104.9 us | 1.00 | - | NA | +// | Friflo_Hierarchy | 1000 | 129,978.8 us | 3.81 | - | NA | +// +// ------------------------------------------------------------------------------------ +// +// BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a +// +// Job=.NET 10.0 Runtime=.NET 10.0 +// +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |----------------- |------ |------------:|------:|----------:|------------:| +// | Hexecs_Hierarchy | 100 | 156.3 us | 1.00 | - | NA | +// | Friflo_Hierarchy | 100 | 315.1 us | 2.02 | - | NA | +// | | | | | | | +// | Hexecs_Hierarchy | 1000 | 13,998.8 us | 1.00 | - | NA | +// | Friflo_Hierarchy | 1000 | 89,419.8 us | 6.39 | - | NA | + [SimpleJob(RuntimeMoniker.Net10_0)] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] [MeanColumn, MemoryDiagnoser] [HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")] +[JsonExporterAttribute.Full] +[JsonExporterAttribute.FullCompressed] [BenchmarkCategory("Actors")] public class ActorHierarchyBenchmark { - [Params(100, 1_000, 2_000)] public int Count; + [Params(100, 1_000)] public int Count; private ActorContext _actorContext = null!; + private Actor[] _buffer = null!; private Actor[] _parents = null!; private Actor[] _children = null!; private World _world = null!; - [Benchmark] - public int Do() + // Friflo поля + private Entity[] _friBuffer = null!; + private Entity[] _friParents = null!; + private Entity[] _friChildren = null!; + private EntityStore _friStore = null!; + + [Benchmark(Baseline = true)] + public int Hexecs_Hierarchy() { - // Часть 1: Построение иерархии - // Для каждого родителя добавляем Count детей (аналогично бенчмарку отношений) var childIdx = 0; - for (var i = 0; i < _parents.Length; i++) + foreach (var parent in _parents) { - var parent = _parents[i]; for (var j = 0; j < Count; j++) { parent.AddChild(_children[childIdx++]); @@ -32,49 +73,90 @@ public int Do() } var result = 0; - var buffer = ArrayPool.Shared.Rent(Count); + var buffer = _buffer; - // Часть 2: Итерация и удаление - for (var i = 0; i < _parents.Length; i++) + foreach (var parent in _parents) { - var parent = _parents[i]; var children = parent.Children(); var k = 0; - // Сбор детей в буфер foreach (var child in children) { - buffer[k++] = child.Id; + buffer[k++] = child; result++; } - // Удаление детей for (var j = 0; j < k; j++) { - parent.RemoveChild(new Actor(_actorContext, buffer[j])); + parent.RemoveChild(buffer[j]); + } + } + + return result; + } + + [Benchmark] + public int Friflo_Hierarchy() + { + var childIdx = 0; + foreach (var parent in _friParents) + { + for (var j = 0; j < Count; j++) + { + parent.AddChild(_friChildren[childIdx++]); + } + } + + var result = 0; + var buffer = _friBuffer; + + for (var i = 0; i < _friParents.Length; i++) + { + var parent = _friParents[i]; + var children = parent.ChildEntities; + var k = 0; + + foreach (var child in children) + { + buffer[k++] = child; + result++; + } + + for (var j = 0; j < k; j++) + { + parent.RemoveChild(buffer[j]); } } - ArrayPool.Shared.Return(buffer); return result; } [GlobalSetup] public void Setup() { + // Setup Hexecs _world = new WorldBuilder().Build(); + _buffer = new Actor[Count]; _actorContext = _world.Actors; - _parents = new Actor[Count]; _children = new Actor[Count * Count]; + // Setup Friflo + _friBuffer = new Entity[Count]; + _friParents = new Entity[Count]; + _friChildren = new Entity[Count * Count]; + _friStore = new EntityStore(); + for (var i = 0; i < Count; i++) { _parents[i] = _actorContext.CreateActor(); + _friParents[i] = _friStore.CreateEntity(); + for (var j = 0; j < Count; j++) { var index = (i * Count) + j; _children[index] = _actorContext.CreateActor(); + _friChildren[index] = _friStore.CreateEntity(); } } } diff --git a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs index 933763b..cc71f40 100644 --- a/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/ActorRelationBenchmark.cs @@ -1,4 +1,3 @@ -using System.Buffers; using Friflo.Engine.ECS; using Hexecs.Benchmarks.Mocks.ActorComponents; using Hexecs.Worlds; @@ -7,36 +6,43 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // // | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | // |------- |------ |--------------:|------:|----------:|------------:| -// | Hexecs | 10 | 13.32 us | 1.00 | - | NA | -// | FriFlo | 10 | 15.66 us | 1.18 | - | NA | +// | Hexecs | 10 | 12.17 us | 1.00 | - | NA | +// | FriFlo | 10 | 14.94 us | 1.23 | - | NA | // | | | | | | | -// | Hexecs | 100 | 1,929.54 us | 1.00 | - | NA | -// | FriFlo | 100 | 2,301.57 us | 1.19 | - | NA | +// | Hexecs | 100 | 2,125.12 us | 1.00 | - | NA | +// | FriFlo | 100 | 2,167.52 us | 1.02 | - | NA | // | | | | | | | -// | Hexecs | 1000 | 604,757.61 us | 1.00 | - | NA | -// | FriFlo | 1000 | 807,112.05 us | 1.33 | - | NA | +// | Hexecs | 1000 | 596,797.48 us | 1.00 | - | NA | +// | FriFlo | 1000 | 827,027.70 us | 1.39 | - | NA | +// +// ------------------------------------------------------------------------------------ // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] -// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 -// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// +// Apple M3 Max, 1 CPU, 16 logical and 16 physical cores +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a +// // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | Count | Mean | Allocated | -// |------- |------ |---------------:|----------:| -// | Do | 100 | 798.9 us | - | -// | Do | 1000 | 261,046.6 us | - | -// | Do | 2000 | 2,049,070.5 us | - | +// | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | +// |------- |------ |---------------:|------:|----------:|------------:| +// | Hexecs | 10 | 5.104 us | 1.00 | - | NA | +// | FriFlo | 10 | 5.735 us | 1.12 | - | NA | +// | | | | | | | +// | Hexecs | 100 | 820.843 us | 1.00 | - | NA | +// | FriFlo | 100 | 979.904 us | 1.19 | - | NA | +// | | | | | | | +// | Hexecs | 1000 | 244,640.907 us | 1.00 | - | NA | +// | FriFlo | 1000 | 440,642.817 us | 1.80 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs index 073fa81..b426bc0 100644 --- a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs +++ b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs @@ -9,39 +9,41 @@ namespace Hexecs.Benchmarks.Actors; // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 // // Job=.NET 10.0 Runtime=.NET 10.0 // // | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | // |-------------------- |-------- |------------:|------:|----------:|------------:| -// | FriFlo_Parallel | 100000 | 94.42 us | 0.77 | - | NA | -// | Hexecs_Parallel | 100000 | 122.17 us | 1.00 | - | NA | -// | DefaultEcs_Parallel | 100000 | 221.83 us | 1.82 | - | NA | +// | FriFlo_Parallel | 100000 | 92.29 us | 0.45 | - | NA | +// | DefaultEcs_Parallel | 100000 | 199.27 us | 0.96 | - | NA | +// | Hexecs_Parallel | 100000 | 206.64 us | 1.00 | - | NA | // | | | | | | | -// | FriFlo_Parallel | 1000000 | 842.69 us | 0.91 | - | NA | -// | Hexecs_Parallel | 1000000 | 931.08 us | 1.00 | - | NA | -// | DefaultEcs_Parallel | 1000000 | 2,370.75 us | 2.55 | - | NA | +// | FriFlo_Parallel | 1000000 | 765.89 us | 0.31 | - | NA | +// | DefaultEcs_Parallel | 1000000 | 2,078.13 us | 0.83 | - | NA | +// | Hexecs_Parallel | 1000000 | 2,497.94 us | 1.00 | - | NA | // // ------------------------------------------------------------------------------------ // // BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] // Apple M3 Max, 1 CPU, 16 logical and 16 physical cores -// .NET SDK 10.0.101 +// .NET SDK 10.0.102 // [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a -// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), Arm64 RyuJIT armv8.0-a // // Job=.NET 10.0 Runtime=.NET 10.0 // // | Method | Count | Mean | Ratio | Allocated | Alloc Ratio | // |-------------------- |-------- |----------:|------:|----------:|------------:| -// | Hexecs_Parallel | 100000 | 51.38 us | 1.00 | - | NA | -// | DefaultEcs_Parallel | 100000 | 77.63 us | 1.51 | - | NA | +// | FriFlo_Parallel | 100000 | 30.08 us | 0.33 | - | NA | +// | DefaultEcs_Parallel | 100000 | 86.19 us | 0.94 | - | NA | +// | Hexecs_Parallel | 100000 | 92.12 us | 1.00 | - | NA | // | | | | | | | -// | Hexecs_Parallel | 1000000 | 390.26 us | 1.00 | - | NA | -// | DefaultEcs_Parallel | 1000000 | 804.39 us | 2.06 | - | NA | +// | FriFlo_Parallel | 1000000 | 294.42 us | 0.38 | - | NA | +// | Hexecs_Parallel | 1000000 | 783.70 us | 1.00 | - | NA | +// | DefaultEcs_Parallel | 1000000 | 916.69 us | 1.17 | - | NA | [SimpleJob(RuntimeMoniker.Net10_0)] [Orderer(SummaryOrderPolicy.FastestToSlowest)] diff --git a/src/Hexecs.Benchmarks/Collections/SparseDictionary.cs b/src/Hexecs.Benchmarks/Collections/SparseDictionary.cs new file mode 100644 index 0000000..540a630 --- /dev/null +++ b/src/Hexecs.Benchmarks/Collections/SparseDictionary.cs @@ -0,0 +1,205 @@ +using System.Runtime.InteropServices; + +namespace Hexecs.Benchmarks.Collections; + +public sealed class SparseDictionary where TValue : struct +{ + private uint[] _sparse; + private uint[] _dense; + private TValue[] _values; + private int _count; + + public SparseDictionary(int capacity = 64) + { + _sparse = new uint[capacity]; + _dense = new uint[capacity]; + _values = new TValue[capacity]; + } + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _count; + } + + public ReadOnlySpan Keys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dense.AsSpan(0, _count); + } + + public ReadOnlySpan Values + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _values.AsSpan(0, _count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(uint key) + { + var sparse = _sparse; + if ((uint)key < (uint)sparse.Length) + { + return sparse[key] != 0; + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(uint key, out TValue value) + { + var sparse = _sparse; + if ((uint)key < (uint)sparse.Length) + { + var denseIndexPlusOne = sparse[key]; + if (denseIndexPlusOne != 0) + { + value = _values[denseIndexPlusOne - 1]; + return true; + } + } + value = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(uint key, TValue value) + { + if (TryAdd(key, value)) return; + Throw("Key already exists"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(uint key, TValue value) + { + var sparse = _sparse; + // Проверка на наличие места и ключа в массиве + if ((uint)key < (uint)sparse.Length && (uint)_count < (uint)_dense.Length) + { + ref var slot = ref sparse[key]; + if (slot == 0) + { + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = key; + _values[idx] = value; + _count++; + return true; + } + + if (_dense[slot - 1] == key) return false; + } + + return TryAddSlow(key, value); + } + + public bool Remove(uint key) + { + var sparse = _sparse; + if ((uint)key >= (uint)sparse.Length) return false; + + var denseIndexPlusOne = sparse[key]; + if (denseIndexPlusOne == 0) return false; + + var denseIndex = (int)denseIndexPlusOne - 1; + if (_dense[denseIndex] != key) return false; + + var lastIndex = _count - 1; + if (denseIndex != lastIndex) + { + var lastKey = _dense[lastIndex]; + _dense[denseIndex] = lastKey; + _values[denseIndex] = _values[lastIndex]; + _sparse[lastKey] = (uint)denseIndex + 1; + } + + sparse[key] = 0; + _count = lastIndex; + return true; + } + + public void Clear() + { + var dense = _dense; + var sparse = _sparse; + + for (var i = 0; i < _count; i++) + { + sparse[dense[i]] = 0; + } + + _count = 0; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private bool TryAddSlow(uint key, TValue value) + { + if ((uint)key >= (uint)_sparse.Length) + { + var newSize = Math.Max(_sparse.Length * 2, (int)key + 1); + Array.Resize(ref _sparse, newSize); + } + + if ((uint)_count >= (uint)_dense.Length) + { + var newSize = _dense.Length * 2; + Array.Resize(ref _dense, newSize); + Array.Resize(ref _values, newSize); + } + + ref var denseIndexPlusOne = ref _sparse[key]; + if (denseIndexPlusOne != 0) + { + if (_dense[denseIndexPlusOne - 1] == key) return false; + } + + var denseIndex = (uint)_count; + denseIndexPlusOne = denseIndex + 1; + _dense[denseIndex] = key; + _values[denseIndex] = value; + _count++; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(_values.AsSpan(0, _count)); + + public ref struct Enumerator + { + private ref TValue _current; + private int _remaining; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(Span values) + { + _remaining = values.Length; + + if (_remaining == 0) + { + _current = ref Unsafe.NullRef(); + return; + } + + ref var first = ref MemoryMarshal.GetReference(values); + _current = ref Unsafe.Subtract(ref first, 1); + } + + public ref TValue Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (--_remaining < 0) return false; + _current = ref Unsafe.Add(ref _current, 1); + return true; + } + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Throw(string message) => throw new InvalidOperationException(message); +} \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Collections/SparsePageDictionary.cs b/src/Hexecs.Benchmarks/Collections/SparsePageDictionary.cs index 0bdf04b..1109bd0 100644 --- a/src/Hexecs.Benchmarks/Collections/SparsePageDictionary.cs +++ b/src/Hexecs.Benchmarks/Collections/SparsePageDictionary.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Hexecs.Utils; namespace Hexecs.Benchmarks.Collections; @@ -51,10 +51,10 @@ public bool Contains(uint key) var page = pages[pageIndex]; if (page != null) { - var denseIndexPlusOne = page[key & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == key; + return page[key & PageMask] != 0; } } + return false; } @@ -70,15 +70,12 @@ public bool TryGetValue(uint key, out TValue value) var denseIndexPlusOne = page[key & PageMask]; if (denseIndexPlusOne != 0) { - var index = (int)denseIndexPlusOne - 1; - if (_dense[index] == key) - { - value = _values[index]; - return true; - } + value = _values[denseIndexPlusOne - 1]; + return true; } } } + value = default; return false; } @@ -87,7 +84,7 @@ public bool TryGetValue(uint key, out TValue value) public void Add(uint key, TValue value) { if (TryAdd(key, value)) return; - ActorError.AlreadyExists(key); // выбрасывает ошибку + Throw("Key already exists"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -188,39 +185,6 @@ private void EnsureDenseCapacity() } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() - { - var count = _count; - return new Enumerator( - _dense.AsSpan(0, count), - _values.AsSpan(0, count)); - } - - public ref struct Enumerator - { - private readonly ReadOnlySpan _keys; - private readonly Span _values; - private int _index; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Enumerator(ReadOnlySpan keys, Span values) - { - _keys = keys; - _values = values; - _index = -1; - } - - public ref TValue Current - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => ref _values[_index]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; - } - [MethodImpl(MethodImplOptions.NoInlining)] private bool TryAddSlow(uint key, TValue value) { @@ -231,8 +195,7 @@ private bool TryAddSlow(uint key, TValue value) ref var page = ref _sparsePages[pageIndex]; if (page == null) { - page = ArrayUtils.Create(PageSize); - Array.Clear(page, 0, page.Length); + page = new uint[PageSize]; } ref var denseIndexPlusOne = ref page[key & PageMask]; @@ -248,4 +211,46 @@ private bool TryAddSlow(uint key, TValue value) _count++; return true; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(_values.AsSpan(0, _count)); + + public ref struct Enumerator + { + private ref TValue _current; + private int _remaining; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(Span values) + { + _remaining = values.Length; + + if (_remaining == 0) + { + _current = ref Unsafe.NullRef(); + return; + } + + ref var first = ref MemoryMarshal.GetReference(values); + _current = ref Unsafe.Subtract(ref first, 1); + } + + public ref TValue Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + if (--_remaining < 0) return false; + _current = ref Unsafe.Add(ref _current, 1); + return true; + } + } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Throw(string message) => throw new InvalidOperationException(message); } \ No newline at end of file diff --git a/src/Hexecs.Benchmarks/Collections/SparsePageDictionaryBenchmark.cs b/src/Hexecs.Benchmarks/Collections/SparsePageDictionaryBenchmark.cs index 8d276b1..84ae8dc 100644 --- a/src/Hexecs.Benchmarks/Collections/SparsePageDictionaryBenchmark.cs +++ b/src/Hexecs.Benchmarks/Collections/SparsePageDictionaryBenchmark.cs @@ -2,25 +2,104 @@ // BenchmarkDotNet v0.15.8, Windows 11 (10.0.22621.4317/22H2/2022Update/SunValley2) // Intel Xeon CPU E5-2697 v3 2.60GHz, 2 CPU, 56 logical and 28 physical cores -// .NET SDK 10.0.100 -// [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -// .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 - +// .NET SDK 10.0.102 +// [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 +// // Job=.NET 10.0 Runtime=.NET 10.0 // -// | Method | N | Mean | Allocated | -// |---------------------- |----- |-------------:|----------:| -// | Iterate_Sparse | 1000 | 509.2 ns | - | -// | Iterate_Dict | 1000 | 1,062.4 ns | - | -// | Iterate_Sparse_Span | 1000 | 362.0 ns | - | -// | Contains_Sparse_Hit | 1000 | 3,184.4 ns | - | -// | Contains_Dict_Hit | 1000 | 4,950.2 ns | - | -// | Contains_Sparse_Miss | 1000 | 689.1 ns | - | -// | Contains_Dict_Miss | 1000 | 4,156.2 ns | - | -// | TryGetValue_Sparse | 1000 | 5,208.9 ns | - | -// | TryGetValue_Dict | 1000 | 5,057.5 ns | - | -// | AddRemoveCycle_Sparse | 1000 | 94,951.8 ns | - | -// | AddRemoveCycle_Dict | 1000 | 155,528.9 ns | - | +// | Method | N | Mean | Allocated | +// |-------------------------- |--------- |------------------:|----------:| +// | Iterate_Sparse | 10 | 7.054 ns | - | +// | Iterate_SparsePage | 10 | 6.878 ns | - | +// | Iterate_Dict | 10 | 12.023 ns | - | +// | Contains_Sparse_Hit | 10 | 9.285 ns | - | +// | Contains_SparsePage_Hit | 10 | 15.746 ns | - | +// | Contains_Dict_Hit | 10 | 48.268 ns | - | +// | Contains_Sparse_Miss | 10 | 849.891 ns | - | +// | Contains_SparsePage_Miss | 10 | 1,518.830 ns | - | +// | Contains_Dict_Miss | 10 | 4,387.313 ns | - | +// | TryGetValue_Sparse | 10 | 15.586 ns | - | +// | TryGetValue_SparsePage | 10 | 26.951 ns | - | +// | TryGetValue_Dict | 10 | 51.527 ns | - | +// | AddRemoveCycle_Sparse | 10 | 642.318 ns | - | +// | AddRemoveCycle_SparsePage | 10 | 757.177 ns | - | +// | AddRemoveCycle_Dict | 10 | 1,380.607 ns | - | +// | Iterate_Sparse | 100 | 61.185 ns | - | +// | Iterate_SparsePage | 100 | 63.300 ns | - | +// | Iterate_Dict | 100 | 123.782 ns | - | +// | Contains_Sparse_Hit | 100 | 93.043 ns | - | +// | Contains_SparsePage_Hit | 100 | 158.254 ns | - | +// | Contains_Dict_Hit | 100 | 502.972 ns | - | +// | Contains_Sparse_Miss | 100 | 751.221 ns | - | +// | Contains_SparsePage_Miss | 100 | 1,328.104 ns | - | +// | Contains_Dict_Miss | 100 | 3,853.558 ns | - | +// | TryGetValue_Sparse | 100 | 133.746 ns | - | +// | TryGetValue_SparsePage | 100 | 275.022 ns | - | +// | TryGetValue_Dict | 100 | 523.184 ns | - | +// | AddRemoveCycle_Sparse | 100 | 6,144.933 ns | - | +// | AddRemoveCycle_SparsePage | 100 | 7,791.588 ns | - | +// | AddRemoveCycle_Dict | 100 | 13,226.643 ns | - | +// | Iterate_Sparse | 1000 | 545.015 ns | - | +// | Iterate_SparsePage | 1000 | 548.579 ns | - | +// | Iterate_Dict | 1000 | 1,218.623 ns | - | +// | Contains_Sparse_Hit | 1000 | 893.312 ns | - | +// | Contains_SparsePage_Hit | 1000 | 1,592.796 ns | - | +// | Contains_Dict_Hit | 1000 | 5,101.833 ns | - | +// | Contains_Sparse_Miss | 1000 | 841.138 ns | - | +// | Contains_SparsePage_Miss | 1000 | 1,557.011 ns | - | +// | Contains_Dict_Miss | 1000 | 4,262.587 ns | - | +// | TryGetValue_Sparse | 1000 | 1,378.559 ns | - | +// | TryGetValue_SparsePage | 1000 | 2,631.303 ns | - | +// | TryGetValue_Dict | 1000 | 5,671.514 ns | - | +// | AddRemoveCycle_Sparse | 1000 | 59,471.171 ns | - | +// | AddRemoveCycle_SparsePage | 1000 | 77,915.215 ns | - | +// | AddRemoveCycle_Dict | 1000 | 136,277.304 ns | - | +// | Iterate_Sparse | 100000 | 54,179.301 ns | - | +// | Iterate_SparsePage | 100000 | 52,604.863 ns | - | +// | Iterate_Dict | 100000 | 115,975.105 ns | - | +// | Contains_Sparse_Hit | 100000 | 1,213.075 ns | - | +// | Contains_SparsePage_Hit | 100000 | 3,990.269 ns | - | +// | Contains_Dict_Hit | 100000 | 12,338.853 ns | - | +// | Contains_Sparse_Miss | 100000 | 873.536 ns | - | +// | Contains_SparsePage_Miss | 100000 | 1,536.765 ns | - | +// | Contains_Dict_Miss | 100000 | 6,450.297 ns | - | +// | TryGetValue_Sparse | 100000 | 4,264.815 ns | - | +// | TryGetValue_SparsePage | 100000 | 7,288.517 ns | - | +// | TryGetValue_Dict | 100000 | 13,796.095 ns | - | +// | AddRemoveCycle_Sparse | 100000 | 67,421.741 ns | - | +// | AddRemoveCycle_SparsePage | 100000 | 123,045.159 ns | - | +// | AddRemoveCycle_Dict | 100000 | 128,143.984 ns | - | +// | Iterate_Sparse | 1000000 | 502,558.740 ns | - | +// | Iterate_SparsePage | 1000000 | 507,753.446 ns | - | +// | Iterate_Dict | 1000000 | 1,306,812.264 ns | - | +// | Contains_Sparse_Hit | 1000000 | 4,323.616 ns | - | +// | Contains_SparsePage_Hit | 1000000 | 24,987.266 ns | - | +// | Contains_Dict_Hit | 1000000 | 24,568.160 ns | - | +// | Contains_Sparse_Miss | 1000000 | 846.263 ns | - | +// | Contains_SparsePage_Miss | 1000000 | 1,478.259 ns | - | +// | Contains_Dict_Miss | 1000000 | 6,356.062 ns | - | +// | TryGetValue_Sparse | 1000000 | 20,006.167 ns | - | +// | TryGetValue_SparsePage | 1000000 | 37,532.782 ns | - | +// | TryGetValue_Dict | 1000000 | 24,830.077 ns | - | +// | AddRemoveCycle_Sparse | 1000000 | 124,740.165 ns | - | +// | AddRemoveCycle_SparsePage | 1000000 | 585,653.058 ns | - | +// | AddRemoveCycle_Dict | 1000000 | 136,532.017 ns | - | +// | Iterate_Sparse | 10000000 | 6,383,516.042 ns | - | +// | Iterate_SparsePage | 10000000 | 6,266,198.828 ns | - | +// | Iterate_Dict | 10000000 | 19,314,062.188 ns | - | +// | Contains_Sparse_Hit | 10000000 | 8,714.852 ns | - | +// | Contains_SparsePage_Hit | 10000000 | 38,036.706 ns | - | +// | Contains_Dict_Hit | 10000000 | 53,292.242 ns | - | +// | Contains_Sparse_Miss | 10000000 | 861.005 ns | - | +// | Contains_SparsePage_Miss | 10000000 | 1,514.329 ns | - | +// | Contains_Dict_Miss | 10000000 | 9,843.414 ns | - | +// | TryGetValue_Sparse | 10000000 | 44,180.863 ns | - | +// | TryGetValue_SparsePage | 10000000 | 64,472.113 ns | - | +// | TryGetValue_Dict | 10000000 | 56,616.146 ns | - | +// | AddRemoveCycle_Sparse | 10000000 | 206,125.551 ns | - | +// | AddRemoveCycle_SparsePage | 10000000 | 891,220.111 ns | - | +// | AddRemoveCycle_Dict | 10000000 | 201,447.264 ns | - | [SimpleJob(RuntimeMoniker.Net10_0)] [MeanColumn, MemoryDiagnoser] @@ -30,51 +109,76 @@ [BenchmarkCategory("Collections")] public class SparsePageDictionaryBenchmark { - private SparsePageDictionary _sparse = null!; - private SparsePageDictionary _sparseCycle = null!; + private SparsePageDictionary _sparsePage = null!; + private SparsePageDictionary _sparsePageCycle = null!; + + private SparseDictionary _sparse = null!; + private SparseDictionary _sparseCycle = null!; + private Dictionary _dict = null!; private Dictionary _dictCycle = null!; + private uint[] _keys = null!; private uint[] _lookupKeys = null!; private uint[] _missingKeys = null!; - [Params(500)] + [Params(10, 100, 1_000, 100_000, 1_000_000, 10_000_000)] public int N; [GlobalSetup] public void Setup() { - // Генерируем уникальные ключи в диапазоне 0–1M - _keys = Enumerable.Range(0, N) - .Select(_ => (uint)Random.Shared.Next(0, 1_000_000)) + var keyRange = Math.Max(N * 10, 1_000); + _keys = Enumerable.Range(0, N * 2) + .Select(_ => (uint)Random.Shared.Next(0, keyRange)) .Distinct() .Take(N) .ToArray(); - // Ключи для поиска (существующие) _lookupKeys = _keys .OrderBy(_ => Random.Shared.Next()) .Take(Math.Min(1000, _keys.Length)) .ToArray(); - // Ключи которых нет в словаре - _missingKeys = Enumerable.Range(0, 1000) - .Select(_ => (uint)Random.Shared.Next(2_000_000, 3_000_000)) - .ToArray(); + var present = new bool[keyRange]; + for (var i = 0; i < _keys.Length; i++) + present[_keys[i]] = true; + + var missCount = 1000; + var missing = new uint[missCount]; + var written = 0; + + for (var k = 0; k < keyRange && written < missCount; k++) + { + if (!present[k]) missing[written++] = (uint)k; + } - _sparse = new SparsePageDictionary(denseCapacity: N); - _sparseCycle = new SparsePageDictionary(denseCapacity: N); - _dict = new Dictionary(N); - _dictCycle = new Dictionary(N); + if (written < missCount) Array.Resize(ref missing, written); + _missingKeys = missing; + _sparsePage = new SparsePageDictionary(denseCapacity: N); + _sparsePageCycle = new SparsePageDictionary(denseCapacity: N); + + _sparse = new SparseDictionary(capacity: keyRange); + _sparseCycle = new SparseDictionary(capacity: keyRange); + + _dict = new Dictionary(capacity: N); + _dictCycle = new Dictionary(capacity: N); + + const int value = 42; foreach (var key in _keys) { - _sparse.Add(key, 42); - _sparseCycle.Add(key, 42); - _dict[key] = 42; - _dictCycle[key] = 42; + _sparsePage.Add(key, value); + _sparsePageCycle.Add(key, value); + + _sparse.Add(key, value); + _sparseCycle.Add(key, value); + + _dict.Add(key, value); + _dictCycle.Add(key, value); } + _sparsePageCycle.Clear(); _sparseCycle.Clear(); _dictCycle.Clear(); } @@ -94,25 +198,24 @@ public int Iterate_Sparse() } [Benchmark] - public int Iterate_Dict() + public int Iterate_SparsePage() { var sum = 0; - foreach (var kv in _dict) + foreach (var entry in _sparsePage) { - sum += kv.Value; + sum += entry; } return sum; } [Benchmark] - public int Iterate_Sparse_Span() + public int Iterate_Dict() { var sum = 0; - var values = _sparse.Values; - for (var i = 0; i < values.Length; i++) + foreach (var kv in _dict) { - sum += values[i]; + sum += kv.Value; } return sum; @@ -132,6 +235,18 @@ public bool Contains_Sparse_Hit() return found; } + [Benchmark] + public bool Contains_SparsePage_Hit() + { + var found = false; + foreach (var key in _lookupKeys) + { + found = _sparsePage.Contains(key); + } + + return found; + } + [Benchmark] public bool Contains_Dict_Hit() { @@ -158,6 +273,18 @@ public bool Contains_Sparse_Miss() return found; } + [Benchmark] + public bool Contains_SparsePage_Miss() + { + var found = false; + foreach (var key in _missingKeys) + { + found = _sparsePage.Contains(key); + } + + return found; + } + [Benchmark] public bool Contains_Dict_Miss() { @@ -187,6 +314,21 @@ public int TryGetValue_Sparse() return sum; } + [Benchmark] + public int TryGetValue_SparsePage() + { + var sum = 0; + foreach (var key in _lookupKeys) + { + if (_sparsePage.TryGetValue(key, out var value)) + { + sum += value; + } + } + + return sum; + } + [Benchmark] public int TryGetValue_Dict() { @@ -229,6 +371,31 @@ public int AddRemoveCycle_Sparse() return count; } + [Benchmark] + public int AddRemoveCycle_SparsePage() + { + var dict = _sparsePageCycle; + var count = 0; + + // Симуляция ECS: добавляем/удаляем entity из фильтра + for (var i = 0; i < 10; i++) + { + foreach (var key in _lookupKeys) + { + dict.Add(key, 42); + } + + count += dict.Count; + + foreach (var key in _lookupKeys) + { + dict.Remove(key); + } + } + + return count; + } + [Benchmark] public int AddRemoveCycle_Dict() { diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs index 3c07ae5..c06c904 100644 --- a/src/Hexecs.Benchmarks/Program.cs +++ b/src/Hexecs.Benchmarks/Program.cs @@ -1,4 +1,6 @@ using BenchmarkDotNet.Running; +using Hexecs.Benchmarks.Actors; +using Hexecs.Benchmarks.Collections; -//BenchmarkRunner.Run(); -BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); \ No newline at end of file +BenchmarkRunner.Run(); +//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/src/Hexecs.Benchmarks/Usings.cs b/src/Hexecs.Benchmarks/Usings.cs index e9c615c..854fa6e 100644 --- a/src/Hexecs.Benchmarks/Usings.cs +++ b/src/Hexecs.Benchmarks/Usings.cs @@ -1,6 +1,8 @@ // Global using directives global using System; +global using System.Diagnostics.CodeAnalysis; +global using System.Runtime.CompilerServices; global using BenchmarkDotNet.Attributes; global using BenchmarkDotNet.Jobs; global using BenchmarkDotNet.Order; diff --git a/src/Hexecs.Tests/Actors/ActorComponentShould.cs b/src/Hexecs.Tests/Actors/ActorComponentShould.cs index 362987a..d5258b8 100644 --- a/src/Hexecs.Tests/Actors/ActorComponentShould.cs +++ b/src/Hexecs.Tests/Actors/ActorComponentShould.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorConstraintShould.cs b/src/Hexecs.Tests/Actors/ActorConstraintShould.cs index 1663309..974102d 100644 --- a/src/Hexecs.Tests/Actors/ActorConstraintShould.cs +++ b/src/Hexecs.Tests/Actors/ActorConstraintShould.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorContextShould.cs b/src/Hexecs.Tests/Actors/ActorContextShould.cs index 54d5dee..289e7c8 100644 --- a/src/Hexecs.Tests/Actors/ActorContextShould.cs +++ b/src/Hexecs.Tests/Actors/ActorContextShould.cs @@ -134,15 +134,16 @@ public void ClearAllActors() public void TryGetActorWithComponent(int attackValue, int defenceValue) { // arrange - var actor = fixture.Actors.CreateActor(); + var actors = fixture.Actors; + + var actor = actors.CreateActor(); actor.Add(new Attack { Value = attackValue }); actor.Add(new Defence { Value = defenceValue }); // act - var hasAttack = fixture.Actors.TryGetActor(actor.Id, out var actorWithAttack); - var hasDefence = fixture.Actors.TryGetActor(actor.Id, out var actorWithDefence); - var hasNonExistentComponent = - fixture.Actors.TryGetActor(actor.Id, out var actorWithNonExistent); + var hasAttack = actors.TryGetActor(actor.Id, out var actorWithAttack); + var hasDefence = actors.TryGetActor(actor.Id, out var actorWithDefence); + var hasNonExistentComponent = actors.TryGetActor(actor.Id, out _); // assert hasAttack.Should().BeTrue(); @@ -327,7 +328,7 @@ public void CallHandlerWithValueWhenComponentAdded() uint? addedActorId = null; Attack? addedComponent = null; - fixture.Actors.OnComponentAdded((uint id, int _, ref Attack component) => + fixture.Actors.OnComponentAdded((uint id, ref Attack component) => { addedActorId = id; addedComponent = component; @@ -616,7 +617,7 @@ public void GetActorWithPredicate() actor3.Add(new Attack { Value = 15 }); // act - var actorWithAttack10 = fixture.Actors.GetActor((in ActorRef actor) => actor.Get().Value == 10); + var actorWithAttack10 = fixture.Actors.GetActor((in ActorRef actor) => actor.Get().Value == 10); // assert actorWithAttack10.Id.Should().Be(actor2.Id); diff --git a/src/Hexecs.Tests/Actors/ActorFilter1Should.cs b/src/Hexecs.Tests/Actors/ActorFilter1Should.cs index 357cd38..d16fee7 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter1Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter1Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Utils; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorFilter2Should.cs b/src/Hexecs.Tests/Actors/ActorFilter2Should.cs index b37c729..c717fc7 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter2Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter2Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorFilter3Should.cs b/src/Hexecs.Tests/Actors/ActorFilter3Should.cs index 18564ae..1e842b8 100644 --- a/src/Hexecs.Tests/Actors/ActorFilter3Should.cs +++ b/src/Hexecs.Tests/Actors/ActorFilter3Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorListShould.cs b/src/Hexecs.Tests/Actors/ActorListShould.cs index bdb98d1..04adaae 100644 --- a/src/Hexecs.Tests/Actors/ActorListShould.cs +++ b/src/Hexecs.Tests/Actors/ActorListShould.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; diff --git a/src/Hexecs.Tests/Actors/ActorMarshalShould.cs b/src/Hexecs.Tests/Actors/ActorMarshalShould.cs index 115745d..1505cdc 100644 --- a/src/Hexecs.Tests/Actors/ActorMarshalShould.cs +++ b/src/Hexecs.Tests/Actors/ActorMarshalShould.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; -using Hexecs.Actors.Components; -using Hexecs.Tests.Mocks; +using Hexecs.Actors.Components; using Hexecs.Tests.Mocks.ActorComponents; namespace Hexecs.Tests.Actors; @@ -43,59 +41,6 @@ public void GetComponentType() .Be(typeof(Attack)); } - [Fact] - public void GetComponentIndex() - { - // arrange - - var component = fixture.CreateComponent(); - var actor = fixture.CreateActor(component1: component); - var expectedIndex = fixture.Actors - .GetOrCreateComponentPool() - .TryGetIndex(actor.Id); - - // act - - var actualIndex = ActorMarshal.GetComponentIndex(fixture.Actors, actor.Id); - - // assert - - actualIndex - .Should() - .BeGreaterThanOrEqualTo(0); - - actualIndex - .Should() - .Be(expectedIndex); - } - - [Fact] - public void GetComponentByIndex() - { - // arrange - - var component = fixture.CreateComponent(); - var actor = fixture.CreateActor(component1: component); - var index = fixture.Actors - .GetOrCreateComponentPool() - .TryGetIndex(actor.Id); - - // act - - ref var actualComponent = ref ActorMarshal.GetComponentByIndex(fixture.Actors, index); - - // assert - - Unsafe - .IsNullRef(ref actualComponent) - .Should() - .BeFalse(); - - actualComponent - .Should() - .Be(component); - } - [Fact] public void GetComponentOwner() { diff --git a/src/Hexecs.Tests/Actors/ActorNodeShould.cs b/src/Hexecs.Tests/Actors/ActorNodeShould.cs index 7bb3198..0db54d4 100644 --- a/src/Hexecs.Tests/Actors/ActorNodeShould.cs +++ b/src/Hexecs.Tests/Actors/ActorNodeShould.cs @@ -76,8 +76,8 @@ public void HasParent() .Be(expectedParent); } - [Fact(DisplayName = "Родитель должен быть удалён после уничтожения родителя")] - public void RemoveParentAfterDestroyParent() + [Fact(DisplayName = "Дочерний элемент не должен иметь родителя после уничтожения родителя")] + public void RemoveChildAfterDestroyParent() { // Arrange @@ -101,8 +101,8 @@ public void RemoveParentAfterDestroyParent() .Should() .BeEquivalentTo(Actor.Empty); } - - [Fact(DisplayName = "Дочерний элемент должен быть удалён после уничтожения")] + + [Fact(DisplayName = "Дочерний элемент должен быть удалён из родителя после уничтожения дочернего элемента")] public void RemoveChildAfterDestroyChild() { // Arrange @@ -115,7 +115,7 @@ public void RemoveChildAfterDestroyChild() parent.AddChild(child1); parent.AddChild(child2); parent.AddChild(child3); - + // Act child2.Destroy(); @@ -137,4 +137,96 @@ public void RemoveChildAfterDestroyChild() .Should() .NotContain(child2); } + + [Fact(DisplayName = "Дочерний элемент должен сменить родителя при добавлении к новому")] + public void ChangeParentCorrectly() + { + // Arrange + var child = fixture.CreateActor(); + var parent1 = fixture.CreateActor(); + var parent2 = fixture.CreateActor(); + + // Act + parent1.AddChild(child); + parent2.AddChild(child); // Должен автоматически удалиться из parent1 + + // Assert + parent1.Children().Length.Should().Be(0); + parent2.Children().Length.Should().Be(1); + + child.TryGetParent(out var actualParent).Should().BeTrue(); + actualParent.Should().Be(parent2); + } + + [Fact(DisplayName = "Удаление первого ребенка должно корректно обновлять FirstChildId")] + public void RemoveFirstChildCorrectly() + { + // Arrange + var parent = fixture.CreateActor(); + var child1 = fixture.CreateActor(); + var child2 = fixture.CreateActor(); + parent.AddChild(child1); // child1 станет NextSibling для child2 + parent.AddChild(child2); // child2 теперь FirstChildId + + // Act + child2.Destroy(); + + // Assert + parent.Children().Length.Should().Be(1); + parent.Children().ToArray().Should().ContainSingle().Which.Should().Be(child1); + } + + [Fact(DisplayName = "Удаление среднего ребенка должно корректно связывать соседей")] + public void RemoveMiddleChildCorrectly() + { + // Arrange + var parent = fixture.CreateActor(); + var child1 = fixture.CreateActor(); + var child2 = fixture.CreateActor(); + var child3 = fixture.CreateActor(); + + // Порядок в списке (LIFO): child3 -> child2 -> child1 + parent.AddChild(child1); + parent.AddChild(child2); + parent.AddChild(child3); + + // Act + child2.Destroy(); // Удаляем средний + + // Assert + var children = parent.Children().ToArray(); + children.Length.Should().Be(2); + children[0].Should().Be(child3); + children[1].Should().Be(child1); + } + + [Fact(DisplayName = "Проверка метода HasChild")] + public void HasChildMethodWorks() + { + // Arrange + var parent = fixture.CreateActor(); + var child = fixture.CreateActor(); + var stranger = fixture.CreateActor(); + + // Act + parent.AddChild(child); + + // Assert + parent.HasChild(child).Should().BeTrue(); + parent.HasChild(stranger).Should().BeFalse(); + } + + [Fact(DisplayName = "Родитель не может добавить самого себя в дети")] + public void ParentCannotAddItselfAsChild() + { + // Arrange + var actor = fixture.CreateActor(); + + // Act + actor.AddChild(actor); + + // Assert + actor.Children().Length.Should().Be(0); + actor.TryGetParent(out _).Should().BeFalse(); + } } \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorSystemShould1.cs b/src/Hexecs.Tests/Actors/ActorSystemShould1.cs new file mode 100644 index 0000000..6f1b7df --- /dev/null +++ b/src/Hexecs.Tests/Actors/ActorSystemShould1.cs @@ -0,0 +1,91 @@ +using Hexecs.Actors.Systems; +using Hexecs.Dependencies; +using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Threading; +using Hexecs.Worlds; + +namespace Hexecs.Tests.Actors; + +public sealed class ActorSystemShould1(ActorTestFixture fixture) : IClassFixture +{ + [Fact(DisplayName = "Параллельные системы должны быть вызваны параллельно")] + public void ConfigureAndRunSystemsInParallel() + { + var systems = fixture.CreateArray(_ => new Mock()); + + using var world = new WorldBuilder() + .UseDefaultParallelWorker(degreeOfParallelism: 4) + .UseDefaultActorContext(cfg => cfg + .CreateParallelUpdateSystem(systems.Select(mock => mock.Object))) + .Build(); + + world.Update(); + } + + [Theory(DisplayName = "Параллельная система должны параллельно обработать всех акторов и только один раз")] + [InlineData(2, 1)] + [InlineData(2, 2)] + [InlineData(2, 3)] + [InlineData(2, 999)] + [InlineData(2, 1000)] + [InlineData(2, 1001)] + [InlineData(3, 1)] + [InlineData(3, 2)] + [InlineData(3, 3)] + [InlineData(3, 999)] + [InlineData(3, 1000)] + [InlineData(3, 1001)] + public void UpdateActorsInParallel(int degreeOfParallelism, int actorCount) + { + // arrange + + using var world = new WorldBuilder() + .UseDefaultParallelWorker(degreeOfParallelism) + .UseDefaultActorContext(cfg => cfg + .CreateUpdateSystem(ctx => new ParallelUpdateSystem( + ctx, + ctx.GetRequiredService()))) + .Build(); + + var actorContext = world.Actors; + for (uint i = 1; i <= actorCount; i++) + { + var actor = actorContext.CreateActor(i); + actor.Add(new Attack { Value = 0 }); + } + + // act + + world.Update(); + + // assert + + + var actorFilter = actorContext.Filter(); + + actorFilter.Length + .Should() + .Be(actorCount); + + foreach (var actor in actorFilter) + { + actor.Component1.Value + .Should() + .Be(1, + "Component {0} value of actor {1} should be updated to 1", + actor.Component1.GetType().Name, + actor.Id); + } + } + + private sealed class ParallelUpdateSystem( + ActorContext context, + IParallelWorker parallelWorker) + : UpdateSystem(context, parallelWorker: parallelWorker) + { + protected override void Update(in ActorRef actor, in WorldTime time) + { + actor.Component1.Value += 1; + } + } +} \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorSystemShould2.cs b/src/Hexecs.Tests/Actors/ActorSystemShould2.cs new file mode 100644 index 0000000..9cba2c3 --- /dev/null +++ b/src/Hexecs.Tests/Actors/ActorSystemShould2.cs @@ -0,0 +1,100 @@ +using Hexecs.Actors.Systems; +using Hexecs.Dependencies; +using Hexecs.Tests.Mocks.ActorComponents; +using Hexecs.Threading; +using Hexecs.Worlds; + +namespace Hexecs.Tests.Actors; + +public sealed class ActorSystemShould2(ActorTestFixture fixture) : IClassFixture +{ + [Fact(DisplayName = "Параллельные системы должны быть вызваны параллельно")] + public void ConfigureAndRunSystemsInParallel() + { + var systems = fixture.CreateArray(_ => new Mock()); + + using var world = new WorldBuilder() + .UseDefaultParallelWorker(degreeOfParallelism: 4) + .UseDefaultActorContext(cfg => cfg + .CreateParallelUpdateSystem(systems.Select(mock => mock.Object))) + .Build(); + + world.Update(); + } + + [Theory(DisplayName = "Параллельная система должны параллельно обработать всех акторов и только один раз")] + [InlineData(2, 1)] + [InlineData(2, 2)] + [InlineData(2, 3)] + [InlineData(2, 999)] + [InlineData(2, 1000)] + [InlineData(2, 1001)] + [InlineData(3, 1)] + [InlineData(3, 2)] + [InlineData(3, 3)] + [InlineData(3, 999)] + [InlineData(3, 1000)] + [InlineData(3, 1001)] + public void UpdateActorsInParallel(int degreeOfParallelism, int actorCount) + { + // arrange + + using var world = new WorldBuilder() + .UseDefaultParallelWorker(degreeOfParallelism) + .UseDefaultActorContext(cfg => cfg + .CreateUpdateSystem(ctx => new ParallelUpdateSystem( + ctx, + ctx.GetRequiredService()))) + .Build(); + + var actorContext = world.Actors; + for (uint i = 1; i <= actorCount; i++) + { + var actor = actorContext.CreateActor(i); + actor.Add(new Attack { Value = 0 }); + actor.Add(new Defence { Value = 0 }); + } + + // act + + world.Update(); + + // assert + + + var actorFilter = actorContext.Filter(); + + actorFilter.Length + .Should() + .Be(actorCount); + + foreach (var actor in actorFilter) + { + actor.Component1.Value + .Should() + .Be(1, + "Component {0} value of actor {1} should be updated to 1", + actor.Component1.GetType().Name, + actor.Id); + + actor.Component2.Value + .Should() + .Be(1, + "Component {0} value of actor {1} should be updated to 1", + actor.Component1.GetType().Name, + actor.Id); + } + } + + private sealed class ParallelUpdateSystem( + ActorContext context, + IParallelWorker parallelWorker) + : UpdateSystem(context, parallelWorker: parallelWorker) + { + protected override void Update(in ActorRef actor, in WorldTime time) + { + actor.Component1.Value += 1; + actor.Component2.Value += 1; + } + } +} \ No newline at end of file diff --git a/src/Hexecs.Tests/Actors/ActorSystemShould.cs b/src/Hexecs.Tests/Actors/ActorSystemShould3.cs similarity index 96% rename from src/Hexecs.Tests/Actors/ActorSystemShould.cs rename to src/Hexecs.Tests/Actors/ActorSystemShould3.cs index 5bab88f..8d944cf 100644 --- a/src/Hexecs.Tests/Actors/ActorSystemShould.cs +++ b/src/Hexecs.Tests/Actors/ActorSystemShould3.cs @@ -1,13 +1,12 @@ using Hexecs.Actors.Systems; using Hexecs.Dependencies; -using Hexecs.Tests.Mocks; using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Threading; using Hexecs.Worlds; namespace Hexecs.Tests.Actors; -public sealed class ActorSystemShould(ActorTestFixture fixture) : IClassFixture +public sealed class ActorSystemShould3(ActorTestFixture fixture) : IClassFixture { [Fact(DisplayName = "Параллельные системы должны быть вызваны параллельно")] public void ConfigureAndRunSystemsInParallel() diff --git a/src/Hexecs.Tests/Actors/ActorTestFixture.cs b/src/Hexecs.Tests/Actors/ActorTestFixture.cs index bc25dc8..965b6e4 100644 --- a/src/Hexecs.Tests/Actors/ActorTestFixture.cs +++ b/src/Hexecs.Tests/Actors/ActorTestFixture.cs @@ -1,6 +1,5 @@ using Hexecs.Assets; using Hexecs.Assets.Sources; -using Hexecs.Tests.Mocks; using Hexecs.Tests.Mocks.ActorComponents; using Hexecs.Tests.Mocks.Assets; using Hexecs.Worlds; diff --git a/src/Hexecs.Tests/Assets/AssetConstraintShould.cs b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs index f510eec..3198019 100644 --- a/src/Hexecs.Tests/Assets/AssetConstraintShould.cs +++ b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs @@ -1,6 +1,4 @@ using Hexecs.Assets; -using Hexecs.Tests.Actors; -using Hexecs.Tests.Mocks; using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs index c90c659..e452589 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.Assets; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter2Should.cs b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs index 6aee3c5..9008d8f 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter2Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.Assets; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetFilter3Should.cs b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs index d81b730..fe6b906 100644 --- a/src/Hexecs.Tests/Assets/AssetFilter3Should.cs +++ b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs @@ -1,5 +1,4 @@ -using Hexecs.Tests.Mocks; -using Hexecs.Tests.Mocks.Assets; +using Hexecs.Tests.Mocks.Assets; namespace Hexecs.Tests.Assets; diff --git a/src/Hexecs.Tests/Assets/AssetTestFixture.cs b/src/Hexecs.Tests/Assets/AssetTestFixture.cs index a0aa2ca..87098d2 100644 --- a/src/Hexecs.Tests/Assets/AssetTestFixture.cs +++ b/src/Hexecs.Tests/Assets/AssetTestFixture.cs @@ -1,6 +1,5 @@ using Hexecs.Assets; using Hexecs.Assets.Sources; -using Hexecs.Tests.Mocks; using Hexecs.Tests.Mocks.Assets; using Hexecs.Worlds; diff --git a/src/Hexecs/Actors/Actor.cs b/src/Hexecs/Actors/Actor.cs index 7872f09..80f11db 100644 --- a/src/Hexecs/Actors/Actor.cs +++ b/src/Hexecs/Actors/Actor.cs @@ -169,6 +169,9 @@ public ref T AddRelation(in Actor relative, in T relation) where T : struct [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() where T : struct, IActorComponent => Context.HasComponent(Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasChild(in Actor child) => Context.HasChild(Id, child.Id); + /// /// Проверяет, существует ли отношение между текущим актёром и указанным родственным актёром. /// diff --git a/src/Hexecs/Actors/Actor1.cs b/src/Hexecs/Actors/Actor1.cs index 6257dc4..1d13a9d 100644 --- a/src/Hexecs/Actors/Actor1.cs +++ b/src/Hexecs/Actors/Actor1.cs @@ -188,6 +188,9 @@ public ref T AddRelation(in Actor relative, in T relation) where T : struct [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() where T : struct, IActorComponent => Context.HasComponent(Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasChild(in Actor child) => Context.HasChild(Id, child.Id); + /// /// Проверяет, существует ли отношение между текущим актёром и указанным родственным актёром. /// diff --git a/src/Hexecs/Actors/ActorContext.Children.cs b/src/Hexecs/Actors/ActorContext.Children.cs index 2974790..571e14e 100644 --- a/src/Hexecs/Actors/ActorContext.Children.cs +++ b/src/Hexecs/Actors/ActorContext.Children.cs @@ -6,48 +6,151 @@ public sealed partial class ActorContext { public void AddChild(uint parentId, uint childId) { - var pool = GetOrCreateComponentPool(); + if (parentId == childId) return; - ref var child = ref GetOrAddComponent(childId, ActorNodeComponent.Create); - ref var parent = ref GetOrAddComponent(parentId, ActorNodeComponent.Create); + ref var childNode = ref GetOrAddComponent(childId); - parent.Node.AddChild(child.Node); + if (childNode.ParentId == parentId) return; + + // Если у ребенка уже есть родитель — отсоединяем + if (childNode.ParentId != 0) + { + RemoveChild(childNode.ParentId, childId); + } + + ref var parentNode = ref GetOrAddComponent(parentId); + + // Вставляем ребенка в начало списка детей родителя + var oldFirstId = parentNode.FirstChildId; + childNode.ParentId = parentId; + childNode.NextSiblingId = oldFirstId; + childNode.PrevSiblingId = 0; + + if (oldFirstId != 0) + { + ref var oldFirstNode = ref GetComponent(oldFirstId); + oldFirstNode.PrevSiblingId = childId; + } + + parentNode.FirstChildId = childId; + parentNode.ChildCount++; } public ChildrenEnumerator Children(uint parentId) { ref var component = ref TryGetComponentRef(parentId); - return Unsafe.IsNullRef(ref component) + return Unsafe.IsNullRef(ref component) || component.FirstChildId == 0 ? ChildrenEnumerator.Empty - : new ChildrenEnumerator(this, component.Node.AsSpan()); + : new ChildrenEnumerator(this, component.FirstChildId, component.ChildCount); } public bool HasChild(uint parentId, uint childId) { ref var component = ref TryGetComponentRef(parentId); - return !Unsafe.IsNullRef(ref component) && component.Node.HasChild(childId); + if (Unsafe.IsNullRef(ref component) || component.FirstChildId == 0) return false; + + foreach (var child in Children(parentId)) + { + if (child.Id == childId) return true; + } + + return false; } public bool RemoveChild(uint parentId, uint childId) { - ref var component = ref TryGetComponentRef(parentId); - return !Unsafe.IsNullRef(ref component) && component.Node.RemoveChild(childId); + ref var parentNode = ref TryGetComponentRef(parentId); + if (Unsafe.IsNullRef(ref parentNode)) return false; + + ref var childNode = ref TryGetComponentRef(childId); + if (Unsafe.IsNullRef(ref childNode) || childNode.ParentId != parentId) return false; + + // 1. Обновляем ссылку у предыдущего соседа или у родителя + if (childNode.PrevSiblingId != 0) + { + ref var prevNode = ref GetComponent(childNode.PrevSiblingId); + prevNode.NextSiblingId = childNode.NextSiblingId; + } + else + { + // Это был первый ребенок + parentNode.FirstChildId = childNode.NextSiblingId; + } + + // 2. Обновляем ссылку у следующего соседа + if (childNode.NextSiblingId != 0) + { + ref var nextNode = ref GetComponent(childNode.NextSiblingId); + nextNode.PrevSiblingId = childNode.PrevSiblingId; + } + + // 3. Сбрасываем данные узла + childNode.ParentId = 0; + childNode.NextSiblingId = 0; + childNode.PrevSiblingId = 0; + parentNode.ChildCount--; + + return true; } public bool TryGetParent(uint childId, out Actor parent) { ref var component = ref TryGetComponentRef(childId); - if (!Unsafe.IsNullRef(ref component)) + if (!Unsafe.IsNullRef(ref component) && component.ParentId != 0) { - var parentId = component.Node.Parent?.Id; - if (parentId != null) - { - parent = new Actor(this, parentId.Value); - return true; - } + parent = new Actor(this, component.ParentId); + return true; } parent = Actor.Empty; return false; } + + private void OnNodeRemoving(ref ActorNodeComponent node) + { + var parentId = node.ParentId; + if (parentId != 0) + { + ref var parentNode = ref TryGetComponentRef(parentId); + if (!Unsafe.IsNullRef(ref parentNode)) + { + var prevId = node.PrevSiblingId; + var nextId = node.NextSiblingId; + + if (prevId != 0) + { + ref var prevNode = ref TryGetComponentRef(prevId); + if (!Unsafe.IsNullRef(ref prevNode)) prevNode.NextSiblingId = nextId; + } + else + { + parentNode.FirstChildId = nextId; + } + + if (nextId != 0) + { + ref var nextNode = ref TryGetComponentRef(nextId); + if (!Unsafe.IsNullRef(ref nextNode)) nextNode.PrevSiblingId = prevId; + } + + parentNode.ChildCount--; + } + } + + // 2. У всех детей обнуляем ссылки (теперь они сироты без соседей) + var currentChildId = node.FirstChildId; + while (currentChildId != 0) + { + ref var childNode = ref TryGetComponentRef(currentChildId); + if (Unsafe.IsNullRef(ref childNode)) break; + + var nextSiblingId = childNode.NextSiblingId; + + childNode.ParentId = 0; + childNode.PrevSiblingId = 0; + childNode.NextSiblingId = 0; + + currentChildId = nextSiblingId; + } + } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.ChildrenEnumerator.cs b/src/Hexecs/Actors/ActorContext.ChildrenEnumerator.cs index 7ec129d..114832e 100644 --- a/src/Hexecs/Actors/ActorContext.ChildrenEnumerator.cs +++ b/src/Hexecs/Actors/ActorContext.ChildrenEnumerator.cs @@ -15,55 +15,57 @@ public static ChildrenEnumerator Empty public readonly Actor Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(_context, _children[_index].Id); + get => new(_context, _currentId); } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _children.Length; + get => (int)_count; } - private int _index; - private readonly Span _children; private readonly ActorContext _context; + private uint _currentId; + private uint _nextId; + private readonly uint _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ChildrenEnumerator() + internal ChildrenEnumerator(ActorContext context, uint firstChildId, uint count) { - _index = -1; - _children = Span.Empty; - _context = null!; + _context = context; + _currentId = 0; + _nextId = firstChildId; + _count = count; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ChildrenEnumerator(ActorContext context, Span children) + public bool MoveNext() { - _index = -1; - _children = children; - _context = context; - } + if (_nextId == 0) return false; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly int Count() => _children.Length; + _currentId = _nextId; + ref var node = ref _context.GetComponent(_currentId); + _nextId = node.NextSiblingId; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _children.Length; + return true; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ChildrenEnumerator GetEnumerator() => this; public Actor[] ToArray() { - var count = 0; - var children = ArrayUtils.Create(_children.Length); + if (_count == 0) return []; + + var result = new Actor[_count]; + var index = 0; + foreach (var actor in this) { - children[count++] = actor; + result[index++] = actor; } - _index = 0; // reset enumerator - return children; + return result; } } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorContext.Components.cs b/src/Hexecs/Actors/ActorContext.Components.cs index f893ab8..d2b0017 100644 --- a/src/Hexecs/Actors/ActorContext.Components.cs +++ b/src/Hexecs/Actors/ActorContext.Components.cs @@ -85,14 +85,23 @@ public ref T GetComponent(uint actorId) where T : struct, IActorComponent return ref pool.Get(actorId); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComponentsAccess GetComponents() where T: struct, IActorComponent + { + var pool = GetComponentPool(); + return pool != null + ? pool.GetComponentAccess() + : new ComponentsAccess(); + } + /// /// Получает компонент указанного типа для заданного актёра или добавляет его, используя фабричный метод, если компонент отсутствует. /// /// Тип компонента. Должен быть структурой и реализовывать . /// Идентификатор актёра. - /// Фабричный метод для создания компонента, если он отсутствует. + /// Фабричный метод для создания компонента /// Ссылка на существующий или вновь созданный компонент. - public ref T GetOrAddComponent(uint actorId, Func factory) + public ref T GetOrAddComponent(uint actorId, Func? factory = null) where T : struct, IActorComponent { var pool = GetOrCreateComponentPool(); diff --git a/src/Hexecs/Actors/ActorContext.Dictionary.cs b/src/Hexecs/Actors/ActorContext.Dictionary.cs index d5ebc85..b187cf0 100644 --- a/src/Hexecs/Actors/ActorContext.Dictionary.cs +++ b/src/Hexecs/Actors/ActorContext.Dictionary.cs @@ -1,4 +1,5 @@ -using Hexecs.Actors.Relations; +using Hexecs.Actors.Nodes; +using Hexecs.Actors.Relations; namespace Hexecs.Actors; @@ -35,6 +36,12 @@ private ref Entry AddEntry(uint actorId) private void ClearEntry(uint actorId, ref Entry entry) { + ref var node = ref TryGetComponentRef(actorId); + if (!Unsafe.IsNullRef(ref node)) + { + OnNodeRemoving(ref node); + } + ref var relationsComponent = ref TryGetComponentRef(actorId); if (!Unsafe.IsNullRef(ref relationsComponent)) { diff --git a/src/Hexecs/Actors/ActorDictionary.cs b/src/Hexecs/Actors/ActorDictionary.cs index 9cfc59f..99643b8 100644 --- a/src/Hexecs/Actors/ActorDictionary.cs +++ b/src/Hexecs/Actors/ActorDictionary.cs @@ -15,7 +15,7 @@ public int Length get => _actors.Count; } - private readonly Dictionary _actors; + private readonly Dictionary _actors; private readonly Func _keyExtractor; private readonly ActorComponentPool _pool; @@ -30,8 +30,8 @@ public ActorDictionary( Context = context; _actors = comparer == null - ? new Dictionary(capacity) - : new Dictionary(capacity, comparer); + ? new Dictionary(capacity) + : new Dictionary(capacity, comparer); _keyExtractor = keyExtractor; _pool = context.GetOrCreateComponentPool(); @@ -45,11 +45,7 @@ public ActorDictionary( public void Add(Actor actor) { ObjectDisposedException.ThrowIf(_disposed, this); - - var actorId = actor.Id; - var index = _pool.TryGetIndex(actorId); - - _actors.Add(_keyExtractor(actor.Component1), new Entry(actorId, index)); + _actors.Add(_keyExtractor(actor.Component1), actor.Id); } public bool ContainsKey(TKey key) => !_disposed && _actors.ContainsKey(key); @@ -70,22 +66,22 @@ public void Dispose() public void Fill(bool clearBefore = true) { ObjectDisposedException.ThrowIf(_disposed, this); - + if (clearBefore) OnCleared(); var poolEnumerator = _pool.GetEnumerator(); while (poolEnumerator.MoveNext()) { var actor = poolEnumerator.Current; - _actors.Add(_keyExtractor(actor.Component1), new Entry(actor.Id, poolEnumerator.Index)); + _actors.Add(_keyExtractor(actor.Component1), actor.Id); } } - + public ActorRef GetActorRef(TKey key) { if (!_disposed && _actors.TryGetValue(key, out var entry)) { - return new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); + return new ActorRef(Context, entry, ref _pool.Get(entry)); } ActorError.KeyNotFound(); @@ -96,7 +92,7 @@ public bool TryGetActorRef(TKey key, out ActorRef actor) { if (!_disposed && _actors.TryGetValue(key, out var entry)) { - actor = new ActorRef(Context, entry.Id, ref _pool.GetByIndex(entry.Index)); + actor = new ActorRef(Context, entry, ref _pool.Get(entry)); return true; } @@ -122,14 +118,7 @@ private void OnComponentRemoving(uint actorId, ref T1 component) private void OnComponentUpdating(uint actorId, ref T1 exists, in T1 expected) { - _actors.Remove(_keyExtractor(exists), out var entry); - _actors.Add(_keyExtractor(expected), new Entry(actorId, entry.Index)); - } - - [method: MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly struct Entry(uint id, int index) - { - public readonly uint Id = id; - public readonly int Index = index; + _actors.Remove(_keyExtractor(exists), out _); + _actors.Add(_keyExtractor(expected), actorId); } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorError.cs b/src/Hexecs/Actors/ActorError.cs index 49d1fc8..c339f7e 100644 --- a/src/Hexecs/Actors/ActorError.cs +++ b/src/Hexecs/Actors/ActorError.cs @@ -106,7 +106,8 @@ public static void ComponentExists(uint actorId) /// Тип компонента /// Идентификатор актёра [DoesNotReturn] - public static void ComponentNotFound(uint actorId) + [MethodImpl(MethodImplOptions.NoInlining)] + public static ref T ComponentNotFound(uint actorId) { throw new Exception($"Actor {actorId} don't have component {TypeOf.GetTypeName()}"); } diff --git a/src/Hexecs/Actors/ActorFilter1.Dictionary.cs b/src/Hexecs/Actors/ActorFilter1.Dictionary.cs index baf0e22..0bf9a83 100644 --- a/src/Hexecs/Actors/ActorFilter1.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter1.Dictionary.cs @@ -3,39 +3,37 @@ namespace Hexecs.Actors; [SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { - private const int PageBits = 12; - private const int PageSize = 1 << PageBits; // 4096 - private const int PageMask = PageSize - 1; - + public ReadOnlySpan Keys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dense.AsSpan(0, _count); + } + public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } - private uint[]?[] _sparsePages; + private uint[] _sparse; private uint[] _dense; - private int[] _values; private int _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddEntry(uint actorId, int index1) + private void AddEntry(uint actorId) { - if (TryAddEntry(actorId, index1)) return; - ActorError.AlreadyExists(actorId); // выбрасывает ошибку + if (TryAddEntry(actorId)) return; + ActorError.AlreadyExists(actorId); } private void ClearEntries() { var dense = _dense; - var sparsePages = _sparsePages; + var sparse = _sparse; - // Очищаем только занятые ячейки в разреженных страницах за O(Count) for (var i = 0; i < _count; i++) { - var key = dense[i]; - var pageIndex = (int)(key >> PageBits); - sparsePages[pageIndex]![key & PageMask] = 0; + sparse[dense[i]] = 0; } _count = 0; @@ -44,130 +42,91 @@ private void ClearEntries() [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ContainsEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var page = pages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[actorId & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; - } + var denseIndexPlusOne = sparse[actorId]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; } return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref int GetEntryRef(uint actorId) + private void EnsureCapacity(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - if ((uint)pageIndex < (uint)_sparsePages.Length) + if (_count >= _dense.Length) { - var page = _sparsePages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[actorId & PageMask]; - if (denseIndexPlusOne != 0) - { - var index = (int)denseIndexPlusOne - 1; - if (_dense[index] == actorId) - { - return ref _values[index]; - } - } - } + var newSize = _dense.Length * 2; + Array.Resize(ref _dense, newSize); } - return ref Unsafe.NullRef(); + if (actorId >= (uint)_sparse.Length) + { + var newSize = Math.Max((uint)_sparse.Length * 2, actorId + 1); + Array.Resize(ref _sparse, (int)newSize); + } } private bool RemoveEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - // Используем вашу оригинальную "плоскую" логику из коммита - if ((uint)pageIndex >= (uint)_sparsePages.Length) return false; - - var page = _sparsePages[pageIndex]; - if (page == null) return false; - - var offset = (int)(actorId & PageMask); - var denseIndexPlusOne = page[offset]; - if (denseIndexPlusOne == 0) return false; - - var denseIndex = (int)denseIndexPlusOne - 1; - if (_dense[denseIndex] != actorId) return false; - - var lastIndex = _count - 1; - if (denseIndex != lastIndex) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var lastKey = _dense[lastIndex]; - _dense[denseIndex] = lastKey; - _values[denseIndex] = _values[lastIndex]; - - var lastKeyPageIndex = (int)(lastKey >> PageBits); - _sparsePages[lastKeyPageIndex]![lastKey & PageMask] = (uint)denseIndex + 1; - } + var slot = sparse[actorId]; + if (slot != 0) + { + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == actorId) + { + var lastIndex = _count - 1; + if (denseIndex != lastIndex) + { + var lastKey = _dense[lastIndex]; + _dense[denseIndex] = lastKey; + _sparse[lastKey] = slot; + } - page[offset] = 0; - _count = lastIndex; + _sparse[actorId] = 0; + _count = lastIndex; - Removed?.Invoke(actorId); + Removed?.Invoke(actorId); + return true; + } + } + } - return true; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryAddEntry(uint actorId, int index1) + private bool TryAddEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - var pages = _sparsePages; - - // Максимально компактная проверка на готовность страницы и места - if ((uint)pageIndex < (uint)pages.Length) + if (actorId < (uint)_sparse.Length && (uint)_count < (uint)_dense.Length) { - var page = pages[pageIndex]; - if (page != null && (uint)_count < (uint)_dense.Length) + ref var slot = ref _sparse[actorId]; + if (slot == 0) { - ref var slot = ref page[actorId & PageMask]; - if (slot == 0) // Чистая вставка (самый частый случай в ECS) - { - var idx = (uint)_count; - slot = idx + 1; - _dense[idx] = actorId; - _values[idx] = index1; - _count++; - - Added?.Invoke(actorId); + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = actorId; + _count++; - return true; - } - - // Если не 0, проверяем на дубликат (чуть медленнее) - if (_dense[slot - 1] == actorId) return false; + Added?.Invoke(actorId); + return true; } + + if (_dense[slot - 1] == actorId) return false; } - return TryAddEntrySlow(actorId, index1); + return TryAddEntrySlow(actorId); } [MethodImpl(MethodImplOptions.NoInlining)] - private bool TryAddEntrySlow(uint actorId, int index1) + private bool TryAddEntrySlow(uint actorId) { - EnsureDenseCapacity(); - var pageIndex = (int)(actorId >> PageBits); - EnsurePageArraySize(pageIndex); - - ref var page = ref _sparsePages[pageIndex]; - if (page == null) - { - page = ArrayUtils.Create(PageSize); - Array.Clear(page, 0, page.Length); - } + EnsureCapacity(actorId); - ref var denseIndexPlusOne = ref page[actorId & PageMask]; + ref var denseIndexPlusOne = ref _sparse[actorId]; if (denseIndexPlusOne != 0) { if (_dense[denseIndexPlusOne - 1] == actorId) return false; @@ -176,30 +135,9 @@ private bool TryAddEntrySlow(uint actorId, int index1) var denseIndex = (uint)_count; denseIndexPlusOne = denseIndex + 1; _dense[denseIndex] = actorId; - _values[denseIndex] = index1; _count++; Added?.Invoke(actorId); - return true; } - - private void EnsureDenseCapacity() - { - if (_count >= _dense.Length) - { - var newSize = _dense.Length * 2; - Array.Resize(ref _dense, newSize); - Array.Resize(ref _values, newSize); - } - } - - private void EnsurePageArraySize(int pageIndex) - { - if (pageIndex >= _sparsePages.Length) - { - var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); - Array.Resize(ref _sparsePages, newSize); - } - } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter1.Enumerator.cs b/src/Hexecs/Actors/ActorFilter1.Enumerator.cs index 00b9dc3..7c264c1 100644 --- a/src/Hexecs/Actors/ActorFilter1.Enumerator.cs +++ b/src/Hexecs/Actors/ActorFilter1.Enumerator.cs @@ -1,5 +1,3 @@ -using Hexecs.Actors.Components; - namespace Hexecs.Actors; public sealed partial class ActorFilter @@ -23,31 +21,29 @@ public ref struct Enumerator { private readonly ActorContext _context; private readonly ActorFilter _filter; - private readonly ActorComponentPool _pool1; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; + private readonly ComponentsAccess _pool1; + private readonly ReadOnlySpan _ids; private int _index; - + public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry)); + id, + ref _pool1[id]); } } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _keys.Length; + get => _ids.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -55,11 +51,10 @@ internal Enumerator(ActorFilter filter) { _context = filter.Context; _filter = filter; - _pool1 = filter._pool1; + _pool1 = filter._pool1.GetComponentAccess(); var count = filter._count; - _keys = filter._dense.AsSpan(0, count); - _entries = filter._values.AsSpan(0, count); + _ids = filter._dense.AsSpan(0, count); _index = -1; } @@ -68,7 +63,7 @@ internal Enumerator(ActorFilter filter) public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Enumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter1.Operation.cs b/src/Hexecs/Actors/ActorFilter1.Operation.cs index 434d011..c764c1d 100644 --- a/src/Hexecs/Actors/ActorFilter1.Operation.cs +++ b/src/Hexecs/Actors/ActorFilter1.Operation.cs @@ -2,46 +2,33 @@ namespace Hexecs.Actors; public sealed partial class ActorFilter { + [StructLayout(LayoutKind.Sequential)] private readonly struct Operation { - private const int ClearFlag = -1; - private const int RemoveFlag = -2; + private const byte TypeAdd = 1; + private const byte TypeRemove = 2; + private const byte TypeClear = 3; public readonly uint Id; - public readonly int Index1; + public readonly byte Type; - public bool IsAdd - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 >= 0; - } - - public bool IsClear - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == ClearFlag; - } - - public bool IsRemove - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == RemoveFlag; - } + public bool IsAdd => Type == TypeAdd; + public bool IsClear => Type == TypeClear; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Add(uint id, int index1) => new(id, index1); + public static Operation Add(uint id) => new(id, TypeAdd); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Clear() => new(0, ClearFlag); + public static Operation Remove(uint id) => new(id, TypeRemove); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Remove(uint id) => new(id, RemoveFlag); + public static Operation Clear() => new(0, TypeClear); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Operation(uint id, int index1) + private Operation(uint id, byte type) { Id = id; - Index1 = index1; + Type = type; } } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter1.SkipTakeEnumerator.cs b/src/Hexecs/Actors/ActorFilter1.SkipTakeEnumerator.cs index b0f6740..e0b625a 100644 --- a/src/Hexecs/Actors/ActorFilter1.SkipTakeEnumerator.cs +++ b/src/Hexecs/Actors/ActorFilter1.SkipTakeEnumerator.cs @@ -24,23 +24,21 @@ public ref struct SkipTakeEnumerator private readonly ActorContext _context; private readonly ActorFilter _filter; private readonly ActorComponentPool _pool1; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; - + + private readonly ReadOnlySpan _ids; private int _index; public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry)); + id, + ref _pool1.Get(id)); } } @@ -61,9 +59,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = int.Max var actualSkip = Math.Min(skip, count); var actualTake = Math.Min(take, count - actualSkip); - _keys = filter._dense.AsSpan(actualSkip, actualTake); - _entries = filter._values.AsSpan(actualSkip, actualTake); - + _ids = filter._dense.AsSpan(actualSkip, actualTake); _index = -1; } @@ -71,7 +67,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = int.Max public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly SkipTakeEnumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter1.cs b/src/Hexecs/Actors/ActorFilter1.cs index dd2e5ab..c6cf506 100644 --- a/src/Hexecs/Actors/ActorFilter1.cs +++ b/src/Hexecs/Actors/ActorFilter1.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Hexecs.Actors.Components; using Hexecs.Actors.Delegates; @@ -15,7 +16,7 @@ public sealed partial class ActorFilter : IActorFilter public readonly ActorConstraint? Constraint; public readonly ActorContext Context; - private readonly Queue _postponedUpdates; + private readonly ConcurrentQueue _postponedUpdates; private int _postponedReadersCount; #if NET9_0_OR_GREATER private readonly Lock _postponedSyncLock = new(); @@ -32,11 +33,12 @@ internal ActorFilter(ActorContext context, ActorConstraint? constraint = null, i Constraint = constraint; Context = context; - _sparsePages = new uint[16][]; + capacity = Math.Max(capacity, context.Length); + + _sparse = new uint[capacity]; _dense = new uint[capacity]; - _values = new int[capacity]; - _postponedUpdates = new Queue(capacity); + _postponedUpdates = new ConcurrentQueue(); _postponedReadersCount = 0; if (constraint != null) @@ -99,15 +101,15 @@ public void Dispose() Context.Cleared -= OnCleared; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ActorRef GetRef(uint actorId) { - ref var entry = ref GetEntryRef(actorId); - if (Unsafe.IsNullRef(ref entry)) ActorError.NotFound(actorId); + if (!ContainsEntry(actorId)) ActorError.NotFound(actorId); return new ActorRef( Context, actorId, - ref _pool1.GetByIndex(entry)); + ref _pool1.Get(actorId)); } public ActorRef GetRef(ActorPredicate predicate) @@ -156,29 +158,41 @@ public Actor[] ToArray() private void OnAdded(uint actorId) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - Add(actorId, index1); + if (_pool1.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent1(uint actorId, int index1, ref T1 component) - { - Add(actorId, index1); - } + private void OnAddedComponent1(uint actorId, ref T1 component) => Add(actorId); private void OnCleared() { - ClearEntries(); - Cleared?.Invoke(); + if (Volatile.Read(ref _postponedReadersCount) == 0) + { +#if NET9_0_OR_GREATER + using (_postponedSyncLock.EnterScope()) +#else + lock (_postponedSyncLock) +#endif + { + if (_postponedReadersCount == 0) + { + ClearEntries(); + Cleared?.Invoke(); + return; + } + } + } + + _postponedUpdates.Enqueue(Operation.Clear()); } private void OnRemoving(uint actorId) => Remove(actorId); private void OnRemovingComponent1(uint actorId, ref T1 component) => Remove(actorId); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Add(uint actorId, int index1) + private void Add(uint actorId) { if (Constraint != null && !Constraint.Applicable(actorId)) return; @@ -192,13 +206,13 @@ private void Add(uint actorId, int index1) { if (Volatile.Read(ref _postponedReadersCount) == 0) { - AddEntry(actorId, index1); + AddEntry(actorId); } } } else { - _postponedUpdates.Enqueue(Operation.Add(actorId, index1)); + _postponedUpdates.Enqueue(Operation.Add(actorId)); } } @@ -225,7 +239,7 @@ private void ProcessPostponedUpdates() } else if (operation.IsAdd) { - AddEntry(operation.Id, operation.Index1); + AddEntry(operation.Id); } else { @@ -238,7 +252,6 @@ private void ProcessPostponedUpdates() if (isClear) Cleared?.Invoke(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Remove(uint actorId) { if (Volatile.Read(ref _postponedReadersCount) == 0) diff --git a/src/Hexecs/Actors/ActorFilter2.Dictionary.cs b/src/Hexecs/Actors/ActorFilter2.Dictionary.cs index a6ff455..a3486ab 100644 --- a/src/Hexecs/Actors/ActorFilter2.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter2.Dictionary.cs @@ -3,171 +3,130 @@ [SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { - private const int PageBits = 12; - private const int PageSize = 1 << PageBits; // 4096 - private const int PageMask = PageSize - 1; - + public ReadOnlySpan Keys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dense.AsSpan(0, _count); + } + public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } - private uint[]?[] _sparsePages; + private uint[] _sparse; private uint[] _dense; - private Entry[] _values; private int _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddEntry(uint actorId, int index1, int index2) + private void AddEntry(uint actorId) { - if (TryAddEntry(actorId, index1, index2)) return; - ActorError.AlreadyExists(actorId); // выбрасывает ошибку + if (TryAddEntry(actorId)) return; + ActorError.AlreadyExists(actorId); } private void ClearEntries() { var dense = _dense; - var sparsePages = _sparsePages; + var sparse = _sparse; - // Очищаем только занятые ячейки в разреженных страницах за O(Count) for (var i = 0; i < _count; i++) { - var key = dense[i]; - var pageIndex = (int)(key >> PageBits); - sparsePages[pageIndex]![key & PageMask] = 0; + sparse[dense[i]] = 0; } _count = 0; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ContainsEntry(uint key) + private bool ContainsEntry(uint actorId) { - var pageIndex = (int)(key >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var page = pages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[key & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == key; - } + var denseIndexPlusOne = sparse[actorId]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; } return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref Entry GetEntryRef(uint actorId) + private void EnsureCapacity(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - if ((uint)pageIndex < (uint)_sparsePages.Length) + if (_count >= _dense.Length) { - var page = _sparsePages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[actorId & PageMask]; - if (denseIndexPlusOne != 0) - { - var index = (int)denseIndexPlusOne - 1; - if (_dense[index] == actorId) - { - return ref _values[index]; - } - } - } + var newSize = _dense.Length * 2; + Array.Resize(ref _dense, newSize); } - return ref Unsafe.NullRef(); + if (actorId >= (uint)_sparse.Length) + { + var newSize = Math.Max((uint)_sparse.Length * 2, actorId + 1); + Array.Resize(ref _sparse, (int)newSize); + } } private bool RemoveEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - // Используем вашу оригинальную "плоскую" логику из коммита - if ((uint)pageIndex >= (uint)_sparsePages.Length) return false; - - var page = _sparsePages[pageIndex]; - if (page == null) return false; - - var offset = (int)(actorId & PageMask); - var denseIndexPlusOne = page[offset]; - if (denseIndexPlusOne == 0) return false; - - var denseIndex = (int)denseIndexPlusOne - 1; - if (_dense[denseIndex] != actorId) return false; - - var lastIndex = _count - 1; - if (denseIndex != lastIndex) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var lastKey = _dense[lastIndex]; - _dense[denseIndex] = lastKey; - _values[denseIndex] = _values[lastIndex]; - - var lastKeyPageIndex = (int)(lastKey >> PageBits); - _sparsePages[lastKeyPageIndex]![lastKey & PageMask] = (uint)denseIndex + 1; - } + var slot = sparse[actorId]; + if (slot != 0) + { + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == actorId) + { + var lastIndex = _count - 1; + if (denseIndex != lastIndex) + { + var lastKey = _dense[lastIndex]; + _dense[denseIndex] = lastKey; + _sparse[lastKey] = slot; + } - page[offset] = 0; - _count = lastIndex; + _sparse[actorId] = 0; + _count = lastIndex; - Removed?.Invoke(actorId); + Removed?.Invoke(actorId); + return true; + } + } + } - return true; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryAddEntry(uint actorId, int index1, int index2) + private bool TryAddEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - var pages = _sparsePages; - - // Максимально компактная проверка на готовность страницы и места - if ((uint)pageIndex < (uint)pages.Length) + if (actorId < (uint)_sparse.Length && (uint)_count < (uint)_dense.Length) { - var page = pages[pageIndex]; - if (page != null && (uint)_count < (uint)_dense.Length) + ref var slot = ref _sparse[actorId]; + if (slot == 0) { - ref var slot = ref page[actorId & PageMask]; - if (slot == 0) // Чистая вставка (самый частый случай в ECS) - { - var idx = (uint)_count; - slot = idx + 1; - _dense[idx] = actorId; - _values[idx] = new Entry(index1, index2); - _count++; - - Added?.Invoke(actorId); - - return true; - } + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = actorId; + _count++; - // Если не 0, проверяем на дубликат (чуть медленнее) - if (_dense[slot - 1] == actorId) return false; + Added?.Invoke(actorId); + return true; } + + if (_dense[slot - 1] == actorId) return false; } - return TryAddEntrySlow(actorId, index1, index2); + return TryAddEntrySlow(actorId); } [MethodImpl(MethodImplOptions.NoInlining)] - private bool TryAddEntrySlow(uint actorId, int index1, int index2) + private bool TryAddEntrySlow(uint actorId) { - EnsureDenseCapacity(); - var pageIndex = (int)(actorId >> PageBits); - EnsurePageArraySize(pageIndex); - - ref var page = ref _sparsePages[pageIndex]; - if (page == null) - { - page = ArrayUtils.Create(PageSize); - Array.Clear(page, 0, page.Length); - } + EnsureCapacity(actorId); - ref var denseIndexPlusOne = ref page[actorId & PageMask]; + ref var denseIndexPlusOne = ref _sparse[actorId]; if (denseIndexPlusOne != 0) { if (_dense[denseIndexPlusOne - 1] == actorId) return false; @@ -176,30 +135,9 @@ private bool TryAddEntrySlow(uint actorId, int index1, int index2) var denseIndex = (uint)_count; denseIndexPlusOne = denseIndex + 1; _dense[denseIndex] = actorId; - _values[denseIndex] = new Entry(index1, index2); _count++; - + Added?.Invoke(actorId); - return true; } - - private void EnsureDenseCapacity() - { - if (_count >= _dense.Length) - { - var newSize = _dense.Length * 2; - Array.Resize(ref _dense, newSize); - Array.Resize(ref _values, newSize); - } - } - - private void EnsurePageArraySize(int pageIndex) - { - if (pageIndex >= _sparsePages.Length) - { - var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); - Array.Resize(ref _sparsePages, newSize); - } - } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter2.Entry.cs b/src/Hexecs/Actors/ActorFilter2.Entry.cs deleted file mode 100644 index 9eaef7e..0000000 --- a/src/Hexecs/Actors/ActorFilter2.Entry.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Hexecs.Actors; - -public sealed partial class ActorFilter -{ - [StructLayout(LayoutKind.Sequential, Pack = 4)] - private readonly struct Entry - { - public readonly int Index1; - public readonly int Index2; - - [method: MethodImpl(MethodImplOptions.AggressiveInlining)] - public Entry(int index1, int index2) - { - Index1 = index1; - Index2 = index2; - } - } -} \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter2.Enumerator.cs b/src/Hexecs/Actors/ActorFilter2.Enumerator.cs index 2d6254f..16a5f24 100644 --- a/src/Hexecs/Actors/ActorFilter2.Enumerator.cs +++ b/src/Hexecs/Actors/ActorFilter2.Enumerator.cs @@ -1,6 +1,4 @@ -using Hexecs.Actors.Components; - -namespace Hexecs.Actors; +namespace Hexecs.Actors; public sealed partial class ActorFilter { @@ -23,33 +21,31 @@ public ref struct Enumerator { private readonly ActorContext _context; private readonly ActorFilter _filter; - private readonly ActorComponentPool _pool1; - private readonly ActorComponentPool _pool2; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; + private readonly ComponentsAccess _pool1; + private readonly ComponentsAccess _pool2; + private readonly ReadOnlySpan _ids; private int _index; - + public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2)); + id, + ref _pool1[id], + ref _pool2[id]); } } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _keys.Length; + get => _ids.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -57,12 +53,11 @@ internal Enumerator(ActorFilter filter) { _context = filter.Context; _filter = filter; - _pool1 = filter._pool1; - _pool2 = filter._pool2; + _pool1 = filter._pool1.GetComponentAccess(); + _pool2 = filter._pool2.GetComponentAccess(); var count = filter._count; - _keys = filter._dense.AsSpan(0, count); - _entries = filter._values.AsSpan(0, count); + _ids = filter._dense.AsSpan(0, count); _index = -1; } @@ -71,7 +66,7 @@ internal Enumerator(ActorFilter filter) public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Enumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter2.Operation.cs b/src/Hexecs/Actors/ActorFilter2.Operation.cs index cf55395..0587649 100644 --- a/src/Hexecs/Actors/ActorFilter2.Operation.cs +++ b/src/Hexecs/Actors/ActorFilter2.Operation.cs @@ -2,52 +2,33 @@ public sealed partial class ActorFilter { - [StructLayout(LayoutKind.Sequential, Pack = 4)] + [StructLayout(LayoutKind.Sequential)] private readonly struct Operation { - private const int ClearFlag = -1; - private const int RemoveFlag = -2; + private const byte TypeAdd = 1; + private const byte TypeRemove = 2; + private const byte TypeClear = 3; public readonly uint Id; - public readonly int Index1; - public readonly int Index2; + public readonly byte Type; - public bool IsAdd - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 >= 0; - } - - public bool IsClear - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == ClearFlag; - } - - public bool IsRemove - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == RemoveFlag; - } + public bool IsAdd => Type == TypeAdd; + public bool IsClear => Type == TypeClear; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Add(uint id, int index1, int index2) - { - return new Operation(id, index1, index2); - } + public static Operation Add(uint id) => new(id, TypeAdd); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Clear() => new(0, ClearFlag, 0); + public static Operation Remove(uint id) => new(id, TypeRemove); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Remove(uint id) => new(id, RemoveFlag, 0); + public static Operation Clear() => new(0, TypeClear); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Operation(uint id, int index1, int index2) + private Operation(uint id, byte type) { Id = id; - Index1 = index1; - Index2 = index2; + Type = type; } } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter2.SkipTakeEnumerator.cs b/src/Hexecs/Actors/ActorFilter2.SkipTakeEnumerator.cs index 83afdd9..31d20ab 100644 --- a/src/Hexecs/Actors/ActorFilter2.SkipTakeEnumerator.cs +++ b/src/Hexecs/Actors/ActorFilter2.SkipTakeEnumerator.cs @@ -25,24 +25,22 @@ public ref struct SkipTakeEnumerator private readonly ActorFilter _filter; private readonly ActorComponentPool _pool1; private readonly ActorComponentPool _pool2; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; - + + private readonly ReadOnlySpan _ids; private int _index; public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2)); + id, + ref _pool1.Get(id), + ref _pool2.Get(id)); } } @@ -64,9 +62,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = int var actualSkip = Math.Min(skip, count); var actualTake = Math.Min(take, count - actualSkip); - _keys = filter._dense.AsSpan(actualSkip, actualTake); - _entries = filter._values.AsSpan(actualSkip, actualTake); - + _ids = filter._dense.AsSpan(actualSkip, actualTake); _index = -1; } @@ -74,7 +70,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = int public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly SkipTakeEnumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter2.cs b/src/Hexecs/Actors/ActorFilter2.cs index 005d58a..833f320 100644 --- a/src/Hexecs/Actors/ActorFilter2.cs +++ b/src/Hexecs/Actors/ActorFilter2.cs @@ -35,9 +35,10 @@ internal ActorFilter(ActorContext context, ActorConstraint? constraint = null, i Constraint = constraint; Context = context; - _sparsePages = new uint[16][]; + capacity = Math.Max(capacity, context.Length); + + _sparse = new uint[capacity]; _dense = new uint[capacity]; - _values = new Entry[capacity]; _postponedUpdates = new ConcurrentQueue(); _postponedReadersCount = 0; @@ -116,14 +117,13 @@ public void Dispose() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ActorRef GetRef(uint actorId) { - ref var entry = ref GetEntryRef(actorId); - if (Unsafe.IsNullRef(ref entry)) ActorError.NotFound(actorId); + if (!ContainsEntry(actorId)) ActorError.NotFound(actorId); return new ActorRef( Context, actorId, - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2)); + ref _pool1.Get(actorId), + ref _pool2.Get(actorId)); } public ActorRef GetRef(ActorPredicate predicate) @@ -172,29 +172,26 @@ public Actor[] ToArray() private void OnAdded(uint actorId) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - var index2 = _pool2.TryGetIndex(actorId); - if (index2 == -1) return; - - Add(actorId, index1, index2); + if (_pool1.Has(actorId) && _pool2.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent1(uint actorId, int index1, ref T1 component) + private void OnAddedComponent1(uint actorId, ref T1 component) { - var index2 = _pool2.TryGetIndex(actorId); - if (index2 == -1) return; - - Add(actorId, index1, index2); + if (_pool2.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent2(uint actorId, int index2, ref T2 component) + private void OnAddedComponent2(uint actorId, ref T2 component) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - Add(actorId, index1, index2); + if (_pool1.Has(actorId)) + { + Add(actorId); + } } private void OnCleared() @@ -225,7 +222,7 @@ private void OnCleared() private void OnRemovingComponent2(uint actorId, ref T2 component) => Remove(actorId); - private void Add(uint actorId, int index1, int index2) + private void Add(uint actorId) { if (Constraint != null && !Constraint.Applicable(actorId)) return; @@ -239,13 +236,13 @@ private void Add(uint actorId, int index1, int index2) { if (Volatile.Read(ref _postponedReadersCount) == 0) { - AddEntry(actorId, index1, index2); + AddEntry(actorId); } } } else { - _postponedUpdates.Enqueue(Operation.Add(actorId, index1, index2)); + _postponedUpdates.Enqueue(Operation.Add(actorId)); } } @@ -272,7 +269,7 @@ private void ProcessPostponedUpdates() } else if (operation.IsAdd) { - AddEntry(operation.Id, operation.Index1, operation.Index2); + AddEntry(operation.Id); } else { diff --git a/src/Hexecs/Actors/ActorFilter3.Dictionary.cs b/src/Hexecs/Actors/ActorFilter3.Dictionary.cs index 049d146..b60149f 100644 --- a/src/Hexecs/Actors/ActorFilter3.Dictionary.cs +++ b/src/Hexecs/Actors/ActorFilter3.Dictionary.cs @@ -3,39 +3,37 @@ namespace Hexecs.Actors; [SuppressMessage("ReSharper", "InvertIf")] public sealed partial class ActorFilter { - private const int PageBits = 12; - private const int PageSize = 1 << PageBits; // 4096 - private const int PageMask = PageSize - 1; - + public ReadOnlySpan Keys + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _dense.AsSpan(0, _count); + } + public int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _count; } - private uint[]?[] _sparsePages; + private uint[] _sparse; private uint[] _dense; - private Entry[] _values; private int _count; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AddEntry(uint actorId, int index1, int index2, int index3) + private void AddEntry(uint actorId) { - if (TryAddEntry(actorId, index1, index2, index3)) return; - ActorError.AlreadyExists(actorId); // выбрасывает ошибку + if (TryAddEntry(actorId)) return; + ActorError.AlreadyExists(actorId); } private void ClearEntries() { var dense = _dense; - var sparsePages = _sparsePages; + var sparse = _sparse; - // Очищаем только занятые ячейки в разреженных страницах за O(Count) for (var i = 0; i < _count; i++) { - var key = dense[i]; - var pageIndex = (int)(key >> PageBits); - sparsePages[pageIndex]![key & PageMask] = 0; + sparse[dense[i]] = 0; } _count = 0; @@ -44,148 +42,91 @@ private void ClearEntries() [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ContainsEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var page = pages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[actorId & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; - } + var denseIndexPlusOne = sparse[actorId]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == actorId; } return false; } - private void EnsureDenseCapacity() + private void EnsureCapacity(uint actorId) { if (_count >= _dense.Length) { var newSize = _dense.Length * 2; Array.Resize(ref _dense, newSize); - Array.Resize(ref _values, newSize); } - } - private void EnsurePageArraySize(int pageIndex) - { - if (pageIndex >= _sparsePages.Length) + if (actorId >= (uint)_sparse.Length) { - var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); - Array.Resize(ref _sparsePages, newSize); + var newSize = Math.Max((uint)_sparse.Length * 2, actorId + 1); + Array.Resize(ref _sparse, (int)newSize); } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ref Entry GetEntryRef(uint actorId) + private bool RemoveEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - if ((uint)pageIndex < (uint)_sparsePages.Length) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var page = _sparsePages[pageIndex]; - if (page != null) + var slot = sparse[actorId]; + if (slot != 0) { - var denseIndexPlusOne = page[actorId & PageMask]; - if (denseIndexPlusOne != 0) + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == actorId) { - var index = (int)denseIndexPlusOne - 1; - if (_dense[index] == actorId) + var lastIndex = _count - 1; + if (denseIndex != lastIndex) { - return ref _values[index]; + var lastKey = _dense[lastIndex]; + _dense[denseIndex] = lastKey; + _sparse[lastKey] = slot; } - } - } - } - - return ref Unsafe.NullRef(); - } - - private bool RemoveEntry(uint actorId) - { - var pageIndex = (int)(actorId >> PageBits); - if ((uint)pageIndex >= (uint)_sparsePages.Length) return false; - - var page = _sparsePages[pageIndex]; - if (page == null) return false; - - var offset = (int)(actorId & PageMask); - var denseIndexPlusOne = page[offset]; - if (denseIndexPlusOne == 0) return false; - var denseIndex = (int)denseIndexPlusOne - 1; - if (_dense[denseIndex] != actorId) return false; + _sparse[actorId] = 0; + _count = lastIndex; - var lastIndex = _count - 1; - if (denseIndex != lastIndex) - { - var lastKey = _dense[lastIndex]; - _dense[denseIndex] = lastKey; - _values[denseIndex] = _values[lastIndex]; - - var lastKeyPageIndex = (int)(lastKey >> PageBits); - _sparsePages[lastKeyPageIndex]![lastKey & PageMask] = (uint)denseIndex + 1; + Removed?.Invoke(actorId); + return true; + } + } } - page[offset] = 0; - _count = lastIndex; - - Removed?.Invoke(actorId); - - return true; + return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryAddEntry(uint actorId, int index1, int index2, int index3) + private bool TryAddEntry(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - var pages = _sparsePages; - - // Максимально компактная проверка на готовность страницы и места - if ((uint)pageIndex < (uint)pages.Length) + if (actorId < (uint)_sparse.Length && (uint)_count < (uint)_dense.Length) { - var page = pages[pageIndex]; - if (page != null && (uint)_count < (uint)_dense.Length) + ref var slot = ref _sparse[actorId]; + if (slot == 0) { - ref var slot = ref page[actorId & PageMask]; - if (slot == 0) // Чистая вставка (самый частый случай в ECS) - { - var idx = (uint)_count; - slot = idx + 1; - _dense[idx] = actorId; - _values[idx] = new Entry(index1, index2, index3); - _count++; - - Added?.Invoke(actorId); - - return true; - } + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = actorId; + _count++; - // Если не 0, проверяем на дубликат (чуть медленнее) - if (_dense[slot - 1] == actorId) return false; + Added?.Invoke(actorId); + return true; } + + if (_dense[slot - 1] == actorId) return false; } - return TryAddEntrySlow(actorId, index1, index2, index3); + return TryAddEntrySlow(actorId); } [MethodImpl(MethodImplOptions.NoInlining)] - private bool TryAddEntrySlow(uint actorId, int index1, int index2, int index3) + private bool TryAddEntrySlow(uint actorId) { - EnsureDenseCapacity(); - var pageIndex = (int)(actorId >> PageBits); - EnsurePageArraySize(pageIndex); + EnsureCapacity(actorId); - ref var page = ref _sparsePages[pageIndex]; - if (page == null) - { - page = ArrayUtils.Create(PageSize); - Array.Clear(page, 0, page.Length); - } - - ref var denseIndexPlusOne = ref page[actorId & PageMask]; + ref var denseIndexPlusOne = ref _sparse[actorId]; if (denseIndexPlusOne != 0) { if (_dense[denseIndexPlusOne - 1] == actorId) return false; @@ -194,11 +135,9 @@ private bool TryAddEntrySlow(uint actorId, int index1, int index2, int index3) var denseIndex = (uint)_count; denseIndexPlusOne = denseIndex + 1; _dense[denseIndex] = actorId; - _values[denseIndex] = new Entry(index1, index2, index3); _count++; Added?.Invoke(actorId); - return true; } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter3.Entry.cs b/src/Hexecs/Actors/ActorFilter3.Entry.cs deleted file mode 100644 index 00c7ca2..0000000 --- a/src/Hexecs/Actors/ActorFilter3.Entry.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Hexecs.Actors; - -public sealed partial class ActorFilter -{ - [StructLayout(LayoutKind.Sequential, Pack = 4)] - private readonly struct Entry - { - public readonly int Index1; - public readonly int Index2; - public readonly int Index3; - - [method: MethodImpl(MethodImplOptions.AggressiveInlining)] - public Entry(int index1, int index2, int index3) - { - Index1 = index1; - Index2 = index2; - Index3 = index3; - } - } -} \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter3.Enumerator.cs b/src/Hexecs/Actors/ActorFilter3.Enumerator.cs index 3caffe2..e6337a2 100644 --- a/src/Hexecs/Actors/ActorFilter3.Enumerator.cs +++ b/src/Hexecs/Actors/ActorFilter3.Enumerator.cs @@ -1,5 +1,3 @@ -using Hexecs.Actors.Components; - namespace Hexecs.Actors; public sealed partial class ActorFilter @@ -23,35 +21,33 @@ public ref struct Enumerator { private readonly ActorContext _context; private readonly ActorFilter _filter; - private readonly ActorComponentPool _pool1; - private readonly ActorComponentPool _pool2; - private readonly ActorComponentPool _pool3; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; + private readonly ComponentsAccess _pool1; + private readonly ComponentsAccess _pool2; + private readonly ComponentsAccess _pool3; + private readonly ReadOnlySpan _ids; private int _index; public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2), - ref _pool3.GetByIndex(entry.Index3)); + id, + ref _pool1[id], + ref _pool2[id], + ref _pool3[id]); } } public readonly int Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _keys.Length; + get => _ids.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -59,13 +55,12 @@ internal Enumerator(ActorFilter filter) { _context = filter.Context; _filter = filter; - _pool1 = filter._pool1; - _pool2 = filter._pool2; - _pool3 = filter._pool3; + _pool1 = filter._pool1.GetComponentAccess(); + _pool2 = filter._pool2.GetComponentAccess(); + _pool3 = filter._pool3.GetComponentAccess(); var count = filter._count; - _keys = filter._dense.AsSpan(0, count); - _entries = filter._values.AsSpan(0, count); + _ids = filter._dense.AsSpan(0, count); _index = -1; } @@ -74,7 +69,7 @@ internal Enumerator(ActorFilter filter) public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly Enumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter3.Operation.cs b/src/Hexecs/Actors/ActorFilter3.Operation.cs index 4e397b1..2a8221c 100644 --- a/src/Hexecs/Actors/ActorFilter3.Operation.cs +++ b/src/Hexecs/Actors/ActorFilter3.Operation.cs @@ -2,51 +2,33 @@ namespace Hexecs.Actors; public sealed partial class ActorFilter { - [StructLayout(LayoutKind.Sequential, Pack = 4)] + [StructLayout(LayoutKind.Sequential)] private readonly struct Operation { - private const int ClearFlag = -1; - private const int RemoveFlag = -2; + private const byte TypeAdd = 1; + private const byte TypeRemove = 2; + private const byte TypeClear = 3; public readonly uint Id; - public readonly int Index1; - public readonly int Index2; - public readonly int Index3; + public readonly byte Type; - public bool IsAdd - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 >= 0; - } - - public bool IsClear - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == ClearFlag; - } - - public bool IsRemove - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => Index1 == RemoveFlag; - } + public bool IsAdd => Type == TypeAdd; + public bool IsClear => Type == TypeClear; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Add(uint id, int index1, int index2, int index3) => new(id, index1, index2, index3); + public static Operation Add(uint id) => new(id, TypeAdd); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Clear() => new(0, ClearFlag, 0, 0); + public static Operation Remove(uint id) => new(id, TypeRemove); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Operation Remove(uint id) => new(id, RemoveFlag, 0, 0); + public static Operation Clear() => new(0, TypeClear); [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Operation(uint id, int index1, int index2, int index3) + private Operation(uint id, byte type) { Id = id; - Index1 = index1; - Index2 = index2; - Index3 = index3; + Type = type; } } } \ No newline at end of file diff --git a/src/Hexecs/Actors/ActorFilter3.SkipTakeEnumerator.cs b/src/Hexecs/Actors/ActorFilter3.SkipTakeEnumerator.cs index 61a91e5..741f0df 100644 --- a/src/Hexecs/Actors/ActorFilter3.SkipTakeEnumerator.cs +++ b/src/Hexecs/Actors/ActorFilter3.SkipTakeEnumerator.cs @@ -26,25 +26,23 @@ public ref struct SkipTakeEnumerator private readonly ActorComponentPool _pool1; private readonly ActorComponentPool _pool2; private readonly ActorComponentPool _pool3; - - private readonly ReadOnlySpan _keys; - private readonly ReadOnlySpan _entries; - + + private readonly ReadOnlySpan _ids; private int _index; public readonly ActorRef Current { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - var index = _index; - ref readonly var entry = ref _entries[index]; + var id = _ids[_index]; return new ActorRef( _context, - _keys[index], - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2), - ref _pool3.GetByIndex(entry.Index3)); + id, + ref _pool1.Get(id), + ref _pool2.Get(id), + ref _pool3.Get(id)); } } @@ -67,9 +65,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = var actualSkip = Math.Min(skip, count); var actualTake = Math.Min(take, count - actualSkip); - _keys = filter._dense.AsSpan(actualSkip, actualTake); - _entries = filter._values.AsSpan(actualSkip, actualTake); - + _ids = filter._dense.AsSpan(actualSkip, actualTake); _index = -1; } @@ -77,7 +73,7 @@ internal SkipTakeEnumerator(ActorFilter filter, int skip, int take = public void Dispose() => _filter.ProcessPostponedUpdates(); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() => ++_index < _keys.Length; + public bool MoveNext() => ++_index < _ids.Length; [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly SkipTakeEnumerator GetEnumerator() => this; diff --git a/src/Hexecs/Actors/ActorFilter3.cs b/src/Hexecs/Actors/ActorFilter3.cs index 386c025..8f35647 100644 --- a/src/Hexecs/Actors/ActorFilter3.cs +++ b/src/Hexecs/Actors/ActorFilter3.cs @@ -37,9 +37,10 @@ internal ActorFilter(ActorContext context, ActorConstraint? constraint = null, i Constraint = constraint; Context = context; - _sparsePages = new uint[16][]; + capacity = Math.Max(capacity, context.Length); + + _sparse = new uint[capacity]; _dense = new uint[capacity]; - _values = new Entry[capacity]; _postponedUpdates = new ConcurrentQueue(); _postponedReadersCount = 0; @@ -129,15 +130,14 @@ public void Dispose() [MethodImpl(MethodImplOptions.AggressiveInlining)] public ActorRef GetRef(uint actorId) { - ref var entry = ref GetEntryRef(actorId); - if (Unsafe.IsNullRef(ref entry)) ActorError.NotFound(actorId); + if (!ContainsEntry(actorId)) ActorError.NotFound(actorId); return new ActorRef( Context, actorId, - ref _pool1.GetByIndex(entry.Index1), - ref _pool2.GetByIndex(entry.Index2), - ref _pool3.GetByIndex(entry.Index3)); + ref _pool1.Get(actorId), + ref _pool2.Get(actorId), + ref _pool3.Get(actorId)); } public ActorRef GetRef(ActorPredicate predicate) @@ -186,49 +186,34 @@ public Actor[] ToArray() private void OnAdded(uint actorId) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - var index2 = _pool2.TryGetIndex(actorId); - if (index2 == -1) return; - - var index3 = _pool3.TryGetIndex(actorId); - if (index3 == -1) return; - - Add(actorId, index1, index2, index3); + if (_pool1.Has(actorId) && _pool2.Has(actorId) && _pool3.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent1(uint actorId, int index1, ref T1 component) + private void OnAddedComponent1(uint actorId, ref T1 component) { - var index2 = _pool2.TryGetIndex(actorId); - if (index2 == -1) return; - - var index3 = _pool3.TryGetIndex(actorId); - if (index3 == -1) return; - - Add(actorId, index1, index2, index3); + if (_pool2.Has(actorId) && _pool3.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent2(uint actorId, int index2, ref T2 component) + private void OnAddedComponent2(uint actorId, ref T2 component) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - var index3 = _pool3.TryGetIndex(actorId); - if (index3 == -1) return; - - Add(actorId, index1, index2, index3); + if (_pool1.Has(actorId) && _pool3.Has(actorId)) + { + Add(actorId); + } } - private void OnAddedComponent3(uint actorId, int index3, ref T3 component) + private void OnAddedComponent3(uint actorId, ref T3 component) { - var index1 = _pool1.TryGetIndex(actorId); - if (index1 == -1) return; - - var index2 = _pool2.TryGetIndex(actorId); - if (index2 == -1) return; - - Add(actorId, index1, index2, index3); + if (_pool1.Has(actorId) && _pool2.Has(actorId)) + { + Add(actorId); + } } private void OnCleared() @@ -261,7 +246,7 @@ private void OnCleared() private void OnRemovingComponent3(uint actorId, ref T3 component) => Remove(actorId); - private void Add(uint actorId, int index1, int index2, int index3) + private void Add(uint actorId) { if (Constraint != null && !Constraint.Applicable(actorId)) return; @@ -275,13 +260,13 @@ private void Add(uint actorId, int index1, int index2, int index3) { if (Volatile.Read(ref _postponedReadersCount) == 0) { - AddEntry(actorId, index1, index2, index3); + AddEntry(actorId); } } } else { - _postponedUpdates.Enqueue(Operation.Add(actorId, index1, index2, index3)); + _postponedUpdates.Enqueue(Operation.Add(actorId)); } } @@ -308,7 +293,7 @@ private void ProcessPostponedUpdates() } else if (operation.IsAdd) { - AddEntry(operation.Id, operation.Index1, operation.Index2, operation.Index3); + AddEntry(operation.Id); } else { diff --git a/src/Hexecs/Actors/ActorMarshal.cs b/src/Hexecs/Actors/ActorMarshal.cs index cf1aa88..356960c 100644 --- a/src/Hexecs/Actors/ActorMarshal.cs +++ b/src/Hexecs/Actors/ActorMarshal.cs @@ -50,37 +50,6 @@ public static ushort GetComponentId(Type componentType) /// Выбрасывается, если по указанному идентификатору не найден тип компонента. public static Type GetComponentType(ushort componentId) => ActorComponentType.GetType(componentId); - /// - /// Получает ссылку на компонент по его индексу в пуле. - /// - /// Тип компонента. - /// Контекст актёра. - /// Индекс компонента в пуле. - /// Ссылка на компонент или нулевая ссылка, если компонент не найден. - /// Выбрасывается, если индекс выходит за границы пула. - public static ref T GetComponentByIndex(ActorContext context, int index) - where T : struct, IActorComponent - { - var pool = context.GetComponentPool(); - if (pool == null) return ref Unsafe.NullRef(); - - return ref pool.GetByIndex(index); - } - - /// - /// Получает индекс компонента в пуле по идентификатору владельца. - /// - /// Тип компонента. - /// Контекст актёра. - /// Идентификатор актёра-владельца. - /// Индекс компонента или -1, если компонент не найден. - public static int GetComponentIndex(ActorContext context, uint ownerId) - where T : struct, IActorComponent - { - var pool = context.GetComponentPool(); - return pool?.TryGetIndex(ownerId) ?? -1; - } - /// /// Получает владельца компонента. /// diff --git a/src/Hexecs/Actors/ActorRef1.cs b/src/Hexecs/Actors/ActorRef1.cs index 774ca5d..f320e2e 100644 --- a/src/Hexecs/Actors/ActorRef1.cs +++ b/src/Hexecs/Actors/ActorRef1.cs @@ -187,6 +187,9 @@ public Actor AsActor() => IsEmpty [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() where T : struct, IActorComponent => Context.HasComponent(Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasChild(in Actor child) => Context.HasChild(Id, child.Id); + /// /// Проверяет, существует ли отношение между текущим актёром и указанным родственным актёром. /// diff --git a/src/Hexecs/Actors/ActorRef2.cs b/src/Hexecs/Actors/ActorRef2.cs index 1b90e2b..3fa1cc8 100644 --- a/src/Hexecs/Actors/ActorRef2.cs +++ b/src/Hexecs/Actors/ActorRef2.cs @@ -100,6 +100,9 @@ public ref T AddRelation(in Actor relative, in T relation) where T : struct [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() where T : struct, IActorComponent => Context.HasComponent(Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasChild(in Actor child) => Context.HasChild(Id, child.Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasRelation(in Actor relative) where T : struct => Context.HasRelation(Id, relative.Id); diff --git a/src/Hexecs/Actors/ActorRef3.cs b/src/Hexecs/Actors/ActorRef3.cs index 7711469..5643ebd 100644 --- a/src/Hexecs/Actors/ActorRef3.cs +++ b/src/Hexecs/Actors/ActorRef3.cs @@ -109,6 +109,9 @@ public ref T AddRelation(in Actor relative, in T relation) where T : struct [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has() where T : struct, IActorComponent => Context.HasComponent(Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasChild(in Actor child) => Context.HasChild(Id, child.Id); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasRelation(in Actor relative) where T : struct => Context.HasRelation(Id, relative.Id); diff --git a/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs b/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs index 7076fdb..0f94df1 100644 --- a/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs +++ b/src/Hexecs/Actors/Components/ActorComponentPool.Dictionary.cs @@ -2,9 +2,10 @@ namespace Hexecs.Actors.Components; internal sealed partial class ActorComponentPool { - private const int PageBits = 12; - private const int PageSize = 1 << PageBits; // 4096 - private const int PageMask = PageSize - 1; + private uint[] _sparse; + private uint[] _dense; + private T[] _values; + private int _count; public int Length { @@ -12,66 +13,50 @@ public int Length get => _count; } - private uint[]?[] _sparsePages; - private uint[] _dense; - private T[] _values; - private int _count; - [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ContainsEntry(uint ownerId) { - var pageIndex = (int)(ownerId >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + var sparse = _sparse; + if (ownerId < (uint)sparse.Length) { - var page = pages[pageIndex]; - if (page != null) - { - var denseIndexPlusOne = page[ownerId & PageMask]; - return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == ownerId; - } + var denseIndexPlusOne = sparse[ownerId]; + return denseIndexPlusOne != 0 && _dense[denseIndexPlusOne - 1] == ownerId; } return false; } - private void EnsureDenseCapacity() + private void EnsureCapacity(uint ownerId) { + // Проверка емкости плотных массивов (количество элементов) if (_count >= _dense.Length) { var newSize = _dense.Length * 2; Array.Resize(ref _dense, newSize); Array.Resize(ref _values, newSize); } - } - private void EnsurePageArraySize(int pageIndex) - { - if (pageIndex >= _sparsePages.Length) + // Проверка емкости разреженного массива (максимальный ID) + if (ownerId >= (uint)_sparse.Length) { - var newSize = Math.Max(_sparsePages.Length * 2, pageIndex + 1); - Array.Resize(ref _sparsePages, newSize); + var newSize = Math.Max((uint)_sparse.Length * 2, ownerId + 1); + Array.Resize(ref _sparse, (int)newSize); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref T GetEntryRef(uint actorId) { - var pageIndex = (int)(actorId >> PageBits); - if ((uint)pageIndex < (uint)_sparsePages.Length) + var sparse = _sparse; + if (actorId < (uint)sparse.Length) { - var page = _sparsePages[pageIndex]; - if (page != null) + var denseIndexPlusOne = sparse[actorId]; + if (denseIndexPlusOne != 0) { - var denseIndexPlusOne = page[actorId & PageMask]; - if (denseIndexPlusOne != 0) + var index = (int)denseIndexPlusOne - 1; + if (_dense[index] == actorId) { - var index = (int)denseIndexPlusOne - 1; - if (_dense[index] == actorId) - { - return ref _values[index]; - } + return ref _values[index]; } } } @@ -81,43 +66,36 @@ private ref T GetEntryRef(uint actorId) private bool RemoveEntry(uint ownerId, out T value) { - var pageIndex = (int)(ownerId >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + var sparse = _sparse; + if (ownerId < (uint)sparse.Length) { - var page = pages[pageIndex]; - if (page != null) + var slot = sparse[ownerId]; + if (slot != 0) { - var offset = (int)(ownerId & PageMask); - var slot = page[offset]; - if (slot != 0) + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == ownerId) { - var denseIndex = (int)slot - 1; - if (_dense[denseIndex] == ownerId) - { - ref var componentRef = ref _values[denseIndex]; - value = componentRef; - - Removing?.Invoke(ownerId); - ComponentRemoving?.Invoke(ownerId, ref componentRef); - _disposeHandler?.Invoke(ref componentRef); + ref var componentRef = ref _values[denseIndex]; + value = componentRef; - var lastIndex = _count - 1; - if (denseIndex != lastIndex) - { - var lastKey = _dense[lastIndex]; - _dense[denseIndex] = lastKey; - _values[denseIndex] = _values[lastIndex]; + Removing?.Invoke(ownerId); + ComponentRemoving?.Invoke(ownerId, ref componentRef); + _disposeHandler?.Invoke(ref componentRef); - var lastKeyPageIndex = (int)(lastKey >> PageBits); - pages[lastKeyPageIndex]![lastKey & PageMask] = slot; - } + var lastIndex = _count - 1; + if (denseIndex != lastIndex) + { + var lastKey = _dense[lastIndex]; + _dense[denseIndex] = lastKey; + _values[denseIndex] = _values[lastIndex]; - page[offset] = 0; - _count = lastIndex; - return true; + // Обновляем указатель в sparse для переехавшего элемента + _sparse[lastKey] = slot; } + + _sparse[ownerId] = 0; + _count = lastIndex; + return true; } } } @@ -129,33 +107,23 @@ private bool RemoveEntry(uint ownerId, out T value) [MethodImpl(MethodImplOptions.AggressiveInlining)] private AddResult TryAddEntry(uint ownerId) { - var pageIndex = (int)(ownerId >> PageBits); - var pages = _sparsePages; - - // Максимально компактная проверка на готовность страницы и места - if ((uint)pageIndex < (uint)pages.Length) + // Fast Path: если ID влезает в массив и есть место в dense + if (ownerId < (uint)_sparse.Length && (uint)_count < (uint)_dense.Length) { - var page = pages[pageIndex]; - if (page != null && (uint)_count < (uint)_dense.Length) + ref var slot = ref _sparse[ownerId]; + if (slot == 0) { - ref var slot = ref page[ownerId & PageMask]; - if (slot == 0) // Чистая вставка (самый частый случай в ECS) - { - var idx = (uint)_count; - slot = idx + 1; - _dense[idx] = ownerId; - - ref var internalRef = ref _values[idx]; - var result = AddResult.Success(_count, ref internalRef); - - _count++; - - return result; - } - - // Если не 0, проверяем на дубликат (чуть медленнее) - if (_dense[slot - 1] == ownerId) return AddResult.Failure(); + var idx = (uint)_count; + slot = idx + 1; + _dense[idx] = ownerId; + + ref var internalRef = ref _values[idx]; + var result = AddResult.Success(_count, ref internalRef); + _count++; + return result; } + + if (_dense[slot - 1] == ownerId) return AddResult.Failure(); } return TryAddEntrySlow(ownerId); @@ -164,18 +132,9 @@ private AddResult TryAddEntry(uint ownerId) [MethodImpl(MethodImplOptions.NoInlining)] private AddResult TryAddEntrySlow(uint ownerId) { - EnsureDenseCapacity(); - var pageIndex = (int)(ownerId >> PageBits); - EnsurePageArraySize(pageIndex); - - ref var page = ref _sparsePages[pageIndex]; - if (page == null) - { - page = ArrayUtils.Create(PageSize); - Array.Clear(page, 0, page.Length); - } + EnsureCapacity(ownerId); - ref var denseIndexPlusOne = ref page[ownerId & PageMask]; + ref var denseIndexPlusOne = ref _sparse[ownerId]; if (denseIndexPlusOne != 0) { if (_dense[denseIndexPlusOne - 1] == ownerId) return AddResult.Failure(); @@ -187,75 +146,38 @@ private AddResult TryAddEntrySlow(uint ownerId) ref var internalRef = ref _values[denseIndex]; var result = AddResult.Success(_count, ref internalRef); - _count++; return result; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int TryGetEntryIndex(uint ownerId) + private AddResult UpsertEntry(uint ownerId, out bool exists) { - var pageIndex = (int)(ownerId >> PageBits); - var pages = _sparsePages; - - if ((uint)pageIndex < (uint)pages.Length) + if (ownerId < (uint)_sparse.Length) { - var page = pages[pageIndex]; - if (page != null) + ref var slot = ref _sparse[ownerId]; + if (slot != 0) { - var slot = page[ownerId & PageMask]; - if (slot != 0) + var denseIndex = (int)slot - 1; + if (_dense[denseIndex] == ownerId) { - var denseIndex = (int)slot - 1; - if (_dense[denseIndex] == ownerId) - { - return denseIndex; - } + exists = true; + return AddResult.Success(denseIndex, ref _values[denseIndex]); } } - } - return -1; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private AddResult UpsertEntry(uint ownerId, out bool exists) - { - var pageIndex = (int)(ownerId >> PageBits); - var pages = _sparsePages; - - // 1. Пытаемся найти существующий (Fast Path) - if ((uint)pageIndex < (uint)pages.Length) - { - var page = pages[pageIndex]; - if (page != null) + if ((uint)_count < (uint)_dense.Length) { - ref var slot = ref page[ownerId & PageMask]; - if (slot != 0) - { - var denseIndex = (int)slot - 1; - if (_dense[denseIndex] == ownerId) - { - exists = true; - return AddResult.Success(denseIndex, ref _values[denseIndex]); - } - } - - // 2. Если страница есть, но ключа нет, и есть место - добавляем сразу (Fast Add) - if ((uint)_count < (uint)_dense.Length) - { - var idx = _count; - slot = (uint)idx + 1; - _dense[idx] = ownerId; - _count = idx + 1; - exists = false; - return AddResult.Success(idx, ref _values[idx]); - } + var idx = _count; + slot = (uint)idx + 1; + _dense[idx] = ownerId; + _count = idx + 1; + exists = false; + return AddResult.Success(idx, ref _values[idx]); } } - // 3. Если всё сложно (нужен ресайз или новая страница) - идем в Slow Path exists = false; return TryAddEntrySlow(ownerId); } diff --git a/src/Hexecs/Actors/Components/ActorComponentPool.cs b/src/Hexecs/Actors/Components/ActorComponentPool.cs index db7bb50..aa5d52d 100644 --- a/src/Hexecs/Actors/Components/ActorComponentPool.cs +++ b/src/Hexecs/Actors/Components/ActorComponentPool.cs @@ -38,9 +38,9 @@ public ActorComponentPool(ActorContext context, ActorComponentConfiguration c { Context = context; - var capacity = HashHelper.GetPrime(configuration.Capacity ?? 16); + var capacity = HashHelper.GetPrime(configuration.Capacity ?? Math.Max(context.Length, 16)); - _sparsePages = new uint[16][]; + _sparse = new uint[capacity]; _dense = new uint[capacity]; _values = new T[capacity]; @@ -58,7 +58,7 @@ public ref T Add(uint ownerId, in T component) componentRef = component; Added?.Invoke(ownerId); - ComponentAdded?.Invoke(ownerId, result.Index, ref componentRef); + ComponentAdded?.Invoke(ownerId, ref componentRef); return ref componentRef; } @@ -70,7 +70,7 @@ public ref T Add(uint ownerId, in T component) public void Clear() { var dense = _dense; - var sparsePages = _sparsePages; + var sparse = _sparse; if (_disposeHandler != null) { @@ -80,11 +80,11 @@ public void Clear() } } + // Очищаем только те индексы в sparse, которые реально используются for (var i = 0; i < _count; i++) { var key = dense[i]; - var pageIndex = (int)(key >> PageBits); - sparsePages[pageIndex]![key & PageMask] = 0; + sparse[key] = 0; } _count = 0; @@ -130,13 +130,21 @@ public ActorRef First(ActorPredicate predicate) return ActorRef.Empty; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T Get(uint ownerId) { ref var entry = ref GetEntryRef(ownerId); - if (Unsafe.IsNullRef(ref entry)) ActorError.ComponentNotFound(ownerId); - return ref entry; + if (!Unsafe.IsNullRef(ref entry)) + { + return ref entry; + } + + return ref ActorError.ComponentNotFound(ownerId); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComponentsAccess GetComponentAccess() => new(_sparse, _values); + public ref T GetOrCreate(uint ownerId, out bool added, Func? factory = null) { var result = UpsertEntry(ownerId, out var exists); @@ -152,18 +160,15 @@ public ref T GetOrCreate(uint ownerId, out bool added, Func? factory = componentRef = factory?.Invoke(ownerId) ?? new T(); Added?.Invoke(ownerId); - ComponentAdded?.Invoke(ownerId, result.Index, ref componentRef); + ComponentAdded?.Invoke(ownerId, ref componentRef); added = true; return ref componentRef; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref T GetByIndex(int index) => ref _values[index]; - [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetValues() => _values.AsSpan(0, _count); - + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool Has(uint ownerId) => ContainsEntry(ownerId); @@ -183,7 +188,7 @@ public bool TryAdd(uint ownerId, in T component) componentRef = component; Added?.Invoke(ownerId); - ComponentAdded?.Invoke(ownerId, result.Index, ref componentRef); + ComponentAdded?.Invoke(ownerId, ref componentRef); return true; } @@ -191,9 +196,6 @@ public bool TryAdd(uint ownerId, in T component) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T TryGet(uint ownerId) => ref GetEntryRef(ownerId); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryGetIndex(uint ownerId) => TryGetEntryIndex(ownerId); - public bool Update(uint ownerId, in T component) { ref var exists = ref GetEntryRef(ownerId); diff --git a/src/Hexecs/Actors/Components/IActorComponentPool.cs b/src/Hexecs/Actors/Components/IActorComponentPool.cs index 698ff82..9b87eef 100644 --- a/src/Hexecs/Actors/Components/IActorComponentPool.cs +++ b/src/Hexecs/Actors/Components/IActorComponentPool.cs @@ -76,11 +76,4 @@ internal interface IActorComponentPool /// /// JSON-писатель для записи данных. void Serialize(Utf8JsonWriter writer); - - /// - /// Пытается получить индекс компонента для актёра с указанным идентификатором. - /// - /// Идентификатор актёра. - /// Индекс компонента или отрицательное значение, если компонент не найден. - int TryGetIndex(uint ownerId); } \ No newline at end of file diff --git a/src/Hexecs/Actors/Delegates/ActorComponentAdded.cs b/src/Hexecs/Actors/Delegates/ActorComponentAdded.cs index b2af844..e41d3d4 100644 --- a/src/Hexecs/Actors/Delegates/ActorComponentAdded.cs +++ b/src/Hexecs/Actors/Delegates/ActorComponentAdded.cs @@ -5,7 +5,6 @@ /// /// Тип компонента, должен быть структурой и реализовывать интерфейс IActorComponent. /// Идентификатор актёра, к которому был добавлен компонент. -/// Индекс компонента в коллекции компонентов. /// Ссылка на добавленный компонент. -public delegate void ActorComponentAdded(uint actorId, int index, ref T component) +public delegate void ActorComponentAdded(uint actorId, ref T component) where T : struct, IActorComponent; diff --git a/src/Hexecs/Actors/Nodes/ActorNodeComponent.cs b/src/Hexecs/Actors/Nodes/ActorNodeComponent.cs index ea4fca0..26e0043 100644 --- a/src/Hexecs/Actors/Nodes/ActorNodeComponent.cs +++ b/src/Hexecs/Actors/Nodes/ActorNodeComponent.cs @@ -2,31 +2,21 @@ namespace Hexecs.Actors.Nodes; -[method: MethodImpl(MethodImplOptions.AggressiveInlining)] -internal readonly struct ActorNodeComponent(ActorNode node) : IActorComponent, IDisposable +[StructLayout(LayoutKind.Sequential, Size = 32)] +internal struct ActorNodeComponent : IActorComponent { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ActorNodeComponent Create(uint ownerId) => new(new ActorNode(ownerId)); + public uint NextSiblingId; + public uint FirstChildId; + public uint ParentId; + public uint PrevSiblingId; + public uint ChildCount; public static ActorComponentConfiguration CreatePoolConfiguration() { return new ActorComponentConfiguration( null, null, - DisposeHandler, + null, ActorNodeComponentConverter.Instance); } - - public static void DisposeHandler(ref ActorNodeComponent component) - { - component.Dispose(); - component = new ActorNodeComponent(null!); // remove reference to Node for GC - } - - public readonly ActorNode Node = node; - - public void Dispose() - { - Node.Dispose(); - } } \ No newline at end of file diff --git a/src/Hexecs/Hexecs.csproj b/src/Hexecs/Hexecs.csproj index 18f830a..51de86b 100644 --- a/src/Hexecs/Hexecs.csproj +++ b/src/Hexecs/Hexecs.csproj @@ -65,9 +65,6 @@ ActorFilter2.cs - - ActorFilter2.cs - ActorFilter2.cs @@ -119,9 +116,6 @@ ActorFilter3.cs - - ActorFilter3.cs - ActorFilter3.cs diff --git a/src/Hexecs/Utils/ComponentsAccess.cs b/src/Hexecs/Utils/ComponentsAccess.cs new file mode 100644 index 0000000..f405985 --- /dev/null +++ b/src/Hexecs/Utils/ComponentsAccess.cs @@ -0,0 +1,29 @@ +namespace Hexecs.Utils; + +public readonly ref struct ComponentsAccess +{ + private readonly uint[] _sparse; + private readonly T[] _values; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ComponentsAccess() + { + _sparse = []; + _values = []; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ComponentsAccess(uint[] sparse, T[] values) + { + _sparse = sparse; + _values = values; + } + + public ref T this[uint id] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref Unsafe.Add( + ref MemoryMarshal.GetArrayDataReference(_values), + (int)Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_sparse), (int)id) - 1); + } +} \ No newline at end of file