Skip to content

Commit

Permalink
Edges should now be also trimmed once an Archetype was removed duri…
Browse files Browse the repository at this point in the history
…ng `World.TrimExcess`.
  • Loading branch information
genaray committed Aug 24, 2023
1 parent 53e5068 commit ff48d79
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 66 deletions.
38 changes: 33 additions & 5 deletions src/Arch.Tests/WorldTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ public partial class WorldTest
public void Setup()
{
_world = World.Create();
/*
Entity entity = default;
EntityReference entityReference = default;
_world.Create();*/

for (var index = 0; index < 10000; index++)
{
Expand Down Expand Up @@ -232,7 +228,7 @@ public void TrimExcess()
world.Create<HeavyComponent>();
}

// Destroy half of the world entities
// Destroy all but one
var counter = 0;
var query = new QueryDescription().WithAll<HeavyComponent>();
world.Query(in query, (in Entity entity) =>
Expand All @@ -254,6 +250,38 @@ public void TrimExcess()
That(archetype.Capacity, Is.EqualTo(1));
}

/// <summary>
/// Checks if the <see cref="World"/> trims its archetypes correctly and removes them when empty.
/// </summary>
[Test]
public void TrimExcessEmptyArchetypes()
{
// Fill world
var amount = 10000;
using var world = World.Create();
for (int index = 0; index < amount; index++)
{
world.Create<int>();
world.Create<byte>();
}

var entity = world.Create<byte>();
world.Add<int>(entity);
world.Destroy(entity);

// Destroy all of the world entities
var query = new QueryDescription().WithAll<int>();
world.Destroy(query);

// Trim
world.TrimExcess();

var archetype = world.Archetypes[0];
That(world.Archetypes.Count, Is.EqualTo(1));
That(world.Capacity, Is.EqualTo(archetype.Entities));
}


/// <summary>
/// Checks if the <see cref="World"/> clears itself correctly.
/// </summary>
Expand Down
85 changes: 67 additions & 18 deletions src/Arch/Core/Edges/Archetype.Edges.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Arch.Core.Extensions.Internal;
using System.Buffers;
using Arch.Core.Extensions.Internal;
using Arch.Core.Utils;

namespace Arch.Core;
Expand All @@ -19,8 +20,11 @@ public partial class Archetype
/// otherwise a dictionary is used.
/// </summary>
/// <remarks>The index used is <see cref="ComponentType.Id"/> minus one.</remarks>
/// TODO : Kill me and replace me with a better jaggedarray.
private readonly ArrayDictionary<Archetype> _addEdges;

#if !NET5_0_OR_GREATER

/// <summary>
/// Tries to get a cached archetype that is reached through adding a component
/// type to this archetype.
Expand All @@ -32,28 +36,25 @@ public partial class Archetype
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void CreateAddEdge(int index, Archetype archetype)
{
_addEdges.Set(index, archetype);
_addEdges.Add(index, archetype);
}

#if NET5_0_OR_GREATER
/// <summary>
/// Gets a reference to a cached archetype that is reached through adding a
/// component type to this archetype.
/// Tries to get a cached archetype that is reached through adding a component
/// type to this archetype.
/// </summary>
/// <param name="index">
/// The index of the archetype in the cache, <see cref="ComponentType.Id"/> - 1
/// </param>
/// <param name="exists">True if the cached archetype existed, false otherwise.</param>
/// <returns>
/// A reference to the archetype, or a null reference to the created slot in the
/// cache if it did not exist.
/// </returns>
/// <param name="archetype">The cached archetype if it exists, null otherwise.</param>
/// <returns>True if the archetype exists, false otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref Archetype CreateOrGetAddEdge(int index, [UnscopedRef] out bool exists)
internal bool TryGetAddEdge(int index, [NotNullWhen(true)] out Archetype? archetype)
{
return ref _addEdges.AddOrGet(index, out exists);
return _addEdges.TryGet(index, out archetype);
}
#endif

#else

/// <summary>
/// Tries to get a cached archetype that is reached through adding a component
Expand All @@ -62,12 +63,60 @@ internal ref Archetype CreateOrGetAddEdge(int index, [UnscopedRef] out bool exis
/// <param name="index">
/// The index of the archetype in the cache, <see cref="ComponentType.Id"/> - 1
/// </param>
/// <param name="archetype">The cached archetype if it exists, null otherwise.</param>
/// <returns>True if the archetype exists, false otherwise.</returns>
/// <param name="exists">True if it exists, false if not.</param>
/// <returns>The cached archetype if it exists, null otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryGetAddEdge(int index, [NotNullWhen(true)] out Archetype? archetype)
internal ref Archetype TryGetAddEdge(int index, [UnscopedRef] out bool exists)
{
archetype = _addEdges.AddOrGet(index, out var exists);
return exists;
return ref _addEdges.TryGet(index, out exists!);
}

#endif

/// <summary>
/// Removes an Edge at the given index.
/// </summary>
/// <param name="index">The index of the archetype in the cache, <see cref="ComponentType.Id"/> - 1</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RemoveAddEdge(int index)
{
_addEdges.Remove(index);
}

/// <summary>
/// Removes an edge for a certain <see cref="Archetype"/>.
/// </summary>
/// <param name="archetype">The <see cref="Archetype"/> to remove edges for.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void RemoveAddEdge(Archetype archetype)
{
// Scan array for the archetype and remove it where it exists.
for (var index = 0; index < _addEdges._array.Length; index++)
{
var edge = _addEdges._array[index];
if (edge == archetype)
{
RemoveAddEdge(index);
}
}

// Scan dictionary and remove it where it exists.
var keys = ArrayPool<int>.Shared.Rent(_addEdges._dictionary.Count);
var count = 0;
foreach (var kvp in _addEdges._dictionary)
{
if (kvp.Value == archetype)
{
keys[count] = kvp.Key;
count++;
}
}

for (var index = 0; index < count; index++)
{
var key = keys[index];
_addEdges._dictionary.Remove(key);
}
ArrayPool<int>.Shared.Return(keys, true);
}
}
2 changes: 1 addition & 1 deletion src/Arch/Core/Edges/World.Edges.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private Archetype GetOrCreateArchetypeByEdge(in ComponentType type, Archetype ol
var edgeIndex = type.Id - 1;

#if NET5_0_OR_GREATER
ref var newArchetype = ref oldArchetype.CreateOrGetAddEdge(edgeIndex, out var exists);
ref var newArchetype = ref oldArchetype.TryGetAddEdge(edgeIndex, out var exists);
if (!exists)
{
newArchetype = GetOrCreate(oldArchetype.Types.Add(type));
Expand Down
101 changes: 61 additions & 40 deletions src/Arch/Core/Utils/ArrayDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace Arch.Core.Utils;

/// <summary>
/// The <see cref="ArrayDictionary{TValue}"/> class
/// represents an hybrid collection that uses arrays and dictionarys.
/// Arrays are used for indexes below <see cref="_maxArraySize"/> and for higher indexes
/// </summary>
/// <typeparam name="TValue"></typeparam>
internal class ArrayDictionary<TValue>
{
/// <summary>
Expand All @@ -10,15 +16,15 @@ internal class ArrayDictionary<TValue>
/// When the index used to access one is equal to or bigger than
/// <see cref="_maxArraySize"/>, <see cref="_dictionary"/> is used instead.
/// </summary>
private TValue[] _array;
internal TValue[] _array;

/// <summary>
/// Stores <see cref="TValue"/>s with an index equal to or bigger than
/// <see cref="_maxArraySize"/>.
/// When the index used to access one is smaller than <see cref="_maxArraySize"/>,
/// <see cref="_dictionary"/> is used instead.
/// </summary>
private readonly Dictionary<int, TValue> _dictionary;
internal readonly Dictionary<int, TValue> _dictionary;

/// <summary>
/// The maximum size that <see cref="_array"/> will grow to.
Expand All @@ -33,75 +39,90 @@ internal ArrayDictionary(int maxArraySize)
_maxArraySize = maxArraySize;
}

#if NET5_0_OR_GREATER
/// <summary>
/// Gets a reference to an element in this collection by its index.
/// Adds an element in this collection by its index.
/// If the index is smaller than <see cref="_maxArraySize"/>
/// <see cref="_array"/> is used, otherwise <see cref="_dictionary"/> is used.
/// </summary>
/// <param name="index">The index of the value to get.</param>
/// <param name="exists">Whether or not the value existed in this collection.</param>
/// <returns>A ref to the value, with a default value if it did not exist.</returns>
/// <param name="item">The item being added.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref TValue AddOrGet(int index, [UnscopedRef] out bool exists)
internal void Add(int index, TValue item)
{
Debug.Assert(index >= 0);

if (index < _maxArraySize)
{
ref var value = ref ArrayExtensions.GetOrResize(ref _array, index, _maxArraySize);
exists = !Equals(value, default(TValue));
return ref value;
Array.Resize(ref _array, _maxArraySize);
_array[index] = item;
}
else
{
_dictionary[index] = item;
}

return ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, index, out exists)!;
}
#else

/// <summary>
/// Gets an element in this collection by its index.
/// If the index is smaller than <see cref="_maxArraySize"/>
/// <see cref="_array"/> is used, otherwise <see cref="_dictionary"/> is used.
/// Removes an item at the given index.
/// </summary>
/// <param name="index">The index of the value to get.</param>
/// <param name="exists">Whether or not the value existed in this collection.</param>
/// <returns>The value, with a default value if it did not exist.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal TValue AddOrGet(int index, out bool exists)
/// <param name="index">The index.</param>
internal void Remove(int index)
{
Debug.Assert(index >= 0);

if (index >= _maxArraySize)
if (index < _maxArraySize)
{
ref var value = ref ArrayExtensions.GetOrResize(ref _array, index, _maxArraySize);
exists = !Equals(value, default(TValue));
return value;
Array.Resize(ref _array, _maxArraySize);
_array[index] = default!;
}
else
{
exists = _dictionary.TryGetValue(index, out var value);
return value;
_dictionary.Remove(index);
}
}
#endif

#if !NET5_0_OR_GREATER

/// <summary>
/// Sets an element in this collection by <see cref="index"/>.
/// Trys to return an item at the given index.
/// </summary>
/// <param name="index">The index of the <see cref="value"/> to set.</param>
/// <param name="value">The value to set at <see cref="index"/></param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Set(int index, in TValue value)
/// <param name="index">The index.</param>
/// <param name="item">The item.</param>
/// <returns>True if it exists, false if it does not.</returns>
internal bool TryGet(int index, out TValue item)
{
Debug.Assert(index >= 0);

if (index >= _maxArraySize)
if (index < _maxArraySize)
{
ref var arrayValue = ref ArrayExtensions.GetOrResize(ref _array, index, _maxArraySize);
arrayValue = value;
Array.Resize(ref _array, _maxArraySize);
item = _array[index];
var exists = !Equals(item, default(TValue));
return exists;
}
else

return _dictionary.TryGetValue(index, out item);
}

#else

/// <summary>
/// Trys to return an item at the given index.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="exists">If the item exists..</param>
/// <returns>A reference to the existing item.</returns>
internal ref TValue TryGet(int index, [UnscopedRef] out bool exists)
{
Debug.Assert(index >= 0);

if (index < _maxArraySize)
{
_dictionary[index] = value;
Array.Resize(ref _array, _maxArraySize);
ref var item = ref _array[index];
exists = !Equals(item, default(TValue));
return ref item;
}

return ref CollectionsMarshal.GetValueRefOrAddDefault(_dictionary, index, out exists)!;
}
#endif
}
22 changes: 20 additions & 2 deletions src/Arch/Core/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,29 @@ public void TrimExcess()
{
Capacity = 0;

// Trim entity info and chunks
// Trim entity info and archetypes
EntityInfo.TrimExcess();
foreach (ref var archetype in this)
for (var index = Archetypes.Count-1; index >= 0; index--)
{
var archetype = Archetypes[index];
archetype.TrimExcess();

// Remove empty archetypes to clean up memory, skip if so
if (archetype.Entities == 0)
{
var hash = Component.GetHashCode(archetype.Types);
Archetypes.RemoveAt(index);
GroupToArchetype.Remove(hash);

// Remove archetype from other archetypes edges.
foreach (var otherArchetype in this)
{
otherArchetype.RemoveAddEdge(archetype);
}

continue;
}

Capacity += archetype.Size * archetype.EntitiesPerChunk; // Since always one chunk always exists.
}
}
Expand Down

0 comments on commit ff48d79

Please sign in to comment.