diff --git a/README.md b/README.md
index 02fe46e..0748b2f 100644
--- a/README.md
+++ b/README.md
@@ -104,7 +104,7 @@ git clone --recurse-submodules https://git.mcft.net/copygirl/gaemstone.ECS.git
git submodule add https://git.mcft.net/copygirl/gaemstone.ECS.git
# To add a reference to this library to your .NET project:
-dotnet add reference gaemstone.ECS/gaemstone.ECS.csproj
+dotnet add reference gaemstone.ECS/src/gaemstone.ECS/gaemstone.ECS.csproj
# To generate flecs-cs' bindings:
./gaemstone.ECS/src/flecs-cs/library.sh
diff --git a/gaemstone.ECS.csproj b/gaemstone.ECS.csproj
deleted file mode 100644
index d9995da..0000000
--- a/gaemstone.ECS.csproj
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- net7.0
- true
- disable
- enable
-
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/gaemstone.ECS/EntityBuilder.cs b/src/gaemstone.ECS/EntityBuilder.cs
index 5890379..7cd8e50 100644
--- a/src/gaemstone.ECS/EntityBuilder.cs
+++ b/src/gaemstone.ECS/EntityBuilder.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/EntityPath.cs b/src/gaemstone.ECS/EntityPath.cs
index bcbd57d..0940fee 100644
--- a/src/gaemstone.ECS/EntityPath.cs
+++ b/src/gaemstone.ECS/EntityPath.cs
@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/EntityRef.cs b/src/gaemstone.ECS/EntityRef.cs
index 9dc0e48..01d4152 100644
--- a/src/gaemstone.ECS/EntityRef.cs
+++ b/src/gaemstone.ECS/EntityRef.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/EntityType.cs b/src/gaemstone.ECS/EntityType.cs
index fb34df3..39e490a 100644
--- a/src/gaemstone.ECS/EntityType.cs
+++ b/src/gaemstone.ECS/EntityType.cs
@@ -1,6 +1,6 @@
using System.Collections;
using System.Collections.Generic;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Filter.cs b/src/gaemstone.ECS/Filter.cs
index 9437446..91288a1 100644
--- a/src/gaemstone.ECS/Filter.cs
+++ b/src/gaemstone.ECS/Filter.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/IdRef.cs b/src/gaemstone.ECS/IdRef.cs
index 1099c49..9ee5137 100644
--- a/src/gaemstone.ECS/IdRef.cs
+++ b/src/gaemstone.ECS/IdRef.cs
@@ -1,5 +1,5 @@
using System;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Iterator.cs b/src/gaemstone.ECS/Iterator.cs
index b35d818..09886f9 100644
--- a/src/gaemstone.ECS/Iterator.cs
+++ b/src/gaemstone.ECS/Iterator.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Observer.cs b/src/gaemstone.ECS/Observer.cs
index 849ab42..80a4c52 100644
--- a/src/gaemstone.ECS/Observer.cs
+++ b/src/gaemstone.ECS/Observer.cs
@@ -1,6 +1,6 @@
using System;
using System.Runtime.InteropServices;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Query.cs b/src/gaemstone.ECS/Query.cs
index a215a74..3a421ea 100644
--- a/src/gaemstone.ECS/Query.cs
+++ b/src/gaemstone.ECS/Query.cs
@@ -1,5 +1,5 @@
using System;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Rule.cs b/src/gaemstone.ECS/Rule.cs
index 142905d..6228f8d 100644
--- a/src/gaemstone.ECS/Rule.cs
+++ b/src/gaemstone.ECS/Rule.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/System.cs b/src/gaemstone.ECS/System.cs
index 4497ba1..4275a59 100644
--- a/src/gaemstone.ECS/System.cs
+++ b/src/gaemstone.ECS/System.cs
@@ -1,6 +1,6 @@
using System;
using System.Runtime.InteropServices;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Term.cs b/src/gaemstone.ECS/Term.cs
index bf79298..b31a3a2 100644
--- a/src/gaemstone.ECS/Term.cs
+++ b/src/gaemstone.ECS/Term.cs
@@ -1,5 +1,5 @@
using System;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/Utility/Allocators.cs b/src/gaemstone.ECS/Utility/Allocators.cs
new file mode 100644
index 0000000..526a89b
--- /dev/null
+++ b/src/gaemstone.ECS/Utility/Allocators.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using static flecs_hub.flecs.Runtime;
+
+namespace gaemstone.ECS.Utility;
+
+public interface IAllocator
+{
+ nint Allocate(int byteCount);
+ void Free(nint pointer);
+}
+
+public unsafe static class AllocatorExtensions
+{
+ public static Span Allocate(this IAllocator allocator, int count) where T : unmanaged
+ => new((void*)allocator.Allocate(sizeof(T) * count), count);
+ public static void Free(this IAllocator allocator, Span span) where T : unmanaged
+ => allocator.Free((nint)Unsafe.AsPointer(ref span[0]));
+
+ public static Span AllocateCopy(this IAllocator allocator, ReadOnlySpan orig) where T : unmanaged
+ { 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)));
+ public static void Free(this IAllocator allocator, ref T value) where T : unmanaged
+ => allocator.Free((nint)Unsafe.AsPointer(ref value));
+
+ public static CString AllocateCString(this IAllocator allocator, string? value)
+ {
+ if (value == null) return default;
+ var bytes = Encoding.UTF8.GetByteCount(value);
+ var span = allocator.Allocate(bytes + 1);
+ Encoding.UTF8.GetBytes(value, span);
+ 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)
+ {
+ 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 static class TempAllocator
+{
+ public const int Capacity = 1024 * 1024; // 1 MB
+ private static readonly ThreadLocal _allocator
+ = new(() => new(Capacity));
+
+ public static ResetOnDispose Use()
+ {
+ var allocator = _allocator.Value!;
+ return new(allocator, allocator.Used);
+ }
+
+ public sealed class ResetOnDispose
+ : IAllocator
+ , IDisposable
+ {
+ private readonly ArenaAllocator _allocator;
+ private readonly int _start;
+
+ public ResetOnDispose(ArenaAllocator allocator, int start)
+ { _allocator = allocator; _start = start; }
+
+ // TODO: Print warning in finalizer if Dispose wasn't called manually.
+
+ // 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
+ : IAllocator
+{
+ public static GlobalHeapAllocator Instance { get; } = new();
+
+ public nint Allocate(int byteCount)
+ => Marshal.AllocHGlobal(byteCount);
+ public void Free(nint pointer)
+ => Marshal.FreeHGlobal(pointer);
+}
+
+public class ArenaAllocator
+ : IAllocator
+ , IDisposable
+{
+ private nint _buffer;
+ public int Capacity { get; private set; }
+ public int Used { get; private set; } = 0;
+
+ public ArenaAllocator(int capacity)
+ {
+ if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
+ _buffer = Marshal.AllocHGlobal(capacity);
+ Capacity = capacity;
+ }
+
+ public void Dispose()
+ {
+ Marshal.FreeHGlobal(_buffer);
+ _buffer = default;
+ Capacity = 0;
+ }
+
+ public nint Allocate(int byteCount)
+ {
+ if (_buffer == default) throw new ObjectDisposedException(nameof(ArenaAllocator));
+ if (Used + byteCount > Capacity) throw new InvalidOperationException(
+ $"Cannot allocate more than {Capacity} bytes with this {nameof(ArenaAllocator)}");
+ var ptr = _buffer + Used;
+ Used += byteCount;
+ return ptr;
+ }
+
+ public void Free(nint pointer)
+ { /* Do nothing. */ }
+
+ 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 class RingAllocator
+ : IAllocator
+ , IDisposable
+{
+ private nint _buffer;
+ private int _current = 0;
+ public int Capacity { get; private set; }
+
+ public RingAllocator(int capacity)
+ {
+ if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
+ _buffer = Marshal.AllocHGlobal(capacity);
+ Capacity = capacity;
+ }
+
+ public void Dispose()
+ {
+ Marshal.FreeHGlobal(_buffer);
+ _buffer = default;
+ Capacity = 0;
+ }
+
+ public nint Allocate(int byteCount)
+ {
+ if (_buffer == default) throw new ObjectDisposedException(nameof(RingAllocator));
+ if (byteCount > Capacity) throw new ArgumentOutOfRangeException(nameof(byteCount));
+ if (_current + byteCount > Capacity) _current = 0;
+ var ptr = _buffer + _current;
+ _current += byteCount;
+ return ptr;
+ }
+
+ public void Free(nint pointer)
+ { /* Do nothing. */ }
+}
diff --git a/src/gaemstone.ECS/Utility/CStringExtensions.cs b/src/gaemstone.ECS/Utility/CStringExtensions.cs
new file mode 100644
index 0000000..96c691a
--- /dev/null
+++ b/src/gaemstone.ECS/Utility/CStringExtensions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Runtime.InteropServices;
+using static flecs_hub.flecs;
+using static flecs_hub.flecs.Runtime;
+
+namespace gaemstone.ECS.Utility;
+
+public unsafe static class CStringExtensions
+{
+ public static CString Empty { get; } = (CString)"";
+ public static CString ETX { get; } = (CString)"\x3"; // FIXME: Temporary, until flecs supports Empty.
+
+ public static unsafe byte[]? FlecsToBytes(this CString str)
+ {
+ if (str.IsNull) return null;
+ var pointer = (byte*)(nint)str;
+ // Find length of the string by locating the NUL character.
+ var length = 0; while (true) if (pointer[length++] == 0) break;
+ // Create span over the region, NUL included, copy it into an array.
+ 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);
+
+ public static string? FlecsToStringAndFree(this CString str)
+ { var result = str.FlecsToString(); str.FlecsFree(); return result; }
+
+ public static void FlecsFree(this CString str)
+ { if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); }
+}
diff --git a/src/gaemstone.ECS/Utility/CallbackContextHelper.cs b/src/gaemstone.ECS/Utility/CallbackContextHelper.cs
new file mode 100644
index 0000000..9c838bb
--- /dev/null
+++ b/src/gaemstone.ECS/Utility/CallbackContextHelper.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace gaemstone.ECS.Utility;
+
+public static class CallbackContextHelper
+{
+ private static readonly Dictionary _contexts = new();
+ private static nint _counter = 0;
+
+ public static nint Create(T context) where T : notnull
+ {
+ lock (_contexts) {
+ var id = _counter++;
+ _contexts.Add(id, context);
+ return id;
+ }
+ }
+
+ public static T Get(nint id)
+ {
+ lock (_contexts)
+ return (T)_contexts[id];
+ }
+
+ public static void Free(nint id)
+ {
+ lock (_contexts)
+ _contexts.Remove(id);
+ }
+}
diff --git a/src/gaemstone.ECS/Utility/FlecsException.cs b/src/gaemstone.ECS/Utility/FlecsException.cs
new file mode 100644
index 0000000..13f2868
--- /dev/null
+++ b/src/gaemstone.ECS/Utility/FlecsException.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace gaemstone.ECS.Utility;
+
+public class FlecsException
+ : Exception
+{
+ public FlecsException() : base() { }
+ public FlecsException(string message) : base(message) { }
+}
+
+public class FlecsAbortException
+ : FlecsException
+{
+ private readonly string _stackTrace = new StackTrace(2, true).ToString();
+ public override string? StackTrace => _stackTrace;
+
+ private FlecsAbortException()
+ : base("Abort was called by flecs") { }
+
+ [UnmanagedCallersOnly]
+ internal static void Callback()
+ => throw new FlecsAbortException();
+}
diff --git a/src/gaemstone.ECS/Utility/SpanExtensions.cs b/src/gaemstone.ECS/Utility/SpanExtensions.cs
new file mode 100644
index 0000000..7636a6a
--- /dev/null
+++ b/src/gaemstone.ECS/Utility/SpanExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace gaemstone.ECS.Utility;
+
+public static class SpanExtensions
+{
+ public static T? GetOrNull(this Span span, int index)
+ where T : struct => GetOrNull((ReadOnlySpan)span, index);
+ public static T? GetOrNull(this ReadOnlySpan span, int index)
+ where T : struct => (index >= 0 && index < span.Length) ? span[index] : null;
+
+
+ public static ReadOnlySpanSplitEnumerator Split(this ReadOnlySpan span, T splitOn)
+ where T : IEquatable => new(span, splitOn);
+
+ public ref struct ReadOnlySpanSplitEnumerator
+ where T : IEquatable
+ {
+ private readonly ReadOnlySpan _span;
+ private readonly T _splitOn;
+ private int _index = -1;
+ private int _end = -1;
+
+ public ReadOnlySpanSplitEnumerator(ReadOnlySpan span, T splitOn)
+ { _span = span; _splitOn = splitOn; }
+
+ public ReadOnlySpanSplitEnumerator GetEnumerator() => this;
+
+ public ReadOnlySpan Current
+ => (_end >= 0) && (_end <= _span.Length)
+ ? _span[_index.._end] : throw new InvalidOperationException();
+
+ public bool MoveNext()
+ {
+ if (_end == _span.Length) return false;
+ _end++; _index = _end;
+ while (_end < _span.Length && !_span[_end].Equals(_splitOn)) _end++;
+ return true;
+ }
+ }
+}
diff --git a/src/gaemstone.ECS/World+Lookup.cs b/src/gaemstone.ECS/World+Lookup.cs
index 088bdb5..1357ff7 100644
--- a/src/gaemstone.ECS/World+Lookup.cs
+++ b/src/gaemstone.ECS/World+Lookup.cs
@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/World.cs b/src/gaemstone.ECS/World.cs
index c52d731..64dd4e8 100644
--- a/src/gaemstone.ECS/World.cs
+++ b/src/gaemstone.ECS/World.cs
@@ -1,5 +1,5 @@
using System;
-using gaemstone.Utility;
+using gaemstone.ECS.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;
diff --git a/src/gaemstone.ECS/gaemstone.ECS.csproj b/src/gaemstone.ECS/gaemstone.ECS.csproj
new file mode 100644
index 0000000..d1b876c
--- /dev/null
+++ b/src/gaemstone.ECS/gaemstone.ECS.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net7.0
+ true
+ disable
+ enable
+
+
+
+
+
+
+