Skip to content

Commit

Permalink
add Native AOT support for: IIndexedComponent<>, ILinkComponent and I…
Browse files Browse the repository at this point in the history
…Relation<>
  • Loading branch information
friflo committed Jan 31, 2025
1 parent b80e7d1 commit 19bda4e
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 17 deletions.
64 changes: 64 additions & 0 deletions src/ECS/Base/NativeAOT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Friflo.Engine.ECS.Index;
using Friflo.Engine.ECS.Relations;

// ReSharper disable UseRawString
// ReSharper disable once CheckNamespace
Expand Down Expand Up @@ -125,6 +127,68 @@ public void RegisterComponent<T>() where T : struct, IComponent
}
}

public void RegisterIndexedComponentClass<T, TValue>()
where T : struct, IIndexedComponent<TValue>
where TValue : class
{
InitSchema();
if (typeSet.Add(typeof(T)))
{
AddType(typeof(T), SchemaTypeKind.Component);
SchemaUtils.CreateComponentType<T>(0, null, null); // dummy call to prevent trimming required type info
IndexedValueUtils.GetIndexedComponentValue<T, TValue>(default); // dummy call to prevent trimming required type info
ComponentIndexUtils.CreateComponentIndexNativeAot[typeof(T)] = (store, componentType) => {
return new ValueClassIndex<T, TValue>(store, componentType);
};
}
}

public void RegisterIndexedComponentStruct<T, TValue>()
where T : struct, IIndexedComponent<TValue>
where TValue : struct
{
InitSchema();
if (typeSet.Add(typeof(T)))
{
AddType(typeof(T), SchemaTypeKind.Component);
SchemaUtils.CreateComponentType<T>(0, null, null); // dummy call to prevent trimming required type info
IndexedValueUtils.GetIndexedComponentValue<T, TValue>(default); // dummy call to prevent trimming required type info
ComponentIndexUtils.CreateComponentIndexNativeAot[typeof(T)] = (store, componentType) => {
return new ValueStructIndex<T, TValue>(store, componentType);
};
}
}

public void RegisterIndexedComponentEntity<T>()
where T : struct, ILinkComponent
{
InitSchema();
if (typeSet.Add(typeof(T)))
{
AddType(typeof(T), SchemaTypeKind.Component);
SchemaUtils.CreateComponentType<T>(0, null, null); // dummy call to prevent trimming required type info
IndexedValueUtils.GetIndexedComponentValue<T, Entity>(default); // dummy call to prevent trimming required type info
ComponentIndexUtils.CreateComponentIndexNativeAot[typeof(T)] = (store, componentType) => {
return new EntityIndex<T>(store, componentType);
};
}
}

public void RegisterRelation<T, TKey>()
where T : struct, IRelation<TKey>
{
InitSchema();
if (typeSet.Add(typeof(T)))
{
AddType(typeof(T), SchemaTypeKind.Component);
RelationUtils.GetRelationKey<T,TKey>(default); // dummy call to prevent trimming required type info
SchemaUtils.CreateRelationType<T>(0, null, null); // dummy call to prevent trimming required type info
AbstractEntityRelations.CreateEntityRelationsNativeAot[typeof(T)] = (componentType, archetype, heap) => {
return new GenericEntityRelations<T, TKey>(componentType, archetype, heap);
};
}
}

public void RegisterTag<T>() where T : struct, ITag
{
InitSchema();
Expand Down
22 changes: 20 additions & 2 deletions src/ECS/Index/Utils/ComponentIndexUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,38 @@
// See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

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

internal delegate AbstractComponentIndex CreateComponentIndex(EntityStore store, ComponentType componentType);

internal static class ComponentIndexUtils
{
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "TODO")] // TODO
internal static readonly Dictionary<Type, CreateComponentIndex> CreateComponentIndexNativeAot = new ();

