Add EntityPath class, wraps a UTF8 string in-memory which represents an entity path within flecs' hierarchy. - Path separator is now forward-slash instead of dot - Only unicode letters, numbers, dot, dash and underscore are valid characters to use for an entity name Add IAllocator interface and implementations, which handle unmanaged allocations (i.e. for use with interop). - ToFlecs methods now use an IAllocator passed to them - Use TempAllocator (one per thread) to allocate flecs structs Add basic ECS-based resource handling / loading. - Resources now loads from any assembly, including EntityPaths that represents an assembly + resource path - TextureManaged has been rewritten, others will follow - Move default shaders to gaemstone.Clientwip/source-generators
parent
c21726dd01
commit
ff9e376f27
42 changed files with 771 additions and 351 deletions
@ -1 +1 @@ |
||||
Subproject commit f5eea6704075601a674e3d759fdadc306b75573d |
||||
Subproject commit 3f9cf9c3793337eabf8647db6a4ac44017f20cc3 |
@ -0,0 +1,22 @@ |
||||
using gaemstone.ECS; |
||||
|
||||
namespace gaemstone.Client.Components; |
||||
|
||||
[Module] |
||||
public class ResourceComponents |
||||
{ |
||||
// Entities can have for example Texture as a tag, in which case |
||||
// they're the actual resource holding the data or handle. |
||||
|
||||
[Tag] |
||||
public struct Resource { } |
||||
|
||||
// Entities can also have a (Texture, $T) pair where $T is a resource, |
||||
// meaning the entity has that resource assigned as their texture. |
||||
|
||||
[Tag, Relation] |
||||
public struct Texture { } |
||||
|
||||
[Tag, Relation] |
||||
public struct Mesh { } |
||||
} |
@ -1,30 +1,54 @@ |
||||
using System; |
||||
using System.IO; |
||||
using System.Reflection; |
||||
using System.Text; |
||||
using gaemstone.ECS; |
||||
|
||||
namespace gaemstone.Client; |
||||
|
||||
public static class Resources |
||||
{ |
||||
public static Assembly ResourceAssembly { get; set; } = null!; |
||||
|
||||
public static Stream GetStream(string name) |
||||
=> ResourceAssembly.GetManifestResourceStream( |
||||
ResourceAssembly.GetName().Name + ".Resources." + name) |
||||
?? throw new ArgumentException($"Could not find embedded resource '{name}'"); |
||||
public static Stream GetStream(EntityPath path) |
||||
{ var (ass, name) = GetAssemblyAndName(path); return GetStream(ass, name); } |
||||
public static Stream GetStream(Assembly assembly, string name) |
||||
{ |
||||
var assemblyName = assembly.GetName().Name; |
||||
return assembly.GetManifestResourceStream($"{assemblyName}.Resources.{name}") |
||||
?? throw new ArgumentException($"Could not find embedded resource '{name}' in assembly '{assemblyName}'"); |
||||
} |
||||
|
||||
public static string GetString(string name) |
||||
public static string GetString(EntityPath path) |
||||
{ var (ass, name) = GetAssemblyAndName(path); return GetString(ass, name); } |
||||
public static string GetString(Assembly assembly, string name) |
||||
{ |
||||
using var stream = GetStream(name); |
||||
using var stream = GetStream(assembly, name); |
||||
using var reader = new StreamReader(stream); |
||||
return reader.ReadToEnd(); |
||||
} |
||||
|
||||
public static byte[] GetBytes(string name) |
||||
public static byte[] GetBytes(EntityPath path) |
||||
{ var (ass, name) = GetAssemblyAndName(path); return GetBytes(ass, name); } |
||||
public static byte[] GetBytes(Assembly assembly, string name) |
||||
{ |
||||
using var stream = GetStream(name); |
||||
using var stream = GetStream(assembly, name); |
||||
using var memoryStream = new MemoryStream(); |
||||
stream.CopyTo(memoryStream); |
||||
return memoryStream.ToArray(); |
||||
} |
||||
|
||||
private static (Assembly, string) GetAssemblyAndName(EntityPath path) |
||||
{ |
||||
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( |
||||
$"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++) |
||||
builder.Append('.').Append(path[i]); |
||||
return (assembly, builder.ToString()); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,63 @@ |
||||
using System; |
||||
using System.IO; |
||||
using gaemstone.Client.Components; |
||||
using gaemstone.ECS; |
||||
using Silk.NET.OpenGL; |
||||
using SixLabors.ImageSharp; |
||||
using SixLabors.ImageSharp.PixelFormats; |
||||
using static gaemstone.Client.Components.RenderingComponents; |
||||
using static gaemstone.Client.Systems.Windowing; |
||||
using Texture = gaemstone.Client.Components.ResourceComponents.Texture; |
||||
|
||||
namespace gaemstone.Client.Systems; |
||||
|
||||
[Module] |
||||
[DependsOn(typeof(RenderingComponents))] |
||||
[DependsOn(typeof(ResourceComponents))] |
||||
[DependsOn(typeof(Windowing))] |
||||
public class TextureManager |
||||
: IModuleInitializer |
||||
{ |
||||
public void Initialize(EntityRef module) |
||||
{ |
||||
var GL = module.Universe.LookupOrThrow<Game>().Get<Canvas>().GL; |
||||
|
||||
// Upload single-pixel white texture into texture slot 0, so when |
||||
// "no" texture is bound, we can still use the texture sampler. |
||||
GL.BindTexture(TextureTarget.Texture2D, 0); |
||||
Span<byte> pixel = stackalloc byte[4]; |
||||
pixel.Fill(0xFF); |
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, |
||||
1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, in pixel[0]); |
||||
} |
||||
|
||||
[System] |
||||
public static void LoadTextureWhenDefined( |
||||
[Game] Canvas canvas, EntityRef entity, |
||||
Texture _1, [Not] TextureHandle _2) |
||||
{ |
||||
var path = entity.GetFullPath(); |
||||
using var stream = Resources.GetStream(path); |
||||
var handle = CreateFromStream(canvas.GL, stream); |
||||
entity.Set(handle); |
||||
} |
||||
|
||||
private static TextureHandle CreateFromStream(GL GL, Stream stream) |
||||
{ |
||||
var target = TextureTarget.Texture2D; |
||||
var handle = GL.GenTexture(); |
||||
GL.BindTexture(target, handle); |
||||
|
||||
var image = Image.Load<Rgba32>(stream); |
||||
ref var origin = ref image.Frames[0].PixelBuffer[0, 0]; |
||||
|
||||
GL.TexImage2D(target, 0, (int)PixelFormat.Rgba, |
||||
(uint)image.Width, (uint)image.Height, 0, |
||||
PixelFormat.Rgba, PixelType.UnsignedByte, origin); |
||||
GL.TexParameter(target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); |
||||
GL.TexParameter(target, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); |
||||
|
||||
GL.BindTexture(target, 0); |
||||
return new(target, handle); |
||||
} |
||||
} |
@ -1,78 +0,0 @@ |
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using gaemstone.ECS; |
||||
using Silk.NET.OpenGL; |
||||
using SixLabors.ImageSharp; |
||||
using SixLabors.ImageSharp.PixelFormats; |
||||
using static gaemstone.Client.Systems.Windowing; |
||||
using Size = System.Drawing.Size; |
||||
using Texture = gaemstone.Client.Components.RenderingComponents.Texture; |
||||
|
||||
namespace gaemstone.Client; |
||||
|
||||
public static class TextureManager |
||||
{ |
||||
private static readonly Dictionary<Texture, TextureInfo> _byTexture = new(); |
||||
private static readonly Dictionary<string, TextureInfo> _bySourceFile = new(); |
||||
|
||||
public static void Initialize(Universe universe) |
||||
{ |
||||
var GL = universe.LookupOrThrow<Game>().Get<Canvas>().GL; |
||||
// Upload single-pixel white texture into texture slot 0, so when |
||||
// "no" texture is bound, we can still use the texture sampler. |
||||
GL.BindTexture(TextureTarget.Texture2D, 0); |
||||
Span<byte> pixel = stackalloc byte[4]; |
||||
pixel.Fill(255); |
||||
GL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, |
||||
1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, in pixel[0]); |
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); |
||||
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); |
||||
} |
||||
|
||||
public static Texture Load(Universe universe, string name) |
||||
{ |
||||
using var stream = Resources.GetStream(name); |
||||
return CreateFromStream(universe, stream, name); |
||||
} |
||||
|
||||
public static Texture CreateFromStream(Universe universe, Stream stream, string? sourceFile = null) |
||||
{ |
||||
var GL = universe.LookupOrThrow<Game>().Get<Canvas>().GL; |
||||
var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); |
||||
GL.BindTexture(texture.Target, texture.Handle); |
||||
|
||||
var image = Image.Load<Rgba32>(stream); |
||||
ref var origin = ref image.Frames[0].PixelBuffer[0, 0]; |
||||
|
||||
GL.TexImage2D(texture.Target, 0, (int)PixelFormat.Rgba, |
||||
(uint)image.Width, (uint)image.Height, 0, |
||||
PixelFormat.Rgba, PixelType.UnsignedByte, origin); |
||||
GL.TexParameter(texture.Target, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); |
||||
GL.TexParameter(texture.Target, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); |
||||
|
||||
var info = new TextureInfo(texture, sourceFile, new(image.Width, image.Height)); |
||||
_byTexture.Add(texture, info); |
||||
if (sourceFile != null) _bySourceFile.Add(sourceFile, info); |
||||
|
||||
GL.BindTexture(texture.Target, 0); |
||||
return texture; |
||||
} |
||||
|
||||
|
||||
public static TextureInfo? Lookup(Texture texture) |
||||
=> _byTexture.TryGetValue(texture, out var value) ? value : null; |
||||
|
||||
public static TextureInfo? Lookup(string sourceFile) |
||||
=> _bySourceFile.TryGetValue(sourceFile, out var value) ? value : null; |
||||
} |
||||
|
||||
public class TextureInfo |
||||
{ |
||||
public Texture Texture { get; } |
||||
public string? SourceFile { get; } |
||||
public Size Size { get; } |
||||
|
||||
public TextureInfo(Texture texture, string? sourceFile, Size size) |
||||
=> (Texture, SourceFile, Size) = (texture, sourceFile, size); |
||||
} |
@ -0,0 +1,203 @@ |
||||
using System; |
||||
using System.Buffers; |
||||
using System.Globalization; |
||||
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 = '/'; |
||||
|
||||
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 IsRelative => !IsAbsolute; |
||||
|
||||
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 unsafe EntityPath(byte* pointer) |
||||
: this(ArrayFromPointer(pointer)) { } |
||||
private static unsafe byte[] ArrayFromPointer(byte* pointer) |
||||
{ |
||||
var length = 0; |
||||
while (true) if (pointer[length++] == 0) break; |
||||
return new Span<byte>(pointer, length).ToArray(); |
||||
} |
||||
|
||||
// TODO: public EntityPath(EntityPath @base, params string[] parts) { } |
||||
public EntityPath(params string[] parts) |
||||
: this(ConcatParts(false, parts)) { } |
||||
public EntityPath(bool absolute, params string[] parts) |
||||
: this(ConcatParts(absolute, parts)) { } |
||||
private static byte[] ConcatParts(bool absolute, string[] parts) |
||||
{ |
||||
// 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; |
||||
} |
||||
|
||||
private EntityPath(byte[] bytes) |
||||
{ |
||||
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; |
||||
} |
||||
} |
||||
|
||||
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<byte> 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"); |
||||
} |
||||
|
||||
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"); |
||||
} |
||||
|
||||
public Enumerator GetEnumerator() => new(this); |
||||
public ref struct Enumerator |
||||
{ |
||||
private readonly EntityPath _path; |
||||
private int index = -1; |
||||
public UTF8View Current => _path[index]; |
||||
internal Enumerator(EntityPath path) => _path = path; |
||||
public bool MoveNext() => (++index >= _path.Depth); |
||||
} |
||||
|
||||
public ReadOnlySpan<byte> 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); |
||||
} |
||||
|
||||
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(); } |
||||
} |
||||
|
||||
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)); |
||||
} |
||||
} |
||||
|
||||
public readonly ref struct UTF8View |
||||
{ |
||||
private readonly ReadOnlySpan<byte> _bytes; |
||||
public UTF8View(ReadOnlySpan<byte> bytes) |
||||
=> _bytes = bytes; |
||||
|
||||
public int Length => _bytes.Length; |
||||
public byte this[int index] => _bytes[index]; |
||||
|
||||
public override string ToString() => Encoding.UTF8.GetString(_bytes); |
||||
public static implicit operator ReadOnlySpan<byte>(UTF8View view) => view._bytes; |
||||
public static implicit operator string(UTF8View view) => view.ToString(); |
||||
|
||||
public Enumerator GetEnumerator() => new(_bytes); |
||||
public ref struct Enumerator |
||||
{ |
||||
private readonly ReadOnlySpan<byte> _bytes; |
||||
private int index = 0; |
||||
private Rune _current = default; |
||||
public Rune Current => _current; |
||||
|
||||
internal Enumerator(ReadOnlySpan<byte> bytes) |
||||
=> _bytes = bytes; |
||||
|
||||
public bool MoveNext() |
||||
{ |
||||
if (index >= _bytes.Length) return false; |
||||
if (Rune.DecodeFromUtf8(_bytes[index..], out _current, out var consumed) != OperationStatus.Done) |
||||
throw new InvalidOperationException("Contains invalid UTF8"); |
||||
index += consumed; |
||||
return true; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,150 @@ |
||||
using System; |
||||
using System.Runtime.CompilerServices; |
||||
using System.Runtime.InteropServices; |
||||
using System.Text; |
||||
using System.Threading; |
||||
using static flecs_hub.flecs.Runtime; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public interface IAllocator |
||||
{ |
||||
nint Allocate(int byteCount); |
||||
void Free(nint pointer); |
||||
} |
||||
|
||||
public unsafe static class AllocatorExtensions |
||||
{ |
||||
public static Span<T> Allocate<T>(this IAllocator allocator, int count) where T : unmanaged |
||||
=> new((void*)allocator.Allocate(sizeof(T) * count), count); |
||||
public static void Free<T>(this IAllocator allocator, Span<T> span) where T : unmanaged |
||||
=> allocator.Free((nint)Unsafe.AsPointer(ref span[0])); |
||||
|
||||
public static Span<T> AllocateCopy<T>(this IAllocator allocator, ReadOnlySpan<T> orig) where T : unmanaged |
||||
{ var span = allocator.Allocate<T>(orig.Length); orig.CopyTo(span); return span; } |
||||
|
||||
public static ref T Allocate<T>(this IAllocator allocator) where T : unmanaged |
||||
=> ref Unsafe.AsRef<T>((void*)allocator.Allocate(sizeof(T))); |
||||
public static void Free<T>(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<byte>(bytes + 1); |
||||
Encoding.UTF8.GetBytes(value, span); |
||||
span[^1] = 0; |
||||
return new((nint)Unsafe.AsPointer(ref span[0])); |
||||
} |
||||
|
||||
public static CString AllocateCString(this IAllocator allocator, ReadOnlySpan<byte> utf8) |
||||
=> new((nint)Unsafe.AsPointer(ref allocator.AllocateCopy(utf8)[0])); |
||||
} |
||||
|
||||
public sealed class TempAllocator |
||||
: IAllocator |
||||
, IDisposable |
||||
{ |
||||
public const int Capacity = 1024 * 1024; // 1 MB |
||||
private static readonly ThreadLocal<TempAllocator> _tempAllocator = new(() => new()); |
||||
public static TempAllocator Lock() |
||||
{ |
||||
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; |
||||
} |
||||
|
||||
private readonly ArenaAllocator _allocator = new(Capacity); |
||||
private bool _isInUse = false; |
||||
|
||||
public nint Allocate(int byteCount) => _allocator.Allocate(byteCount); |
||||
public void Free(nint pointer) { /* Do nothing. */ } |
||||
public void Dispose() { _allocator.Reset(); _isInUse = false; } |
||||
} |
||||
|
||||
public class GlobalHeapAllocator |
||||
: IAllocator |
||||
{ |
||||
public nint Allocate(int byteCount) |
||||
=> Marshal.AllocHGlobal(byteCount); |
||||
public void Free(nint pointer) |
||||
=> Marshal.FreeHGlobal(pointer); |
||||
} |
||||
|
||||
public sealed 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 void Reset() |
||||
=> Used = 0; |
||||
} |
||||
|
||||
public sealed 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) |
||||
{ /* IGNORE */ } |
||||
} |
@ -1,44 +1,17 @@ |
||||
using System; |
||||
using System.Runtime.InteropServices; |
||||
using static flecs_hub.flecs; |
||||
using static flecs_hub.flecs.Runtime; |
||||
|
||||
namespace gaemstone; |
||||
namespace gaemstone.Utility; |
||||
|
||||
internal static unsafe class CStringExtensions |
||||
public unsafe static class CStringExtensions |
||||
{ |
||||
// FIXME: Most if not all strings passed to flecs probably need to be freed. |
||||
public static CString FlecsToCString(this string? str) |
||||
=> (str != null) ? new(Marshal.StringToHGlobalAnsi(str)) : default; |
||||
|
||||
public static AutoFreeCString FlecsToCStringThenFree(this string? str) |
||||
=> new(str); |
||||
|
||||
|
||||
public static void FlecsFree(this CString str) |
||||
{ if (!str.IsNull) ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); } |
||||
|
||||
public static string? FlecsToString(this CString str) |
||||
=> !str.IsNull ? Marshal.PtrToStringAnsi((nint)str)! : null; |
||||
=> Marshal.PtrToStringUTF8(str); |
||||
|
||||
public static string? FlecsToStringAndFree(this CString str) |
||||
{ var result = str.FlecsToString(); str.FlecsFree(); return result; } |
||||
|
||||
|
||||
public class AutoFreeCString |
||||
: IDisposable |
||||
{ |
||||
private readonly CString _cString; |
||||
public AutoFreeCString(string? str) |
||||
=> _cString = str.FlecsToCString(); |
||||
|
||||
~AutoFreeCString() => Dispose(); |
||||
public void Dispose() |
||||
{ |
||||
if (!_cString.IsNull) Marshal.FreeHGlobal((nint)_cString); |
||||
GC.SuppressFinalize(this); |
||||
} |
||||
|
||||
public static implicit operator CString(AutoFreeCString str) => str._cString; |
||||
} |
||||
public static void FlecsFree(this CString str) |
||||
=> ecs_os_get_api().free_.Data.Pointer((void*)(nint)str); |
||||
} |
||||
|
@ -0,0 +1,14 @@ |
||||
using System; |
||||
|
||||
namespace gaemstone.Utility; |
||||
|
||||
public static class CollectionExtensions |
||||
{ |
||||
// public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dict, |
||||
// TKey key, Func<TKey, TValue> valueFactory) { } |
||||
|
||||
public static T? MaybeGet<T>(this Span<T> span, int index) |
||||
where T : struct => MaybeGet((ReadOnlySpan<T>)span, index); |
||||
public static T? MaybeGet<T>(this ReadOnlySpan<T> span, int index) |
||||
where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; |
||||
} |
Loading…
Reference in new issue