diff --git a/src/Arch.Benchmarks/Benchmark.cs b/src/Arch.Benchmarks/Benchmark.cs index eec4e08..358f909 100644 --- a/src/Arch.Benchmarks/Benchmark.cs +++ b/src/Arch.Benchmarks/Benchmark.cs @@ -1,45 +1,16 @@ using System.Numerics; using Arch.Core; using Arch.Core.Extensions; + using Arch.Core.Utils; namespace Arch.Benchmarks; public class Benchmark { - private static void Main(string[] args) + public static void Main(string[] args) { - /* - // NOTE: Can this be replaced with ManualConfig.CreateEmpty()? -#pragma warning disable HAA0101 // Array allocation for params parameter - var config = new ManualConfig() - .WithOptions(ConfigOptions.DisableOptimizationsValidator) - .AddValidator(JitOptimizationsValidator.DontFailOnError) - .AddLogger(ConsoleLogger.Default) - .AddColumnProvider(DefaultColumnProviders.Instance); -#pragma warning restore HAA0101 // Array allocation for params parameter - */ - - - - var world = World.Create(); - for (var index = 0; index <= 100; index++) - { - world.Create(); - } - - var desc = new QueryDescription().WithAll(); - for (var index = 0; index <= 100000; index++) - { - world.Query(in desc, (ref int i) => - { - }); - } - - - - // NOTE: Is `-- --job` a typo? - // Use: dotnet run -c Release --framework net7.0 -- --job short --filter *IterationBenchmark* - //BenchmarkSwitcher.FromAssembly(typeof(Benchmark).Assembly).Run(args, config); + BenchmarkSwitcher.FromAssembly(typeof(Benchmark).Assembly).Run(args); + //BenchmarkSwitcher.FromAssembly(typeof(Benchmark).Assembly).Run(args, new DebugInProcessConfig()); } } diff --git a/src/Arch.Benchmarks/DuplicateBenchmark.cs b/src/Arch.Benchmarks/DuplicateBenchmark.cs new file mode 100644 index 0000000..ba3737d --- /dev/null +++ b/src/Arch.Benchmarks/DuplicateBenchmark.cs @@ -0,0 +1,67 @@ +using Arch.Core; +using Arch.Core.Utils; + +namespace Arch.Benchmarks; + +[HtmlExporter] +//[MemoryDiagnoser] +//[HardwareCounters(HardwareCounter.CacheMisses)] +public class DuplicateBenchmark +{ + public int Amount = 100000; + + private static readonly ComponentType[] _group = { typeof(Transform), typeof(Velocity) }; + private readonly QueryDescription _queryDescription = new(all: _group); + + private static World? _world; + private static Entity _entity = Entity.Null; + private static Entity[]? _array = null; + + [IterationSetup] + public void Setup() + { + _world = World.Create(); + _world.Reserve(_group, 1); + _entity = _world.Create(new Transform { X = 111, Y = 222}, new Velocity { X = 333, Y = 444 }); + _array = new Entity[Amount]; + } + + [IterationCleanup] + public void Cleanup() + { + World.Destroy(_world); + _world = null; + } + + /// DuplicateN() method. + [Benchmark] + public void DuplicateNInternal() + { + _world.DuplicateN(_entity, _array.AsSpan()); + } + + /// DuplicateN() in terms of Duplicate() method. + [Benchmark] + public void DuplicateNDuplicate() + { + for (int i = 0; i < Amount; ++i) + { + _array[i] = _world.Duplicate(_entity); + } + } + + /// Benchmark DuplicateN() if implemented via GetAllComponents. + [Benchmark] + public void DuplicateNGetAllComponents() + { + for (int i = 0; i < Amount; ++i) + { + var arch = _world.GetArchetype(_entity); + var copiedEntity = _world.Create(arch.Signature); + foreach (var c in _world.GetAllComponents(_entity)) + { + _world.Set(_entity, c); + } + } + } +} diff --git a/src/Arch.Tests/WorldTest.cs b/src/Arch.Tests/WorldTest.cs index d219795..c22b6c6 100644 --- a/src/Arch.Tests/WorldTest.cs +++ b/src/Arch.Tests/WorldTest.cs @@ -658,6 +658,42 @@ public void Add_NonGeneric() That(_world.GetArchetype(entity2), Is.EqualTo(_world.GetArchetype(entity))); That(arch, Is.EqualTo(_world.GetArchetype(entity))); } + + [Test] + public void Duplicate() + { + var transform = new Transform { X = 111, Y = 222 }; + var entity = _world.Create(_entityGroup); + _world.Set(entity, transform); + var entity2 = _world.Duplicate(entity); + That(entity2.Id != entity.Id); + That(_world.IsAlive(entity2)); + That(_world.GetArchetype(entity), Is.EqualTo(_world.GetArchetype(entity2))); + That(_world.Get(entity).X, Is.EqualTo(_world.Get(entity2).X)); + That(_world.Get(entity).Y, Is.EqualTo(_world.Get(entity2).Y)); + } + + [Test] + public void DuplicateN() + { + var transform = new Transform { X = 111, Y = 222 }; + var entity = _world.Create(_entityGroup); + _world.Set(entity, transform); + var entities = new Entity[2]; + _world.DuplicateN(entity, entities.AsSpan()); + var entity2 = entities[0]; + var entity3 = entities[1]; + That(entity2.Id != entity.Id); + That(_world.IsAlive(entity2)); + That(_world.GetArchetype(entity), Is.EqualTo(_world.GetArchetype(entity2))); + That(_world.Get(entity).X, Is.EqualTo(_world.Get(entity2).X)); + That(_world.Get(entity).Y, Is.EqualTo(_world.Get(entity2).Y)); + That(entity3.Id != entity.Id); + That(_world.IsAlive(entity3)); + That(_world.GetArchetype(entity), Is.EqualTo(_world.GetArchetype(entity3))); + That(_world.Get(entity).X, Is.EqualTo(_world.Get(entity3).X)); + That(_world.Get(entity).Y, Is.EqualTo(_world.Get(entity3).Y)); + } } /// diff --git a/src/Arch/Core/Archetype.cs b/src/Arch/Core/Archetype.cs index c6a0519..6ee7049 100644 --- a/src/Arch/Core/Archetype.cs +++ b/src/Arch/Core/Archetype.cs @@ -278,7 +278,7 @@ public sealed partial class Archetype /// The component structure of the 's that can be stored in this . internal Archetype(Signature signature) { - Types = signature; + Signature = signature; // Calculations ChunkSizeInBytes = MinimumRequiredChunkSize(signature); @@ -302,7 +302,12 @@ internal Archetype(Signature signature) /// /// The component types that the 's stored here have. /// - public ComponentType[] Types { get; } + public Signature Signature { get; } + + /// + /// The component types that the 's stored here have. + /// + public ComponentType[] Types => Signature; /// /// A bitset representation of the array for fast lookups and queries. diff --git a/src/Arch/Core/Extensions/EntityExtensions.cs b/src/Arch/Core/Extensions/EntityExtensions.cs index d26077e..9e221f6 100644 --- a/src/Arch/Core/Extensions/EntityExtensions.cs +++ b/src/Arch/Core/Extensions/EntityExtensions.cs @@ -216,6 +216,33 @@ public static void Remove(this in Entity entity) var world = World.Worlds[entity.WorldId]; world.Remove(entity); } + + /// + /// Duplicate this entity + /// + public static Entity Duplicate(this in Entity entity) + { + var world = World.Worlds[entity.WorldId]; + return world.Duplicate(entity); + } + + /// + /// Duplicate this output.Length times + /// + public static void DuplicateN(this in Entity entity, Span output) + { + var world = World.Worlds[entity.WorldId]; + world.DuplicateN(entity, output); + } + + /// + /// Duplicate this entity n times + /// + public static void DuplicateN(this in Entity entity, int n, Span output) + { + var world = World.Worlds[entity.WorldId]; + world.DuplicateN(entity, n, output); + } #endif } diff --git a/src/Arch/Core/World.cs b/src/Arch/Core/World.cs index a200b4a..1304336 100644 --- a/src/Arch/Core/World.cs +++ b/src/Arch/Core/World.cs @@ -276,6 +276,31 @@ public Entity Create(params ComponentType[] types) /// [StructuralChange] public Entity Create(in Signature types) + { + var entity = CreateNoEvent(types); + + OnEntityCreated(entity); +#if EVENTS + foreach (ref var type in types) + { + OnComponentAdded(entity, type); + } +#endif + + return entity; + } + + /// + /// Creates a new using its given component structure/. + /// Might resize its target and allocate new space if its full. + /// + /// + /// Causes a structural change. + /// + /// Its component structure/. + /// + [StructuralChange] + private Entity CreateNoEvent(in Signature types) { // Recycle id or increase var recycle = RecycledIds.TryDequeue(out var recycledId); @@ -298,14 +323,6 @@ public Entity Create(in Signature types) // Add entity to info storage EntityInfo.Add(entity.Id, recycled.Version, archetype, slot); Size++; - OnEntityCreated(entity); - -#if EVENTS - foreach (ref var type in types) - { - OnComponentAdded(entity, type); - } -#endif return entity; } @@ -569,6 +586,62 @@ public override string ToString() { return $"{GetType().Name} {{ {nameof(Id)} = {Id}, {nameof(Capacity)} = {Capacity}, {nameof(Size)} = {Size} }}"; } + + /// + /// Create a copy of the given entity. + /// + public Entity Duplicate(Entity sourceEntity) + { + Debug.Assert(IsAlive(sourceEntity)); + Archetype archetype = GetArchetype(sourceEntity); + Entity destinationEntity = CreateNoEvent(archetype.Signature); + EntitySlot fromIndex = EntityInfo.GetEntitySlot(sourceEntity.Id); + EntitySlot destinationIndex = EntityInfo.GetEntitySlot(destinationEntity.Id); + ref Chunk fromChunk = ref archetype.GetChunk(fromIndex.Slot.ChunkIndex); + ref Chunk toChunk = ref archetype.GetChunk(destinationIndex.Slot.ChunkIndex); + for (int i = 0; i < fromChunk.Components.Length; ++i) + { + Array fromArray = fromChunk.Components[i]; + Array toArray = toChunk.Components[i]; + Array.Copy(fromArray, fromIndex.Slot.Index, toArray, destinationIndex.Slot.Index, 1); + } + + OnEntityCreated(sourceEntity); +#if EVENTS + foreach (var type in archetype.Types) + { + OnComponentAdded(sourceEntity, type); + } +#endif + + return destinationEntity; + } + + /// + /// Create n copies of the given entity. + /// + public void DuplicateN(Entity sourceEntity, int n, Span outputSpan) + { + Debug.Assert(IsAlive(sourceEntity)); + Debug.Assert(n > 0); + Debug.Assert(n <= outputSpan.Length); + // Note: this could be optimised by getting the chunks and using + // Array.Fill(), assuming we could guarantee writing to the end of the + // chunk. + for (int i = 0; i < n; ++i) + { + outputSpan[i] = Duplicate(sourceEntity); + } + } + + /// + /// Create n copies of the given entity, where n is outputSpan.Length. + /// + public void DuplicateN(Entity sourceEntity, Span outputSpan) + { + Debug.Assert(IsAlive(sourceEntity)); + DuplicateN(sourceEntity, outputSpan.Length, outputSpan); + } } #endregion