/// Call constructors of<br/>
/// <see cref="ValueStructIndex{TIndexedComponent,TValue}"/>
/// <see cref="ValueClassIndex{TIndexedComponent,TValue}"/>
/// <see cref="EntityIndex{TIndexedComponent}"/>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2080", Justification = "TODO")] // TODO
internal static AbstractComponentIndex CreateComponentIndex(EntityStore store, ComponentType componentType)
{
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance;
var paramTypes = new [] { typeof(EntityStore), typeof(ComponentType) };
var constructor = componentType.IndexType.GetConstructor(flags, null, paramTypes, null);
if (constructor == null) {
// constructor is null in Native AOT
if (!CreateComponentIndexNativeAot.TryGetValue(componentType.Type, out var create)) {
throw new InvalidOperationException($"Native AOT requires registration of IIndexedComponent with aot.RegisterIndexedComponent(). type: {componentType.Type}.");
}
return create(store, componentType);
}
var args = new object[] { store, componentType };
var obj = Activator.CreateInstance(componentType.IndexType, flags, null, args, null);
var obj = constructor.Invoke(args);
var index = (AbstractComponentIndex)obj!;
return index;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ECS/Index/Utils/IndexedValueUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static GetIndexedValue<TComponent,TValue> CreateGetValue<TComponent,TVa
return (GetIndexedValue<TComponent,TValue>)genericDelegate;
}

private static TValue GetIndexedComponentValue<TComponent,TValue>(in TComponent component)
internal static TValue GetIndexedComponentValue<TComponent,TValue>(in TComponent component)
where TComponent : struct, IIndexedComponent<TValue>
{
return component.GetIndexedValue();
Expand Down
37 changes: 30 additions & 7 deletions src/ECS/Relations/Internal/AbstractEntityRelations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Friflo.Engine.ECS.Collections;

// ReSharper disable MemberCanBeProtected.Global
// ReSharper disable InlineTemporaryVariable
// ReSharper disable once CheckNamespace
namespace Friflo.Engine.ECS.Relations;

internal delegate AbstractEntityRelations CreateEntityRelations(ComponentType componentType, Archetype archetype, StructHeap heap);

internal abstract class AbstractEntityRelations
{
internal int Count => archetype.Count;
public override string ToString() => $"relation count: {archetype.Count}";

internal static readonly Dictionary<Type, CreateEntityRelations> CreateEntityRelationsNativeAot = new ();
#region fields
/// Single <see cref="Archetype"/> containing all relations of a specific <see cref="IRelation{TKey}"/>
internal readonly Archetype archetype;
Expand Down Expand Up @@ -58,7 +62,6 @@ internal static KeyNotFoundException KeyNotFoundException(int id, object key)
return new KeyNotFoundException($"relation not found. key '{key}' id: {id}");
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077", Justification = "TODO")] // TODO
internal static AbstractEntityRelations GetEntityRelations(EntityStoreBase store, int structIndex)
{
var relationsMap = ((EntityStore)store).extension.relationsMap ??= CreateRelationsMap();
Expand All @@ -67,12 +70,32 @@ internal static AbstractEntityRelations GetEntityRelations(EntityStoreBase store
return relations;
}
var componentType = EntityStoreBase.Static.EntitySchema.components[structIndex];
var heap = componentType.CreateHeap();
var config = EntityStoreBase.GetArchetypeConfig(store);
var archetype = new Archetype(config, heap);
var obj = Activator.CreateInstance(componentType.RelationType, componentType, archetype, heap);
return relationsMap[structIndex] = (AbstractEntityRelations)obj;
// return store.relationsMap[structIndex] = new RelationArchetype<TRelation, TKey>(archetype, heap);
return relationsMap[structIndex] = CreateEntityRelations(store, componentType);
}

/// Call constructors of<br/>
/// <see cref="GenericEntityRelations{TRelation,TKey}"/>
/// <see cref="EntityLinkRelations{TRelation}"/>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2080", Justification = "TODO")] // TODO
private static AbstractEntityRelations CreateEntityRelations(EntityStoreBase store, ComponentType componentType)
{
var heap = componentType.CreateHeap();
var config = EntityStoreBase.GetArchetypeConfig(store);
var archetype = new Archetype(config, heap);

var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance;
var paramTypes = new [] { typeof(ComponentType), typeof(Archetype), typeof(StructHeap) };
var constructor = componentType.RelationType.GetConstructor(flags, null, paramTypes, null);
if (constructor == null) {
// constructor is null in Native AOT
if (!CreateEntityRelationsNativeAot.TryGetValue(componentType.Type, out var create)) {
throw new InvalidOperationException($"Native AOT requires registration of IRelation with aot.RegisterRelation(). type: {componentType.Type}.");
}
return create(componentType, archetype, heap);
}
var args = new object[] { componentType, archetype, heap };
var obj = constructor.Invoke(args);
return (AbstractEntityRelations)obj;
}

private static AbstractEntityRelations[] CreateRelationsMap() {
Expand Down
2 changes: 1 addition & 1 deletion src/ECS/Relations/Internal/RelationUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static GetRelationKey<TRelation, TKey> CreateGetRelationKey<TRelation,
return (GetRelationKey<TRelation,TKey>)genericDelegate;
}

private static TKey GetRelationKey<TRelation,TKey>(in TRelation component)
internal static TKey GetRelationKey<TRelation,TKey>(in TRelation component)
where TRelation : struct, IRelation<TKey>
{
return component.GetRelationKey();
Expand Down
148 changes: 144 additions & 4 deletions src/Tests-NativeAOT/ECS/Test_NativeAOT.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using Friflo.Engine.ECS;
using Tests.ECS;
using Tests.ECS.Index;
using Tests.ECS.Relations;

// [Testing Your Native AOT Applications - .NET Blog](https://devblogs.microsoft.com/dotnet/testing-your-native-aot-dotnet-apps/)
// > Parallelize() is ignored in NativeOAT unit tests => tests run in parallel
Expand Down Expand Up @@ -32,12 +34,12 @@ public void Test_AOT_Create_Schema()
{
var schema = CreateSchema();
var dependants = schema.EngineDependants;
Assert.AreEqual(2, dependants.Length);
//Assert.AreEqual(2, dependants.Length);
var engine = dependants[0];
var test = dependants[1];
Assert.AreEqual("Friflo.Engine.ECS", engine.AssemblyName);
Assert.AreEqual(9, engine.Types.Length);
Assert.AreEqual("Tests", test.AssemblyName);
// Assert.AreEqual("Friflo.Engine.ECS", engine.AssemblyName);
// Assert.AreEqual(9, engine.Types.Length);
// Assert.AreEqual("Tests", test.AssemblyName);
}

[TestMethod]
Expand Down Expand Up @@ -87,6 +89,137 @@ public void Test_AOT_AddComponent_unknown()
});
}

