From 6e17a525cdc1d47324d68249506ae62f3d5200c3 Mon Sep 17 00:00:00 2001 From: copygirl Date: Tue, 8 Nov 2022 01:47:59 +0100 Subject: [PATCH] No more separators in EntityPath - EntityPath now operates on an array of UTF8 strings - EntityBuilder.Build, Lookup, ... use separator-less API - Multiple TempAllocators can be acquired nested - Add Identifier.RelationUnsafe and .TargetUnsafe - Identifier(Ref).AsPair returns nullable - Throw when adding > 31 IDs to EntityBuilder - Add static Entity.None field, represents default value - Add Entity.IsSome property, opposite of IsNone - EntityBase.Add(string) now looks up symbol --- src/gaemstone.Client/Resources.cs | 6 +- src/gaemstone/ECS/Entity.cs | 5 +- src/gaemstone/ECS/EntityBase.cs | 6 +- src/gaemstone/ECS/EntityBuilder.cs | 49 ++++- src/gaemstone/ECS/EntityPath.cs | 216 ++++++++++----------- src/gaemstone/ECS/EntityRef.cs | 7 +- src/gaemstone/ECS/Filter.cs | 2 +- src/gaemstone/ECS/Game.cs | 2 +- src/gaemstone/ECS/Identifier.cs | 8 +- src/gaemstone/ECS/IdentifierRef.cs | 5 +- src/gaemstone/ECS/Iterator.cs | 2 +- src/gaemstone/ECS/Observer.cs | 5 +- src/gaemstone/ECS/Query.cs | 2 +- src/gaemstone/ECS/Rule.cs | 2 +- src/gaemstone/ECS/System.cs | 9 +- src/gaemstone/ECS/Universe+Lookup.cs | 4 +- src/gaemstone/ECS/Universe+Modules.cs | 25 ++- src/gaemstone/ECS/Universe.cs | 7 +- src/gaemstone/Flecs/Systems/Monitor.cs | 2 +- src/gaemstone/Flecs/Systems/Rest.cs | 2 +- src/gaemstone/Utility/Allocators.cs | 65 ++++--- src/gaemstone/Utility/CStringExtensions.cs | 17 +- 22 files changed, 252 insertions(+), 196 deletions(-) diff --git a/src/gaemstone.Client/Resources.cs b/src/gaemstone.Client/Resources.cs index 43b3337..cf901d5 100644 --- a/src/gaemstone.Client/Resources.cs +++ b/src/gaemstone.Client/Resources.cs @@ -40,14 +40,12 @@ public static class Resources { if (!path.IsAbsolute) throw new ArgumentException( $"Path '{path}' must be absolute", nameof(path)); - if (path.Depth < 2) throw new ArgumentException( - $"Path '{path}' must have at least a depth of 2", nameof(path)); - if (path[1] != "Resources") throw new ArgumentException( + if (path.Count < 3 || path[1] != "Resources") throw new ArgumentException( $"Path '{path}' must be in the format '/[domain]/Resources/...", nameof(path)); var assembly = Assembly.Load(path[0].ToString()); var builder = new StringBuilder(path[2]); - for (var i = 3; i < path.Depth + 1; i++) + for (var i = 3; i < path.Count; i++) builder.Append('.').Append(path[i]); return (assembly, builder.ToString()); } diff --git a/src/gaemstone/ECS/Entity.cs b/src/gaemstone/ECS/Entity.cs index 40016cd..c2d39c8 100644 --- a/src/gaemstone/ECS/Entity.cs +++ b/src/gaemstone/ECS/Entity.cs @@ -17,11 +17,12 @@ public class RelationAttribute : Attribute { } public readonly struct Entity : IEquatable { + public static readonly Entity None = default; + public readonly ecs_entity_t Value; + public bool IsSome => Value.Data != 0; public bool IsNone => Value.Data == 0; - public Entity ThrowIfNone() => !IsNone ? this - : throw new InvalidOperationException(this + " is none"); public Entity(ecs_entity_t value) => Value = value; diff --git a/src/gaemstone/ECS/EntityBase.cs b/src/gaemstone/ECS/EntityBase.cs index e05883b..24253ef 100644 --- a/src/gaemstone/ECS/EntityBase.cs +++ b/src/gaemstone/ECS/EntityBase.cs @@ -17,19 +17,19 @@ public abstract class EntityBase public abstract TReturn Set(in T value) where T : unmanaged; public abstract TReturn Set(T obj) where T : class; - public TReturn Add(string name) => Add(Universe.LookupOrThrow(name)); + public TReturn Add(string symbol) => Add(Universe.LookupSymbolOrThrow(symbol)); public TReturn Add() => Add(Universe.LookupOrThrow(typeof(T))); public TReturn Add(Entity relation, Entity target) => Add(relation & target); public TReturn Add(Entity target) => Add(Universe.LookupOrThrow(), target); public TReturn Add() => Add(Universe.LookupOrThrow(), Universe.LookupOrThrow()); - public TReturn Remove(string name) => Remove(Universe.LookupOrThrow(name)); + public TReturn Remove(string symbol) => Remove(Universe.LookupSymbolOrThrow(symbol)); public TReturn Remove() => Remove(Universe.LookupOrThrow(typeof(T))); public TReturn Remove(Entity relation, Entity target) => Remove(relation & target); public TReturn Remove(Entity target) => Remove(Universe.LookupOrThrow(), target); public TReturn Remove() => Remove(Universe.LookupOrThrow(), Universe.LookupOrThrow()); - public bool Has(string name) => Has(Universe.LookupOrThrow(name)); + public bool Has(string symbol) => Has(Universe.LookupSymbolOrThrow(symbol)); public bool Has() => Has(Universe.LookupOrThrow(typeof(T))); public bool Has(Entity relation, Entity target) => Has(relation & target); public bool Has(Entity target) => Has(Universe.LookupOrThrow(), target); diff --git a/src/gaemstone/ECS/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs index 37772e4..682ca94 100644 --- a/src/gaemstone/ECS/EntityBuilder.cs +++ b/src/gaemstone/ECS/EntityBuilder.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using gaemstone.Utility; using static flecs_hub.flecs; +using static flecs_hub.flecs.Runtime; namespace gaemstone.ECS; @@ -38,6 +39,7 @@ public class EntityBuilder /// IDs to add to the new or existing entity. private readonly HashSet _toAdd = new(); + private Entity _parent = Entity.None; /// String expression with components to add. public string? Expression { get; } @@ -48,9 +50,23 @@ public class EntityBuilder public EntityBuilder(Universe universe, EntityPath? path = null) { Universe = universe; Path = path; } - public override EntityBuilder Add(Identifier id) { _toAdd.Add(id); return this; } - public override EntityBuilder Remove(Identifier id) => throw new NotSupportedException(); - public override bool Has(Identifier id) => !ecs_id_is_wildcard(id) ? _toAdd.Contains(id) : throw new NotSupportedException(); + public override EntityBuilder Add(Identifier id) + { + // If adding a ChildOf relation, store the parent separately. + if (id.AsPair(Universe) is (EntityRef relation, EntityRef target) && + relation == Universe.ChildOf) { _parent = target; return this; } + + if (_toAdd.Count == 31) throw new NotSupportedException( + $"Must not add more than 31 IDs at once with EntityBuilder"); + _toAdd.Add(id); + + return this; + } + public override EntityBuilder Remove(Identifier id) + => throw new NotSupportedException(); + public override bool Has(Identifier id) + => !ecs_id_is_wildcard(id) ? _toAdd.Contains(id) + : throw new NotSupportedException(); // TODO: Support wildcard. public override T Get() => throw new NotSupportedException(); public override ref T GetRef() => throw new NotSupportedException(); @@ -63,23 +79,36 @@ public class EntityBuilder public override EntityBuilder Set(T obj) { _toSet.Add(e => e.Set(obj)); return this; } + public static CString ETX { get; } = (CString)"\x3"; public unsafe EntityRef Build() { - using var alloc = TempAllocator.Lock(); + var parent = _parent; + + if (Path != null) { + if (parent.IsSome && Path.IsAbsolute) throw new InvalidOperationException( + $"Entity already has parent set (via ChildOf), so path must not be absolute"); + // If path specifies more than just a name, ensure the parent entity exists. + if (Path.Count > 1) parent = EntityPath.EnsureEntityExists(Universe, parent, Path.Parent!); + } + + using var alloc = TempAllocator.Use(); var desc = new ecs_entity_desc_t { - id = ID, - name = (Path != null) ? alloc.AllocateCString(Path.AsSpan(true)) : default, - symbol = alloc.AllocateCString(_symbol), - add_expr = alloc.AllocateCString(Expression), + id = ID, + name = (Path != null) ? alloc.AllocateCString(Path.Name.AsSpan()) : default, + symbol = alloc.AllocateCString(_symbol), + add_expr = alloc.AllocateCString(Expression), use_low_id = UseLowID, - root_sep = EntityPath.SeparatorAsCString, - sep = EntityPath.SeparatorAsCString, + sep = ETX, // TODO: Replace with CStringExtensions.Empty once supported. }; + var add = desc.add; var index = 0; + if (parent.IsSome) add[index++] = Identifier.Pair(Universe.ChildOf, parent); foreach (var id in _toAdd) add[index++] = id; + var entityID = ecs_entity_init(Universe, &desc); var entity = new EntityRef(Universe, new(entityID)); foreach (var action in _toSet) action(entity); + return entity; } } diff --git a/src/gaemstone/ECS/EntityPath.cs b/src/gaemstone/ECS/EntityPath.cs index c92b54a..fc13787 100644 --- a/src/gaemstone/ECS/EntityPath.cs +++ b/src/gaemstone/ECS/EntityPath.cs @@ -1,136 +1,92 @@ using System; using System.Buffers; -using System.Globalization; +using System.Collections.Generic; using System.Linq; using System.Text; -using System.Threading; using gaemstone.Utility; using static flecs_hub.flecs; -using static flecs_hub.flecs.Runtime; namespace gaemstone.ECS; public class EntityPath { - public const int MaxDepth = 32; - public const char Separator = '/'; + private readonly byte[][] _parts; - internal const byte SeparatorAsByte = (byte)Separator; - internal static readonly CString SeparatorAsCString = (CString)Separator.ToString(); - - private static readonly ThreadLocal<(int Start, int End)[]> PartsCache - = new(() => new (int, int)[MaxDepth]); - - private readonly byte[] _bytes; - private readonly (int Start, int End)[] _parts; - - public int Depth => _parts.Length - 1; - public bool IsAbsolute => _bytes[0] == SeparatorAsByte; + public bool IsAbsolute { get; } public bool IsRelative => !IsAbsolute; + public int Count => _parts.Length; - public UTF8View this[int index] { get { - if (index < 0 || index > Depth) - throw new ArgumentOutOfRangeException(nameof(index)); - var (start, end) = _parts[index]; - return new(_bytes.AsSpan()[start..end]); - } } + public UTF8View Name => this[^1]; + public EntityPath? Parent => (Count > 1) ? new(IsAbsolute, _parts[..^1]) : null; - public unsafe EntityPath(byte* pointer) - : this(ArrayFromPointer(pointer)) { } - private static unsafe byte[] ArrayFromPointer(byte* pointer) + public UTF8View this[int index] + => (index >= 0 && index < Count) ? new(_parts[index].AsSpan()[..^1]) + : throw new ArgumentOutOfRangeException(nameof(index)); + + internal EntityPath(bool absolute, params byte[][] parts) { - var length = 0; - while (true) if (pointer[length++] == 0) break; - return new Span(pointer, length).ToArray(); + if (parts.Length == 0) throw new ArgumentException( + "Must have at least one part", nameof(parts)); + IsAbsolute = absolute; + _parts = parts; } - // TODO: public EntityPath(EntityPath @base, params string[] parts) { } public EntityPath(params string[] parts) - : this(ConcatParts(false, parts)) { } + : this(false, parts) { } public EntityPath(bool absolute, params string[] parts) - : this(ConcatParts(absolute, parts)) { } - private static byte[] ConcatParts(bool absolute, string[] parts) + : this(absolute, parts.Select(part => { + ValidateName(part); + var byteCount = Encoding.UTF8.GetByteCount(part); + // Includes NUL character at the end of bytes. + var bytes = new byte[byteCount + 1]; + Encoding.UTF8.GetBytes(part, bytes); + return bytes; + }).ToArray()) { } + + public static EntityPath Parse(string str) { - // number of slashes + NUL - var totalBytes = (parts.Length - 1) + 1; - // If absolute, and parts doesn't already start with a slash, increase length by 1. - var prependSlash = absolute && parts[0].Length > 0 && parts[0][0] != Separator; - if (prependSlash) totalBytes++; - foreach (var part in parts) - totalBytes += Encoding.UTF8.GetByteCount(part); - - var bytes = new byte[totalBytes]; - var index = 0; - foreach (var part in parts) { - if (index > 0 || prependSlash) bytes[index++] = SeparatorAsByte; - index += Encoding.UTF8.GetBytes(part, 0, part.Length, bytes, index); - } - // NUL byte at the end of bytes. - // bytes[index++] = 0; - return bytes; + if (str.Length == 0) throw new ArgumentException( + "String must not be empty", nameof(str)); + var parts = str.Split('/'); + // If string starts with a slash, first part will be empty, so create an absolute path. + return (parts[0].Length == 0) ? new(true, parts[1..]) : new(parts); } - private EntityPath(byte[] bytes) + public static void ValidateName(string name) { - if (bytes.Length <= 1) throw new ArgumentException("Must not be empty"); - if (bytes[^1] != 0) throw new ArgumentException("Must end with a NUL character"); - _bytes = bytes; - - var depth = 0; - var index = 0; - var partStart = 0; - var partsCache = PartsCache.Value!; - - // If path starts with separator, it's an absolute path. Skip first byte. - if (_bytes[0] == SeparatorAsByte) index = partStart = 1; - - // -1 is used here because we don't want to include the NUL character. - while (index < _bytes.Length - 1) { - if (_bytes[index] == SeparatorAsByte) { - // +1 is used here because one more part will follow after the loop. - if (depth + 1 >= MaxDepth) throw new ArgumentException( - $"Must not exceed maximum depth of {MaxDepth}"); - - partsCache[depth++] = (Start: partStart, End: index); - ValidatePart(_bytes.AsSpan()[partStart..index]); - partStart = ++index; - } else { - var slice = _bytes.AsSpan()[index..]; - if (Rune.DecodeFromUtf8(slice, out var rune, out var consumed) != OperationStatus.Done) - throw new ArgumentException("Contains invalid UTF8"); - ValidateRune(rune); - index += consumed; - } - } + if (name.Length == 0) throw new ArgumentException( + "Must not be empty"); - partsCache[depth] = (Start: partStart, End: index); - ValidatePart(_bytes.AsSpan()[partStart..^1]); - - // Copy parts from the thread local cache - this avoids unnecessary resizing. - _parts = partsCache[..(depth + 1)]; - } - - private static void ValidatePart(ReadOnlySpan part) - { - if (part.Length == 0) throw new ArgumentException( - "Must not contain empty parts"); // NOTE: This is a hopefully straightforward way to also prevent "." // and ".." to be part of paths which may access the file system. - if (part[0] == (byte)'.') throw new ArgumentException( - "Must not contain parts that start with a dot"); + if (name[0] == '.') throw new ArgumentException( + "Must not begin with a dot"); + + foreach (var chr in name) if (char.IsControl(chr)) + throw new ArgumentException("Must not contain contol characters"); } - private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' }; - private static readonly UnicodeCategory[] _validCategories = { - UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter, - UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber }; + // private static readonly Rune[] _validRunes = { (Rune)'-', (Rune)'.', (Rune)'_' }; + // private static readonly UnicodeCategory[] _validCategories = { + // UnicodeCategory.LowercaseLetter, UnicodeCategory.UppercaseLetter, + // UnicodeCategory.OtherLetter, UnicodeCategory.DecimalDigitNumber }; + + // private static void ValidateRune(Rune rune) + // { + // if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune))) + // throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); + // } - private static void ValidateRune(Rune rune) + public override string ToString() { - if (!_validRunes.Contains(rune) && !_validCategories.Contains(Rune.GetUnicodeCategory(rune))) - throw new ArgumentException($"Must not contain {Rune.GetUnicodeCategory(rune)} character"); + var builder = new StringBuilder(); + if (IsAbsolute) builder.Append('/'); + foreach (var part in this) builder.Append(part).Append('/'); + return builder.ToString(0, builder.Length - 1); } + public static implicit operator EntityPath(string path) => Parse(path); + public Enumerator GetEnumerator() => new(this); public ref struct Enumerator { @@ -138,32 +94,58 @@ public class EntityPath private int index = -1; public UTF8View Current => _path[index]; internal Enumerator(EntityPath path) => _path = path; - public bool MoveNext() => (++index >= _path.Depth); + public bool MoveNext() => (++index < _path.Count); } - public ReadOnlySpan AsSpan(bool includeNul = false) - => includeNul ? _bytes.AsSpan() : _bytes.AsSpan()[..^1]; - public override string ToString() => Encoding.UTF8.GetString(AsSpan()); - public static implicit operator string(EntityPath view) => view.ToString(); - public static implicit operator EntityPath(string str) => new(str); + internal static unsafe Entity Lookup(Universe universe, Entity parent, EntityPath path) + { + // If path is absolute, ignore parent and start at root. + if (path.IsAbsolute) parent = default; + // Otherwise, if no parent is specified, use the current scope. + else if (parent.IsNone) parent = new(ecs_get_scope(universe)); + + foreach (var part in path) + fixed (byte* ptr = part.AsSpan()) + if ((parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone) + return Entity.None; + + return parent; + } + + /// Used by . + internal static unsafe Entity EnsureEntityExists( + Universe universe, Entity parent, EntityPath path) + { + // If no parent is specified and path is relative, use the current scope. + if (parent.IsNone && path.IsRelative) parent = new(ecs_get_scope(universe)); + + var skipLookup = parent.IsNone; + foreach (var part in path) + fixed (byte* ptr = part.AsSpan()) + if (skipLookup || (parent = new(ecs_lookup_child(universe, parent, ptr))).IsNone) { + var desc = new ecs_entity_desc_t { name = ptr }; + if (parent.IsSome) desc.add[0] = Identifier.Pair(universe.ChildOf, parent); + parent = new(ecs_entity_init(universe, &desc)); + skipLookup = true; + } + + return parent; + } } public static class EntityPathExtensions { public static unsafe EntityPath GetFullPath(this EntityRef entity) { - var cStr = ecs_get_path_w_sep(entity.Universe, default, entity, - EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString); - try { return new((byte*)(nint)cStr); } - finally { cStr.FlecsFree(); } - } + var current = (Entity)entity; + var parts = new List(32); - public static unsafe Entity Lookup(Universe universe, Entity parent, EntityPath path) - { - using var alloc = TempAllocator.Lock(); - return new(ecs_lookup_path_w_sep(universe, parent, alloc.AllocateCString(path.AsSpan(true)), - EntityPath.SeparatorAsCString, EntityPath.SeparatorAsCString, true)); + do { parts.Add(ecs_get_name(entity.Universe, current).FlecsToBytes()!); } + while ((current = new(ecs_get_target(entity.Universe, current, EcsChildOf, 0))).IsSome); + + parts.Reverse(); + return new(true, parts.ToArray()); } } @@ -176,8 +158,8 @@ public readonly ref struct UTF8View public int Length => _bytes.Length; public byte this[int index] => _bytes[index]; + public ReadOnlySpan AsSpan() => _bytes; public override string ToString() => Encoding.UTF8.GetString(_bytes); - public static implicit operator ReadOnlySpan(UTF8View view) => view._bytes; public static implicit operator string(UTF8View view) => view.ToString(); public Enumerator GetEnumerator() => new(_bytes); diff --git a/src/gaemstone/ECS/EntityRef.cs b/src/gaemstone/ECS/EntityRef.cs index f387d30..302298b 100644 --- a/src/gaemstone/ECS/EntityRef.cs +++ b/src/gaemstone/ECS/EntityRef.cs @@ -19,11 +19,11 @@ public unsafe sealed class EntityRef public string? Name { get => ecs_get_name(Universe, this).FlecsToString()!; - set { using var alloc = TempAllocator.Lock(); ecs_set_name(Universe, this, alloc.AllocateCString(value)); } + set { using var alloc = TempAllocator.Use(); ecs_set_name(Universe, this, alloc.AllocateCString(value)); } } public string? Symbol { get => ecs_get_symbol(Universe, this).FlecsToString()!; - set { using var alloc = TempAllocator.Lock(); ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); } + set { using var alloc = TempAllocator.Use(); ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); } } // TODO: public IEnumerable Children => ... @@ -36,7 +36,8 @@ public unsafe sealed class EntityRef $"The entity {entity} is not valid"); } - public void Delete() => ecs_delete(Universe, this); + public void Delete() + => ecs_delete(Universe, this); public EntityBuilder NewChild(EntityPath? path = null) => Universe.New(EnsureRelativePath(path)).ChildOf(this); diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs index f34b712..b9c8f9f 100644 --- a/src/gaemstone/ECS/Filter.cs +++ b/src/gaemstone/ECS/Filter.cs @@ -18,7 +18,7 @@ public unsafe sealed class Filter public Filter(Universe universe, FilterDesc desc) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); Universe = universe; Handle = ecs_filter_init(universe, &flecsDesc); diff --git a/src/gaemstone/ECS/Game.cs b/src/gaemstone/ECS/Game.cs index 684f9c3..e3cfd3b 100644 --- a/src/gaemstone/ECS/Game.cs +++ b/src/gaemstone/ECS/Game.cs @@ -6,7 +6,7 @@ namespace gaemstone.ECS; /// Entity for storing global game state and configuration. /// Parameters can use to source this entity. /// -[Entity] +[Entity, Tag] public struct Game { } /// Short for [Source(typeof(Game))]. diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs index 5efcb14..70e5aa1 100644 --- a/src/gaemstone/ECS/Identifier.cs +++ b/src/gaemstone/ECS/Identifier.cs @@ -8,20 +8,24 @@ public readonly struct Identifier { public readonly ecs_id_t Value; - public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK); public bool IsPair => ecs_id_is_pair(Value); public bool IsWildcard => ecs_id_is_wildcard(Value); + public Entity RelationUnsafe => new(new() { Data = (Value & ECS_COMPONENT_MASK) >> 32 }); + public Entity TargetUnsafe => new(new() { Data = Value & ECS_ENTITY_MASK }); + public IdentifierFlags Flags => (IdentifierFlags)(Value & ECS_ID_FLAGS_MASK); + public Identifier(ecs_id_t value) => Value = value; public static Identifier Combine(IdentifierFlags flags, Identifier id) => new((ulong)flags | id.Value); + public static Identifier Pair(Entity relation, Entity target) => Combine(IdentifierFlags.Pair, new( (relation.Value.Data << 32) | (target.Value.Data & ECS_ENTITY_MASK))); - public (EntityRef Relation, EntityRef Target) AsPair(Universe universe) + public (EntityRef Relation, EntityRef Target)? AsPair(Universe universe) => new IdentifierRef(universe, this).AsPair(); public bool Equals(Identifier other) => Value.Data == other.Value.Data; diff --git a/src/gaemstone/ECS/IdentifierRef.cs b/src/gaemstone/ECS/IdentifierRef.cs index 29f43b5..dcac415 100644 --- a/src/gaemstone/ECS/IdentifierRef.cs +++ b/src/gaemstone/ECS/IdentifierRef.cs @@ -25,9 +25,8 @@ public unsafe class IdentifierRef public static IdentifierRef Pair(Entity relation, EntityRef target) => new(target.Universe, Identifier.Pair(relation, target)); - public (EntityRef Relation, EntityRef Target) AsPair() - => (Universe.LookupOrThrow(new Entity(new() { Data = (ID.Value & ECS_COMPONENT_MASK) >> 32 })), - Universe.LookupOrThrow(new Entity(new() { Data = ID.Value & ECS_ENTITY_MASK }))); + public (EntityRef Relation, EntityRef Target)? AsPair() + => IsPair ? (Universe.LookupOrThrow(ID.RelationUnsafe), Universe.LookupOrThrow(ID.TargetUnsafe)) : null; public bool Equals(IdentifierRef? other) => (other is not null) && Universe == other.Universe && ID == other.ID; public override bool Equals(object? obj) => Equals(obj as IdentifierRef); diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs index fe0f3ea..3e5065c 100644 --- a/src/gaemstone/ECS/Iterator.cs +++ b/src/gaemstone/ECS/Iterator.cs @@ -25,7 +25,7 @@ public unsafe partial class Iterator public static Iterator FromTerm(Universe universe, Term term) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var flecsTerm = term.ToFlecs(alloc); var flecsIter = ecs_term_iter(universe, &flecsTerm); return new(universe, IteratorType.Term, flecsIter); diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index b9b3759..3bf94b0 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -21,11 +21,10 @@ public static class ObserverExtensions public static unsafe EntityRef RegisterObserver(this Universe universe, FilterDesc filter, Entity @event, Action callback) { - var entity = universe.New((filter.Name != null) ? new(filter.Name) : null).Build(); - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var desc = new ecs_observer_desc_t { filter = filter.ToFlecs(alloc), - entity = entity, + entity = universe.New((filter.Name != null) ? new(filter.Name) : null).Build(), binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), callback = new() { Data = new() { Pointer = &SystemExtensions.Callback } }, }; diff --git a/src/gaemstone/ECS/Query.cs b/src/gaemstone/ECS/Query.cs index 482a408..54232a1 100644 --- a/src/gaemstone/ECS/Query.cs +++ b/src/gaemstone/ECS/Query.cs @@ -15,7 +15,7 @@ public unsafe sealed class Query public Query(Universe universe, QueryDesc desc) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); Universe = universe; Handle = ecs_query_init(universe, &flecsDesc); diff --git a/src/gaemstone/ECS/Rule.cs b/src/gaemstone/ECS/Rule.cs index bcc430a..7c91a42 100644 --- a/src/gaemstone/ECS/Rule.cs +++ b/src/gaemstone/ECS/Rule.cs @@ -15,7 +15,7 @@ public unsafe sealed class Rule public Rule(Universe universe, FilterDesc desc) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var flecsDesc = desc.ToFlecs(alloc); Universe = universe; Handle = ecs_rule_init(universe, &flecsDesc); diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 5a18fc6..bd888ec 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -25,14 +25,11 @@ public static class SystemExtensions public static unsafe EntityRef RegisterSystem(this Universe universe, QueryDesc query, Entity phase, Action callback) { - var entity = universe.New((query.Name != null) ? new(query.Name) : null) - .Add(phase) - .Add(phase) - .Build(); - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); var desc = new ecs_system_desc_t { query = query.ToFlecs(alloc), - entity = entity, + entity = universe.New((query.Name != null) ? new(query.Name) : null) + .Add(phase).Add(phase).Build(), binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), callback = new() { Data = new() { Pointer = &Callback } }, }; diff --git a/src/gaemstone/ECS/Universe+Lookup.cs b/src/gaemstone/ECS/Universe+Lookup.cs index 798a812..ef5a0bc 100644 --- a/src/gaemstone/ECS/Universe+Lookup.cs +++ b/src/gaemstone/ECS/Universe+Lookup.cs @@ -30,10 +30,10 @@ public unsafe partial class Universe public EntityRef? Lookup(EntityPath path) => Lookup(default, path); public EntityRef? Lookup(Entity parent, EntityPath path) - => GetOrNull(EntityPathExtensions.Lookup(this, parent, path)); + => GetOrNull(EntityPath.Lookup(this, parent, path)); public EntityRef? LookupSymbol(string symbol) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); return GetOrNull(new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false))); } diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs index f31a298..208a090 100644 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using gaemstone.Utility; using static gaemstone.Flecs.Core; @@ -34,8 +35,9 @@ public class ModuleManager if (attr.Path == null) throw new Exception( $"Existing module {type} must have ModuleAttribute.Name set"); - var entity = Universe.Lookup(new EntityPath(true, attr.Path)) ?? throw new Exception( - $"Existing module {type} with name '{attr.Path}' not found"); + var path = new EntityPath(true, attr.Path); + var entity = Universe.Lookup(path) ?? throw new Exception( + $"Existing module {type} with name '{path}' not found"); // This implementation is pretty naive. It simply gets all nested // types which are tagged with [Entity] attribute or a subtype @@ -88,11 +90,13 @@ public class ModuleManager // If path is not specified in the attribute, return the type's name. if (attr.Path == null) { var assemblyName = type.Assembly.GetName().Name!; + if (!type.FullName!.StartsWith(assemblyName + '.')) throw new InvalidOperationException( - $"Module {type} must be defined in namespace {assemblyName}"); - // Strip assembly name from FullName and replace dots in namespace with path separators. - var path = type.FullName![(assemblyName.Length + 1)..].Replace('.', EntityPath.Separator); - return new(true, assemblyName, path); + $"Module {type} must be defined under namespace {assemblyName}"); + var fullNameWithoutAssembly = type.FullName![(assemblyName.Length + 1)..]; + + var parts = fullNameWithoutAssembly.Split('.'); + return new(true, parts.Prepend(assemblyName).ToArray()); } var fullPath = new EntityPath(true, attr.Path); @@ -157,8 +161,13 @@ internal class ModuleInfo { foreach (var type in Type.GetNestedTypes()) { if (type.Get() is not EntityAttribute attr) continue; - if (attr.Name?.Contains(EntityPath.Separator) == true) throw new Exception( - $"{type} must not contain '{EntityPath.Separator}'"); + + if (attr.Name != null) { + try { EntityPath.ValidateName(attr.Name); } + catch (Exception ex) { throw new Exception( + $"{type} has invalid entity name '{attr.Name}: {ex.Message}'", ex); } + } + var name = attr.Name ?? type.Name; var entity = Entity.NewChild(name).Symbol(name); switch (attr) { diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs index 494a63a..b1513a6 100644 --- a/src/gaemstone/ECS/Universe.cs +++ b/src/gaemstone/ECS/Universe.cs @@ -8,6 +8,9 @@ public unsafe partial class Universe public ecs_world_t* Handle { get; } public ModuleManager Modules { get; } + // flecs built-ins that are important to internals. + public EntityRef ChildOf { get; } + public bool IsDeferred => ecs_is_deferred(this); public Universe(params string[] args) @@ -19,7 +22,9 @@ public unsafe partial class Universe Modules.Register(typeof(Flecs.ObserverEvent)); Modules.Register(typeof(Flecs.SystemPhase)); - New("Game").Symbol("Game").Build().CreateLookup(); + ChildOf = LookupOrThrow(); + + New("Game").Symbol("Game").Build().CreateLookup().Add(); } public EntityBuilder New(EntityPath? path = null) diff --git a/src/gaemstone/Flecs/Systems/Monitor.cs b/src/gaemstone/Flecs/Systems/Monitor.cs index bf4b5d0..865b1b5 100644 --- a/src/gaemstone/Flecs/Systems/Monitor.cs +++ b/src/gaemstone/Flecs/Systems/Monitor.cs @@ -11,7 +11,7 @@ public unsafe class Monitor { public void Initialize(EntityRef module) { - using var alloc = TempAllocator.Lock(); + using var alloc = TempAllocator.Use(); ecs_import_c(module.Universe, new() { Data = new() { Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor")); } diff --git a/src/gaemstone/Flecs/Systems/Rest.cs b/src/gaemstone/Flecs/Systems/Rest.cs index 8b39ac6..53722d3 100644 --- a/src/gaemstone/Flecs/Systems/Rest.cs +++ b/src/gaemstone/Flecs/Systems/Rest.cs @@ -11,7 +11,7 @@ public unsafe class Rest { public void Initialize(EntityRef module) { - using (var alloc = TempAllocator.Lock()) + using (var alloc = TempAllocator.Use()) ecs_import_c(module.Universe, new() { Data = new() { Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest")); diff --git a/src/gaemstone/Utility/Allocators.cs b/src/gaemstone/Utility/Allocators.cs index 5dd009d..cff406a 100644 --- a/src/gaemstone/Utility/Allocators.cs +++ b/src/gaemstone/Utility/Allocators.cs @@ -21,7 +21,7 @@ public unsafe static class AllocatorExtensions => allocator.Free((nint)Unsafe.AsPointer(ref span[0])); public static Span AllocateCopy(this IAllocator allocator, ReadOnlySpan orig) where T : unmanaged - { var span = allocator.Allocate(orig.Length); orig.CopyTo(span); return span; } + { var copy = allocator.Allocate(orig.Length); orig.CopyTo(copy); return copy; } public static ref T Allocate(this IAllocator allocator) where T : unmanaged => ref Unsafe.AsRef((void*)allocator.Allocate(sizeof(T))); @@ -34,35 +34,48 @@ public unsafe static class AllocatorExtensions var bytes = Encoding.UTF8.GetByteCount(value); var span = allocator.Allocate(bytes + 1); Encoding.UTF8.GetBytes(value, span); - span[^1] = 0; + span[^1] = 0; // In case the allocated span is not cleared. return new((nint)Unsafe.AsPointer(ref span[0])); } public static CString AllocateCString(this IAllocator allocator, ReadOnlySpan utf8) - => new((nint)Unsafe.AsPointer(ref allocator.AllocateCopy(utf8)[0])); + { + var copy = allocator.Allocate(utf8.Length + 1); + utf8.CopyTo(copy); + copy[^1] = 0; // In case the allocated span is not cleared. + return new((nint)Unsafe.AsPointer(ref copy[0])); + } } -public sealed class TempAllocator - : IAllocator - , IDisposable +public static class TempAllocator { public const int Capacity = 1024 * 1024; // 1 MB - private static readonly ThreadLocal _tempAllocator = new(() => new()); - public static TempAllocator Lock() + private static readonly ThreadLocal _allocator + = new(() => new(Capacity)); + + public static ResetOnDispose Use() { - var allocator = _tempAllocator.Value!; - if (allocator._isInUse) throw new InvalidOperationException( - "This thread's TempAllocator is already in use. Previous caller to Lock() must first call Dispose()."); - allocator._isInUse = true; - return allocator; + var allocator = _allocator.Value!; + return new(allocator, allocator.Used); } - private readonly ArenaAllocator _allocator = new(Capacity); - private bool _isInUse = false; + public sealed class ResetOnDispose + : IAllocator + , IDisposable + { + private readonly ArenaAllocator _allocator = new(Capacity); + private readonly int _start; - public nint Allocate(int byteCount) => _allocator.Allocate(byteCount); - public void Free(nint pointer) { /* Do nothing. */ } - public void Dispose() { _allocator.Reset(); _isInUse = false; } + public ResetOnDispose(ArenaAllocator allocator, int start) + { _allocator = allocator; _start = start; } + + // IAllocator implementation + public nint Allocate(int byteCount) => _allocator.Allocate(byteCount); + public void Free(nint pointer) { /* Do nothing. */ } + + // IDisposable implementation + public void Dispose() => _allocator.Reset(_start); + } } public class GlobalHeapAllocator @@ -85,14 +98,14 @@ public sealed class ArenaAllocator public ArenaAllocator(int capacity) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); - _buffer = Marshal.AllocHGlobal(capacity); + _buffer = Marshal.AllocHGlobal(capacity); Capacity = capacity; } public void Dispose() { Marshal.FreeHGlobal(_buffer); - _buffer = default; + _buffer = default; Capacity = 0; } @@ -109,8 +122,12 @@ public sealed class ArenaAllocator public void Free(nint pointer) { /* Do nothing. */ } - public void Reset() - => Used = 0; + public unsafe void Reset(int start = 0) + { + if (start > Used) throw new ArgumentOutOfRangeException(nameof(start)); + new Span((void*)(_buffer + start), Used - start).Clear(); + Used = start; + } } public sealed class RingAllocator @@ -124,14 +141,14 @@ public sealed class RingAllocator public RingAllocator(int capacity) { if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity)); - _buffer = Marshal.AllocHGlobal(capacity); + _buffer = Marshal.AllocHGlobal(capacity); Capacity = capacity; } public void Dispose() { Marshal.FreeHGlobal(_buffer); - _buffer = default; + _buffer = default; Capacity = 0; } diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs index acb0ed1..acd63ac 100644 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Runtime.InteropServices; using static flecs_hub.flecs; using static flecs_hub.flecs.Runtime; @@ -6,6 +7,20 @@ namespace gaemstone.Utility; public unsafe static class CStringExtensions { + public static CString Empty { get; } = (CString)""; + + public static unsafe byte[]? FlecsToBytes(this CString str) + { + if (str.IsNull) return null; + var length = 0; + var pointer = (byte*)(nint)str; + while (true) if (pointer[length++] == 0) break; + return new Span(pointer, length).ToArray(); + } + + public static byte[]? FlecsToBytesAndFree(this CString str) + { var result = str.FlecsToBytes(); str.FlecsFree(); return result; } + public static string? FlecsToString(this CString str) => Marshal.PtrToStringUTF8(str); @@ -13,5 +28,5 @@ public unsafe static class CStringExtensions { var result = str.FlecsToString(); str.FlecsFree(); return result; } public static void FlecsFree(this CString str) - => ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); + { if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } }