Skip to content

Commit

Permalink
ECS - CommandBuffer: optimize add/remove component/tags
Browse files Browse the repository at this point in the history
  • Loading branch information
friflo committed Aug 26, 2024
1 parent 11b92db commit 1156457
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 45 deletions.
47 changes: 29 additions & 18 deletions src/ECS/CommandBuffer/CommandBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ public void Playback()
}
finally {
intern.Reset(hasComponentChanges);
playback.entityChanges.Clear();
playback.entityChangesCount = 0;
playback.entityChangesIndexes.Clear();
if (!intern.reuseBuffer) {
ReturnBuffer();
}
Expand Down Expand Up @@ -239,34 +240,42 @@ private void ExecuteEntityCommands()

private void ExecuteTagCommands(Playback playback)
{
var entityChanges = playback.entityChanges;
var nodes = playback.store.nodes.AsSpan();
var commands = intern.tagCommands.AsSpan(0, intern.tagCommandsCount);
var indexes = playback.entityChangesIndexes;
var changes = playback.entityChanges;
var nodes = playback.store.nodes.AsSpan();
var commands = intern.tagCommands.AsSpan(0, intern.tagCommandsCount);
bool exists;

foreach (var tagCommand in commands)
{
var entityId = tagCommand.entityId;
#if NET6_0_OR_GREATER
ref var change = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(entityChanges, entityId, out exists);
ref var changeIndex = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(indexes, entityId, out exists);
#else
exists = entityChanges.TryGetValue(entityId, out var change);
exists = indexes.TryGetValue(entityId, out var changeIndex);
#endif
if (!exists) {
var archetype = nodes[entityId].archetype;
if (archetype == null) {
throw EntityNotFound(tagCommand);
}
change.componentTypes = archetype.componentTypes;
change.tags = archetype.tags;
change.oldArchetype = archetype;
changeIndex = playback.entityChangesCount++;
if (changes.Length == changeIndex) {
changes = playback.ResizeChanges();
}
ref var newChange = ref changes[changeIndex];
newChange.componentTypes = archetype.componentTypes;
newChange.tags = archetype.tags;
newChange.oldArchetype = archetype;
newChange.entityId = entityId;
}
ref var change = ref changes[changeIndex];
if (tagCommand.change == TagChange.Add) {
change.tags.bitSet.SetBit(tagCommand.tagIndex);
} else {
change.tags.bitSet.ClearBit(tagCommand.tagIndex);
}
MapUtils.Set(entityChanges, entityId, change);
MapUtils.Set(indexes, entityId, changeIndex);
}
}

Expand All @@ -277,13 +286,14 @@ private static void SendTagEvents(Playback playback)
if (tagsChanged == null) {
return;
}
foreach (var (id, change) in playback.entityChanges)
var changes = new Span<EntityChange>(playback.entityChanges, 0 , playback.entityChangesCount);
foreach (ref var change in changes)
{
var oldArchetype = change.oldArchetype;
if (change.tags.bitSet.Equals(oldArchetype.tags.bitSet)) {
continue;
}
tagsChanged.Invoke(new TagsChanged(store, id, change.tags, oldArchetype.tags));
tagsChanged.Invoke(new TagsChanged(store, change.entityId, change.tags, oldArchetype.tags));
}
}

Expand Down Expand Up @@ -370,21 +380,22 @@ private static InvalidOperationException EntityNotFound(TagCommand command) {

private static void UpdateEntityArchetypes(Playback playback)
{
var store = playback.store;
var nodes = store.nodes.AsSpan();
var store = playback.store;
var changes = new Span<EntityChange>(playback.entityChanges, 0 , playback.entityChangesCount);
var nodes = store.nodes.AsSpan();

foreach (var (entityId, change) in playback.entityChanges)
foreach (ref var change in changes)
{
var oldArchetype = change.oldArchetype;
var oldArchetype = change.oldArchetype;
if (oldArchetype.componentTypes.bitSet.Equals(change.componentTypes.bitSet) &&
oldArchetype.tags. bitSet.Equals(change.tags. bitSet)) {
continue;
}
// case: archetype changed
ref var node = ref nodes[entityId];
ref var node = ref nodes[change.entityId];
var newArchetype = store.GetArchetype(change.componentTypes, change.tags);
node.archetype = newArchetype;
node.compIndex = Archetype.MoveEntityTo(oldArchetype, entityId, node.compIndex, newArchetype);
node.compIndex = Archetype.MoveEntityTo(oldArchetype, change.entityId, node.compIndex, newArchetype);
}
}

Expand Down
44 changes: 27 additions & 17 deletions src/ECS/CommandBuffer/Commands/ComponentCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ internal ComponentCommands(int structIndex) : base(structIndex) { }

internal override void UpdateComponentTypes(Playback playback, bool storeOldComponent)
{
var index = structIndex;
var commands = componentCommands.AsSpan(0, commandCount);
var entityChanges = playback.entityChanges;
var nodes = playback.store.nodes.AsSpan();
var index = structIndex;
var commands = componentCommands.AsSpan(0, commandCount);
var indexes = playback.entityChangesIndexes;
var changes = playback.entityChanges;
var nodes = playback.store.nodes.AsSpan();

bool exists;

// --- set new entity component types for Add/Remove commands
Expand All @@ -61,9 +63,9 @@ internal override void UpdateComponentTypes(Playback playback, bool storeOldComp
}
var entityId = command.entityId;
#if NET6_0_OR_GREATER
ref var change = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(entityChanges, entityId, out exists);
ref var changeIndex = ref System.Runtime.InteropServices.CollectionsMarshal.GetValueRefOrAddDefault(indexes, entityId, out exists);
#else
exists = entityChanges.TryGetValue(entityId, out var change);
exists = indexes.TryGetValue(entityId, out var changeIndex);
#endif
ref var node = ref nodes[entityId];
var archetype = node.archetype;
Expand All @@ -77,16 +79,23 @@ internal override void UpdateComponentTypes(Playback playback, bool storeOldComp
}
}
if (!exists) {
change.componentTypes = archetype.componentTypes;
change.tags = archetype.tags;
change.oldArchetype = archetype;
changeIndex = playback.entityChangesCount++;
if (changes.Length == changeIndex) {
changes = playback.ResizeChanges();
}
ref var newChange = ref changes[changeIndex];
newChange.componentTypes = archetype.componentTypes;
newChange.tags = archetype.tags;
newChange.oldArchetype = archetype;
newChange.entityId = entityId;
}
ref var change = ref changes[changeIndex];
if (command.change == Remove) {
change.componentTypes.bitSet.ClearBit(index);
} else {
change.componentTypes.bitSet.SetBit (index);
}
MapUtils.Set(entityChanges, entityId, change);
MapUtils.Set(indexes, entityId, changeIndex);
}
}