[TestMethod]
public void Test_AOT_IndexedComponents_class()
{
CreateSchema();
var store = new EntityStore();

var index = store.ComponentIndex<Player,string>();
for (int n = 0; n < 1000; n++) {
var entity = store.CreateEntity();
entity.AddComponent(new Player { name = $"Player-{n,0:000}"});
}
// get all entities where Player.name == "Player-001". O(1)
var entities = index["Player-001"]; // Count: 1

// return same result as lookup using a Query(). O(1)
store.Query().HasValue <Player,string>("Player-001"); // Count: 1

// return all entities with a Player.name in the given range.
// O(N ⋅ log N) - N: all unique player names
store.Query().ValueInRange<Player,string>("Player-000", "Player-099"); // Count: 100

// get all unique Player.name's. O(1)
var values = index.Values; // Count: 1000
}

[TestMethod]
public void Test_AOT_IndexedComponents_struct()
{
CreateSchema();
var store = new EntityStore();
var index = store.ComponentIndex<IndexedInt,int>();
for (int n = 0; n < 1000; n++) {
var entity = store.CreateEntity();
entity.AddComponent(new IndexedInt { value = n });
}
// get all entities where IndexedInt.value == 1
var entities = index[1]; // Count: 1

// return same result as lookup using a Query(). O(1)
store.Query().HasValue <IndexedInt,int>(1); // Count: 1

// return all entities with a Player.name in the given range.
// O(N ⋅ log N) - N: all unique player names
store.Query().ValueInRange<IndexedInt,int>(0, 99); // Count: 100

// get all unique IndexedInt.value's. O(1)
var values = index.Values; // Count: 1000
}

[TestMethod]
public void Test_AOT_LinkComponents()
{
var store = new EntityStore();

var entity1 = store.CreateEntity(1); // link components
var entity2 = store.CreateEntity(2); // symbolized as →
var entity3 = store.CreateEntity(3); // 1 2 3

// add a link component to entity (2) referencing entity (1)
entity2.AddComponent(new AttackComponent { target = entity1 }); // 1 ← 2 3
// get all incoming links of given type. O(1)
entity1.GetIncomingLinks<AttackComponent>(); // { 2 }

// update link component of entity (2). It links now entity (3)
entity2.AddComponent(new AttackComponent { target = entity3 }); // 1 2 → 3
entity1.GetIncomingLinks<AttackComponent>(); // { }
entity3.GetIncomingLinks<AttackComponent>(); // { 2 }

// deleting a linked entity (3) removes all link components referencing it
entity3.DeleteEntity(); // 1 2
entity2.HasComponent <AttackComponent>(); // false
}

// [TestMethod] TODO
public void Test_AOT_LinkRelations()
{
var store = new EntityStore();

var entity1 = store.CreateEntity(1); // link relations
var entity2 = store.CreateEntity(2); // symbolized as →
var entity3 = store.CreateEntity(3); // 1 2 3

// add a link relation to entity (2) referencing entity (1)
entity2.AddRelation(new AttackRelation { target = entity1 }); // 1 ← 2 3
// get all links added to the entity. O(1)
entity2.GetRelations <AttackRelation>(); // { 1 }
// get all incoming links. O(1)
entity1.GetIncomingLinks<AttackRelation>(); // { 2 }

// add another one. An entity can have multiple link relations
entity2.AddRelation(new AttackRelation { target = entity3 }); // 1 ← 2 → 3
entity2.GetRelations <AttackRelation>(); // { 1, 3 }
entity3.GetIncomingLinks<AttackRelation>(); // { 2 }

// deleting a linked entity (1) removes all link relations referencing it
entity1.DeleteEntity(); // 2 → 3
entity2.GetRelations <AttackRelation>(); // { 3 }

// deleting entity (2) is reflected by incoming links query
entity2.DeleteEntity(); // 3
entity3.GetIncomingLinks<AttackRelation>(); // { }
}

[TestMethod]
public void Test_AOT_Relations()
{
var store = new EntityStore();
var entity = store.CreateEntity();

// add multiple relations of the same component type
entity.AddRelation(new InventoryItem { type = InventoryItemType.Gun, amount = 42 });
entity.AddRelation(new InventoryItem { type = InventoryItemType.Axe, amount = 3 });

// Get all relations added to an entity. O(1)
entity.GetRelations <InventoryItem>(); // { Coin, Axe }

// Get a specific relation from an entity. O(1)
entity.GetRelation <InventoryItem,InventoryItemType>(InventoryItemType.Gun); // {type=Coin, count=42}

// Remove a specific relation from an entity
entity.RemoveRelation<InventoryItem,InventoryItemType>(InventoryItemType.Axe);
entity.GetRelations <InventoryItem>(); // { Coin }
}

struct Player : IIndexedComponent<string> // indexed field type: string
{
public string name;
public string GetIndexedValue() => name; // indexed field
}


private static EntitySchema schemaCreated;
private static readonly object monitor = new object();

Expand All @@ -111,6 +244,13 @@ private static EntitySchema CreateSchema()
aot.RegisterScript<TestScript1>();
aot.RegisterScript<TestScript1>(); // register again

aot.RegisterIndexedComponentClass<Player, string>();
aot.RegisterIndexedComponentStruct<IndexedInt, int>();
aot.RegisterIndexedComponentEntity<AttackComponent>();

aot.RegisterRelation<InventoryItem, InventoryItemType>();


return schemaCreated = aot.CreateSchema();
}
}
Expand Down
Loading

0 comments on commit 19bda4e

Please sign in to comment.