Expand Down Expand Up @@ -124,30 +133,31 @@ internal override void SendCommandEvents(Playback playback)
var store = playback.store;
var added = store.internBase.componentAdded;
var removed = store.internBase.componentRemoved;
var entityChanges = playback.entityChanges;
var indexes = playback.entityChangesIndexes;
var changes = playback.entityChanges;
Action<ComponentChanged> changed;

foreach (ref var command in commands)
{
var entityId = command.entityId;
var changes = entityChanges[entityId];
var oldHeap = changes.oldArchetype.heapMap[index];
ComponentChangedAction change;
ref var change = ref changes[indexes[entityId]];
var oldHeap = change.oldArchetype.heapMap[index];
ComponentChangedAction action;
if (command.change == Remove) {
change = Remove;
action = Remove;
if (oldHeap == null) {
continue;
}
changed = removed;
} else {
change = oldHeap == null ? Add : Update;
action = oldHeap == null ? Add : Update;
changed = added;
}
if (changed == null) {
continue;
}
stashValue = command.oldComponent;
changed.Invoke(new ComponentChanged(store, entityId, change, index, this));
changed.Invoke(new ComponentChanged(store, entityId, action, index, this));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/ECS/CommandBuffer/EntityStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal void ReturnCommandBuffer(CommandBuffer commandBuffer)

internal Playback GetPlayback()
{
if (intern.playback.entityChanges == null) {
if (intern.playback == null) {
intern.playback = new Playback(this);
}
return intern.playback;
Expand Down
25 changes: 19 additions & 6 deletions src/ECS/CommandBuffer/Playback.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Ullrich Praetz - https://github.com/friflo. All rights reserved.
// See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;

// ReSharper disable UseCollectionExpression
// ReSharper disable once CheckNamespace
namespace Friflo.Engine.ECS;

Expand All @@ -12,16 +14,27 @@ internal struct EntityChange
internal ComponentTypes componentTypes; // 32
internal Tags tags; // 32
internal Archetype oldArchetype; // 8
internal int entityId; // 4
}

internal readonly struct Playback
internal class Playback
{
// possible opportunity for optimization as Dictionary values are using 64 bytes
internal readonly Dictionary<int, EntityChange> entityChanges; // 8
internal readonly EntityStore store; // 8
/// <summary>
/// Store indexes into <see cref="entityChanges"/> instead the <see cref="EntityChange"/> value directly.<br/>
/// Size of <see cref="EntityChange"/> is too big (96 bytes) which degrade performance when rehashing Dictionary.
/// </summary>
internal readonly Dictionary<int, int> entityChangesIndexes; // 8
internal EntityChange[] entityChanges; // 8
internal int entityChangesCount; // 4
internal readonly EntityStore store; // 8

internal Playback(EntityStore store) {
this.store = store;
entityChanges = new Dictionary<int, EntityChange>();
this.store = store;
entityChangesIndexes = new Dictionary<int, int>();
entityChanges = Array.Empty<EntityChange>();
}

public EntityChange[] ResizeChanges() {
return ArrayUtils.Resize(ref entityChanges, Math.Max(8, 2 * entityChanges.Length));
}
}
4 changes: 1 addition & 3 deletions src/Tests/ECS/CommandBuffer/Test_CommandBuffer_Events.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Friflo.Engine.ECS;
using NUnit.Framework;
using NUnit.Framework.Internal;
using Tests.Examples;
using Tests.Utils;
using static NUnit.Framework.Assert;

Expand Down Expand Up @@ -95,7 +93,7 @@ public static void Test_CommandBuffer_Events_Tags()
[Test]
public static void Test_CommandBuffer_AddRemoveComponent_Perf()
{
int repeat = 10; // 1_000_000 - 13.394 sec
int repeat = 10; // 1_000_000 ~ #PC: 10.298 sec
var store = new EntityStore();
var entities = new Entity[100];
for (int n = 0; n < entities.Length; n++) {
Expand Down

0 comments on commit 1156457

Please sign in to comment.