diff --git a/src/Immersion/ObserverTest.cs b/src/Immersion/ObserverTest.cs index d593599..8ae1c61 100644 --- a/src/Immersion/ObserverTest.cs +++ b/src/Immersion/ObserverTest.cs @@ -10,6 +10,6 @@ namespace Immersion; public class ObserverTest { [Observer(typeof(ObserverEvent.OnSet))] - public static void DoObserver(in Chunk chunk, in Mesh _) + public static void DoObserver(in Chunk chunk, in MeshHandle _) => Console.WriteLine($"Chunk at {chunk.Position} now has a Mesh!"); } diff --git a/src/Immersion/Program.cs b/src/Immersion/Program.cs index ee68435..ad25244 100644 --- a/src/Immersion/Program.cs +++ b/src/Immersion/Program.cs @@ -7,19 +7,17 @@ using gaemstone.ECS; using gaemstone.Flecs; using gaemstone.Utility; using Silk.NET.Maths; -using Silk.NET.OpenGL; using Silk.NET.Windowing; using static gaemstone.Bloxel.Components.CoreComponents; using static gaemstone.Client.Components.CameraComponents; using static gaemstone.Client.Components.RenderingComponents; +using static gaemstone.Client.Components.ResourceComponents; using static gaemstone.Client.Systems.FreeCameraController; using static gaemstone.Client.Systems.Input; using static gaemstone.Client.Systems.Windowing; using static gaemstone.Components.TransformComponents; FlecsAbortException.SetupHook(); -Resources.ResourceAssembly = typeof(Program).Assembly; - var universe = new Universe(); var game = universe.LookupOrThrow(); @@ -35,7 +33,7 @@ window.Initialize(); window.Center(); universe.Modules.Register(); -game.Set(new Canvas(window.CreateOpenGL())); +game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window))); game.Set(new GameWindow(window)); universe.Modules.Register(); @@ -43,7 +41,8 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); -TextureManager.Initialize(universe); +universe.Modules.Register(); +universe.Modules.Register(); universe.Modules.Register(); game.Set(new RawInput()); @@ -56,16 +55,17 @@ universe.New("MainCamera") .Set(new CameraController { MouseSensitivity = 12.0F }) .Build(); -var heartMesh = MeshManager.Load(universe, "heart.glb"); -var swordMesh = MeshManager.Load(universe, "sword.glb"); +var heartMesh = MeshManager.Load(universe, "/Immersion/Resources/heart.glb"); +var swordMesh = MeshManager.Load(universe, "/Immersion/Resources/sword.glb"); +var entities = universe.New("Entities").Build(); var rnd = new Random(); for (var x = -12; x <= 12; x++) for (var z = -12; z <= 12; z++) { var position = Matrix4X4.CreateTranslation(x * 2, 0.0F, z * 2); var rotation = Matrix4X4.CreateRotationY(rnd.NextFloat(MathF.PI * 2)); var (type, mesh) = rnd.Pick(("Heart", heartMesh), ("Sword", swordMesh)); - universe.New($"{type} {x}:{z}") + entities.NewChild() .Set((GlobalTransform)(rotation * position)) .Set(mesh) .Build(); @@ -75,24 +75,24 @@ universe.Modules.Register(); universe.Modules.Register(); universe.Modules.Register(); -var texture = TextureManager.Load(universe, "terrain.png"); +var texture = universe.New("/Immersion/Resources/terrain.png").Add().Build(); var stone = universe.New("Stone").Set(TextureCoords4.FromGrid(4, 4, 1, 0)).Build(); var dirt = universe.New("Dirt" ).Set(TextureCoords4.FromGrid(4, 4, 2, 0)).Build(); var grass = universe.New("Grass").Set(TextureCoords4.FromGrid(4, 4, 3, 0)).Build(); -var sizeH = 4; -var sizeY = 2; +var chunks = universe.New("Chunks").Build(); +var sizeH = 4; var sizeY = 2; for (var cx = -sizeH; cx < sizeH; cx++) for (var cy = -sizeY; cy < sizeY; cy++) for (var cz = -sizeH; cz < sizeH; cz++) { var pos = new ChunkPos(cx, cy - 2, cz); var storage = new ChunkStoreBlocks(); - universe.New($"Chunk {cx}:{cy}:{cz}") + chunks.NewChild() .Set((GlobalTransform)Matrix4X4.CreateTranslation(pos.GetOrigin())) .Set(new Chunk(pos)) .Set(storage) - .Set(texture) + .Add(texture) .Build(); } diff --git a/src/flecs-cs b/src/flecs-cs index f5eea67..3f9cf9c 160000 --- a/src/flecs-cs +++ b/src/flecs-cs @@ -1 +1 @@ -Subproject commit f5eea6704075601a674e3d759fdadc306b75573d +Subproject commit 3f9cf9c3793337eabf8647db6a4ac44017f20cc3 diff --git a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs index 46d9547..b6b5bae 100644 --- a/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs +++ b/src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs @@ -34,15 +34,15 @@ public class ChunkMeshGenerator [System] public void GenerateChunkMeshes(Universe universe, EntityRef entity, in Chunk chunk, ChunkStoreBlocks blocks, - HasBasicWorldGeneration _1, [Not] Mesh _2) + HasBasicWorldGeneration _1, [Not] MeshHandle _2) { var maybeMesh = Generate(universe, chunk.Position, blocks); - if (maybeMesh is Mesh mesh) entity.Set(mesh); + if (maybeMesh is MeshHandle mesh) entity.Set(mesh); else entity.Delete(); } - public Mesh? Generate(Universe universe, ChunkPos chunkPos, - ChunkStoreBlocks centerBlocks) + public MeshHandle? Generate(Universe universe, + ChunkPos chunkPos, ChunkStoreBlocks centerBlocks) { // TODO: We'll need a way to get neighbors again. // var storages = new ChunkStoreBlocks[3, 3, 3]; diff --git a/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs index d28a394..103c455 100644 --- a/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs +++ b/src/gaemstone.Bloxel/Utility/ChunkedOctree.cs @@ -72,7 +72,7 @@ public class ChunkedOctree while (enumerator.MoveNext()) yield return enumerator.Current; } - public class Enumerator + public sealed class Enumerator : IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)> { private readonly ChunkedOctree _octree; @@ -107,7 +107,7 @@ public class ChunkedOctree } public void Reset() => throw new NotSupportedException(); - public void Dispose() { } + public void Dispose() { } internal void SearchFrom(ZOrder nodePos) diff --git a/src/gaemstone.Client/Components/RenderingComponents.cs b/src/gaemstone.Client/Components/RenderingComponents.cs index 7266362..1d019f1 100644 --- a/src/gaemstone.Client/Components/RenderingComponents.cs +++ b/src/gaemstone.Client/Components/RenderingComponents.cs @@ -9,23 +9,23 @@ namespace gaemstone.Client.Components; public class RenderingComponents { [Component] - public readonly struct Mesh + public readonly struct MeshHandle { public uint Handle { get; } public int Count { get; } public bool IsIndexed { get; } - public Mesh(uint handle, int count, bool indexed = true) + public MeshHandle(uint handle, int count, bool indexed = true) { Handle = handle; Count = count; IsIndexed = indexed; } } [Component] - public readonly struct Texture + public readonly struct TextureHandle { public TextureTarget Target { get; } public uint Handle { get; } - public Texture(TextureTarget target, uint handle) + public TextureHandle(TextureTarget target, uint handle) => (Target, Handle) = (target, handle); } diff --git a/src/gaemstone.Client/Components/ResourceComponents.cs b/src/gaemstone.Client/Components/ResourceComponents.cs new file mode 100644 index 0000000..0c21ad0 --- /dev/null +++ b/src/gaemstone.Client/Components/ResourceComponents.cs @@ -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 { } +} diff --git a/src/gaemstone.Client/Resources.cs b/src/gaemstone.Client/Resources.cs index c68751f..43b3337 100644 --- a/src/gaemstone.Client/Resources.cs +++ b/src/gaemstone.Client/Resources.cs @@ -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()); + } } diff --git a/src/Immersion/Resources/default.fs.glsl b/src/gaemstone.Client/Resources/default.fs.glsl similarity index 100% rename from src/Immersion/Resources/default.fs.glsl rename to src/gaemstone.Client/Resources/default.fs.glsl diff --git a/src/Immersion/Resources/default.vs.glsl b/src/gaemstone.Client/Resources/default.vs.glsl similarity index 100% rename from src/Immersion/Resources/default.vs.glsl rename to src/gaemstone.Client/Resources/default.vs.glsl diff --git a/src/gaemstone.Client/MeshManager.cs b/src/gaemstone.Client/Systems/MeshManager.cs similarity index 93% rename from src/gaemstone.Client/MeshManager.cs rename to src/gaemstone.Client/Systems/MeshManager.cs index 1a7651b..605c371 100644 --- a/src/gaemstone.Client/MeshManager.cs +++ b/src/gaemstone.Client/Systems/MeshManager.cs @@ -14,10 +14,10 @@ public static class MeshManager private const uint NormalAttribIndex = 1; private const uint UvAttribIndex = 2; - public static Mesh Load(Universe universe, string name) + public static MeshHandle Load(Universe universe, EntityPath path) { ModelRoot root; - using (var stream = Resources.GetStream(name)) + using (var stream = Resources.GetStream(path)) root = ModelRoot.ReadGLB(stream, new()); var primitive = root.LogicalMeshes[0].Primitives[0]; @@ -48,7 +48,7 @@ public static class MeshManager return new(vao, numVertices); } - public static Mesh Create(Universe universe, + public static MeshHandle Create(Universe universe, ReadOnlySpan indices, ReadOnlySpan> vertices, ReadOnlySpan> normals, ReadOnlySpan> uvs) { @@ -77,7 +77,7 @@ public static class MeshManager return new(vao, indices.Length); } - public static Mesh Create(Universe universe, ReadOnlySpan> vertices, + public static MeshHandle Create(Universe universe, ReadOnlySpan> vertices, ReadOnlySpan> normals, ReadOnlySpan> uvs) { var GL = universe.LookupOrThrow().Get().GL; diff --git a/src/gaemstone.Client/Systems/Renderer.cs b/src/gaemstone.Client/Systems/Renderer.cs index 2607c72..87611ea 100644 --- a/src/gaemstone.Client/Systems/Renderer.cs +++ b/src/gaemstone.Client/Systems/Renderer.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using gaemstone.ECS; using gaemstone.Flecs; +using gaemstone.Utility; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.Windowing; @@ -10,7 +11,6 @@ using static gaemstone.Client.Components.CameraComponents; using static gaemstone.Client.Components.RenderingComponents; using static gaemstone.Client.Systems.Windowing; using static gaemstone.Components.TransformComponents; -using Texture = gaemstone.Client.Components.RenderingComponents.Texture; namespace gaemstone.Client.Systems; @@ -22,10 +22,11 @@ public class Renderer private uint _program; private int _cameraMatrixUniform; private int _modelMatrixUniform; + private Rule? _renderEntityRule; - public void Initialize(EntityRef entity) + public void Initialize(EntityRef module) { - var GL = entity.Universe.LookupOrThrow().Get().GL; + var GL = module.Universe.LookupOrThrow().Get().GL; GL.Enable(EnableCap.DebugOutputSynchronous); GL.DebugMessageCallback(DebugCallback, 0); @@ -39,8 +40,8 @@ public class Renderer GL.Enable(EnableCap.Blend); GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - var vertexShaderSource = Resources.GetString("default.vs.glsl"); - var fragmentShaderSource = Resources.GetString("default.fs.glsl"); + var vertexShaderSource = Resources.GetString("/gaemstone.Client/Resources/default.vs.glsl"); + var fragmentShaderSource = Resources.GetString("/gaemstone.Client/Resources/default.fs.glsl"); var vertexShader = GL.CreateAndCompileShader(ShaderType.VertexShader , "vertex" , vertexShaderSource); var fragmentShader = GL.CreateAndCompileShader(ShaderType.FragmentShader, "fragment", fragmentShaderSource); @@ -59,7 +60,8 @@ public class Renderer GL.ClearColor(new Vector4D(0, 0, 0, 255)); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - Filter.RunOnce(universe, (in GlobalTransform transform, in Camera camera, CameraViewport? viewport) => { + Filter.RunOnce(universe, (in GlobalTransform cameraTransform, + in Camera camera, CameraViewport? viewport) => { var color = viewport?.ClearColor ?? new(0x4B, 0x00, 0x82, 255); var bounds = viewport?.Viewport ?? new(default, canvas.Size); @@ -69,7 +71,7 @@ public class Renderer GL.Disable(EnableCap.ScissorTest); // Get the camera's transform matrix and invert it. - Matrix4X4.Invert(transform, out var cameraTransform); + Matrix4X4.Invert(cameraTransform, out var invertedTransform); // Create the camera's projection matrix. var cameraProjection = camera.IsOrthographic @@ -82,22 +84,35 @@ public class Renderer camera.NearPlane, camera.FarPlane); // Set the uniform to the combined transform and projection. - var cameraMatrix = cameraTransform * cameraProjection; + var cameraMatrix = invertedTransform * cameraProjection; GL.UniformMatrix4(_cameraMatrixUniform, 1, false, in cameraMatrix.Row1.X); - Filter.RunOnce(universe, (in GlobalTransform transform, in Mesh mesh, Texture? texture) => { - // If entity has Texture, bind it now. - if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle); - - // Draw the mesh. - GL.UniformMatrix4(_modelMatrixUniform, 1, false, in transform.Value.Row1.X); - GL.BindVertexArray(mesh.Handle); - if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count); - else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); } - - // If entity has Texture, unbind it after it has been rendered. - if (texture.HasValue) GL.BindTexture(texture.Value.Target, 0); - }); + _renderEntityRule ??= new(universe, new("GlobalTransform, MeshHandle, ?(Texture, $tex), ?TextureHandle($tex)")); + foreach (var iter in _renderEntityRule) { + var transforms = iter.Field(1); + var meshes = iter.Field(2); + // var texPairs = iter.MaybeField(3); + var textures = iter.MaybeField(4); + + for (var i = 0; i < iter.Count; i++) { + var rTransform = transforms[i]; + var mesh = meshes[i]; + // var hasTexture = (texPairs.Length > 0); + var texture = textures.MaybeGet(i); + + // If entity has Texture, bind it now. + if (texture.HasValue) GL.BindTexture(texture.Value.Target, texture.Value.Handle); + + // Draw the mesh. + GL.UniformMatrix4(_modelMatrixUniform, 1, false, in rTransform.Value.Row1.X); + GL.BindVertexArray(mesh.Handle); + if (!mesh.IsIndexed) GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Count); + else unsafe { GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Count, DrawElementsType.UnsignedShort, null); } + + // If entity has Texture, unbind it after it has been rendered. + if (texture.HasValue) GL.BindTexture(texture.Value.Target, 0); + } + } }); window.Handle.SwapBuffers(); diff --git a/src/gaemstone.Client/Systems/TextureManager.cs b/src/gaemstone.Client/Systems/TextureManager.cs new file mode 100644 index 0000000..8a94742 --- /dev/null +++ b/src/gaemstone.Client/Systems/TextureManager.cs @@ -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().Get().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 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(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); + } +} diff --git a/src/gaemstone.Client/TextureManager.cs b/src/gaemstone.Client/TextureManager.cs deleted file mode 100644 index af8688a..0000000 --- a/src/gaemstone.Client/TextureManager.cs +++ /dev/null @@ -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 _byTexture = new(); - private static readonly Dictionary _bySourceFile = new(); - - public static void Initialize(Universe universe) - { - var GL = universe.LookupOrThrow().Get().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 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().Get().GL; - var texture = new Texture(TextureTarget.Texture2D, GL.GenTexture()); - GL.BindTexture(texture.Target, texture.Handle); - - var image = Image.Load(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); -} diff --git a/src/gaemstone.Client/gaemstone.Client.csproj b/src/gaemstone.Client/gaemstone.Client.csproj index 8d853d5..3e4b245 100644 --- a/src/gaemstone.Client/gaemstone.Client.csproj +++ b/src/gaemstone.Client/gaemstone.Client.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/gaemstone/ECS/EntityBuilder.cs b/src/gaemstone/ECS/EntityBuilder.cs index 503a86e..37772e4 100644 --- a/src/gaemstone/ECS/EntityBuilder.cs +++ b/src/gaemstone/ECS/EntityBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -13,11 +14,11 @@ public class EntityBuilder public Entity ID { get; set; } /// - /// Name of the entity. If no entity is provided, an entity with this name - /// will be looked up first. When an entity is provided, the name will be + /// Path of the entity. If no entity is provided, an entity with this path + /// will be looked up first. When an entity is provided, the path will be /// verified with the existing entity. /// - public string? Name { get; set; } + public EntityPath? Path { get; set; } /// /// Optional entity symbol. A symbol is an unscoped identifier that can @@ -26,7 +27,8 @@ public class EntityBuilder /// function name, where these identifiers differ from the name they are /// registered with in flecs. /// - public string? Symbol { get; set; } + public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; } + private string? _symbol = null; /// /// When set to true, a low id (typically reserved for components) @@ -43,8 +45,8 @@ public class EntityBuilder /// Actions to run once the entity has been created. private readonly List> _toSet = new(); - public EntityBuilder(Universe universe, string? name = null, string? symbol = null) - { Universe = universe; Name = name; Symbol = symbol; } + 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(); @@ -63,12 +65,15 @@ public class EntityBuilder public unsafe EntityRef Build() { + using var alloc = TempAllocator.Lock(); var desc = new ecs_entity_desc_t { - id = ID, - name = Name.FlecsToCString(), - symbol = Symbol.FlecsToCString(), - add_expr = Expression.FlecsToCString(), + id = ID, + name = (Path != null) ? alloc.AllocateCString(Path.AsSpan(true)) : default, + symbol = alloc.AllocateCString(_symbol), + add_expr = alloc.AllocateCString(Expression), use_low_id = UseLowID, + root_sep = EntityPath.SeparatorAsCString, + sep = EntityPath.SeparatorAsCString, }; var add = desc.add; var index = 0; foreach (var id in _toAdd) add[index++] = id; diff --git a/src/gaemstone/ECS/EntityPath.cs b/src/gaemstone/ECS/EntityPath.cs new file mode 100644 index 0000000..c92b54a --- /dev/null +++ b/src/gaemstone/ECS/EntityPath.cs @@ -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(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 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 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 _bytes; + public UTF8View(ReadOnlySpan 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(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 _bytes; + private int index = 0; + private Rune _current = default; + public Rune Current => _current; + + internal Enumerator(ReadOnlySpan 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; + } + } +} diff --git a/src/gaemstone/ECS/EntityRef.cs b/src/gaemstone/ECS/EntityRef.cs index 30b8d7e..f387d30 100644 --- a/src/gaemstone/ECS/EntityRef.cs +++ b/src/gaemstone/ECS/EntityRef.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -15,15 +16,14 @@ public unsafe sealed class EntityRef public bool IsValid => ecs_is_valid(Universe, this); public bool IsAlive => ecs_is_alive(Universe, this); public EntityType Type => new(Universe, ecs_get_type(Universe, this)); - public string FullPath => ecs_get_path_w_sep(Universe, default, this, ".", default).FlecsToStringAndFree()!; public string? Name { get => ecs_get_name(Universe, this).FlecsToString()!; - set => ecs_set_name(Universe, this, value.FlecsToCStringThenFree()); + set { using var alloc = TempAllocator.Lock(); ecs_set_name(Universe, this, alloc.AllocateCString(value)); } } public string? Symbol { get => ecs_get_symbol(Universe, this).FlecsToString()!; - set => ecs_set_symbol(Universe, this, value.FlecsToCStringThenFree()); + set { using var alloc = TempAllocator.Lock(); ecs_set_symbol(Universe, this, alloc.AllocateCString(value)); } } // TODO: public IEnumerable Children => ... @@ -38,8 +38,10 @@ public unsafe sealed class EntityRef public void Delete() => ecs_delete(Universe, this); - public EntityBuilder NewChild(string? name = null, string? symbol = null) - => Universe.New(name, symbol).ChildOf(this); + public EntityBuilder NewChild(EntityPath? path = null) + => Universe.New(EnsureRelativePath(path)).ChildOf(this); + private static EntityPath? EnsureRelativePath(EntityPath? path) + { if (path?.IsAbsolute == true) throw new ArgumentException("path must not be absolute", nameof(path)); return path; } public override EntityRef Add(Identifier id) { ecs_add_id(Universe, this, id); return this; } public override EntityRef Remove(Identifier id) { ecs_remove_id(Universe, this, id); return this; } diff --git a/src/gaemstone/ECS/EntityType.cs b/src/gaemstone/ECS/EntityType.cs index 2644d11..543e4c3 100644 --- a/src/gaemstone/ECS/EntityType.cs +++ b/src/gaemstone/ECS/EntityType.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; diff --git a/src/gaemstone/ECS/Filter.cs b/src/gaemstone/ECS/Filter.cs index e5d8b6a..f34b712 100644 --- a/src/gaemstone/ECS/Filter.cs +++ b/src/gaemstone/ECS/Filter.cs @@ -2,7 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; +using gaemstone.Utility; using gaemstone.Utility.IL; using static flecs_hub.flecs; @@ -15,18 +16,18 @@ public unsafe sealed class Filter public Universe Universe { get; } public ecs_filter_t* Handle { get; } - private Filter(Universe universe, ecs_filter_t* handle) - { Universe = universe; Handle = handle; } - private Filter(Universe universe, ecs_filter_desc_t desc) - : this(universe, ecs_filter_init(universe, &desc)) { } - public Filter(Universe universe, FilterDesc desc) - : this(universe, desc.ToFlecs()) { } + { + using var alloc = TempAllocator.Lock(); + var flecsDesc = desc.ToFlecs(alloc); + Universe = universe; + Handle = ecs_filter_init(universe, &flecsDesc); + } public static void RunOnce(Universe universe, Delegate action) { var gen = IterActionGenerator.GetOrBuild(universe, action.Method); - var desc = new FilterDesc(action.Method.Name, gen.Terms.ToArray()); + var desc = new FilterDesc(gen.Terms.ToArray()) { Name = action.Method.Name }; using var filter = new Filter(universe, desc); foreach (var iter in filter) gen.RunWithTryCatch(action.Target, iter); } @@ -47,7 +48,9 @@ public unsafe sealed class Filter public class FilterDesc { - public IReadOnlyList Terms { get; set; } + public IReadOnlyList Terms { get; } + + public string? Expression { get; } /// /// Optional name of filter, used for debugging. If a filter is created @@ -55,9 +58,6 @@ public class FilterDesc /// public string? Name { get; set; } - /// Filter expression. Should not be set at the same time as terms. - public string? Expression { get; set; } - /// /// When true, terms returned by an iterator may either contain 1 or N /// elements, where terms with N elements are owned, and terms with 1 @@ -67,26 +67,26 @@ public class FilterDesc /// public bool Instanced { get; set; } - public FilterDesc(string? name = null, params Term[] terms) - { Name = name; Terms = terms; } + public FilterDesc(params Term[] terms) + => Terms = terms; + public FilterDesc(string expression) : this() + => Expression = expression; - public unsafe ecs_filter_desc_t ToFlecs() + public unsafe ecs_filter_desc_t ToFlecs(IAllocator allocator) { var desc = new ecs_filter_desc_t { - name = Name.FlecsToCString(), - expr = Expression.FlecsToCString(), + name = allocator.AllocateCString(Name), + expr = allocator.AllocateCString(Expression), instanced = Instanced, }; var span = desc.terms; if (Terms.Count > span.Length) { - var byteCount = sizeof(ecs_term_t) * Terms.Count; - var ptr = (ecs_term_t*)Marshal.AllocHGlobal(byteCount); // FIXME: Free this. - desc.terms_buffer = ptr; + span = allocator.Allocate(Terms.Count); + desc.terms_buffer = (ecs_term_t*)Unsafe.AsPointer(ref span[0]); desc.terms_buffer_count = Terms.Count; - span = new(ptr, Terms.Count); } for (var i = 0; i < Terms.Count; i++) - span[i] = Terms[i].ToFlecs(); + span[i] = Terms[i].ToFlecs(allocator); return desc; } } diff --git a/src/gaemstone/ECS/Identifier.cs b/src/gaemstone/ECS/Identifier.cs index 40cfbe3..5efcb14 100644 --- a/src/gaemstone/ECS/Identifier.cs +++ b/src/gaemstone/ECS/Identifier.cs @@ -21,6 +21,9 @@ public readonly struct Identifier (relation.Value.Data << 32) | (target.Value.Data & ECS_ENTITY_MASK))); + public (EntityRef Relation, EntityRef Target) AsPair(Universe universe) + => new IdentifierRef(universe, this).AsPair(); + public bool Equals(Identifier other) => Value.Data == other.Value.Data; public override bool Equals(object? obj) => (obj is Identifier other) && Equals(other); public override int GetHashCode() => Value.Data.GetHashCode(); diff --git a/src/gaemstone/ECS/IdentifierRef.cs b/src/gaemstone/ECS/IdentifierRef.cs index 5bab82a..29f43b5 100644 --- a/src/gaemstone/ECS/IdentifierRef.cs +++ b/src/gaemstone/ECS/IdentifierRef.cs @@ -1,4 +1,5 @@ using System; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; diff --git a/src/gaemstone/ECS/Iterator.cs b/src/gaemstone/ECS/Iterator.cs index fedb026..fe0f3ea 100644 --- a/src/gaemstone/ECS/Iterator.cs +++ b/src/gaemstone/ECS/Iterator.cs @@ -25,7 +25,8 @@ public unsafe partial class Iterator public static Iterator FromTerm(Universe universe, Term term) { - var flecsTerm = term.ToFlecs(); + using var alloc = TempAllocator.Lock(); + var flecsTerm = term.ToFlecs(alloc); var flecsIter = ecs_term_iter(universe, &flecsTerm); return new(universe, IteratorType.Term, flecsIter); } @@ -55,6 +56,9 @@ public unsafe partial class Iterator } } + public Span MaybeField(int index) + where T : unmanaged => FieldIsSet(index) ? Field(index) : default; + public SpanToRef FieldRef(int index) where T : class => new(Field(index)); diff --git a/src/gaemstone/ECS/Module.cs b/src/gaemstone/ECS/Module.cs index 191e829..1215175 100644 --- a/src/gaemstone/ECS/Module.cs +++ b/src/gaemstone/ECS/Module.cs @@ -5,21 +5,27 @@ namespace gaemstone.ECS; [AttributeUsage(AttributeTargets.Class)] public class ModuleAttribute : Attribute { - public string? Name { get; set; } + public string[]? Path { get; set; } + + public ModuleAttribute() { } + public ModuleAttribute(params string[] path) + { + if (path.Length == 0) throw new ArgumentException( + "Path must not be empty", nameof(path)); + Path = path; + } } [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class DependsOnAttribute : Attribute { - public string Name { get; } + public Type Type { get; } - public DependsOnAttribute(string name) - => Name = name; public DependsOnAttribute(Type type) - : this(ModuleManager.GetModuleName(type)) { } + { Type = type; } } public interface IModuleInitializer { - void Initialize(EntityRef entity); + void Initialize(EntityRef module); } diff --git a/src/gaemstone/ECS/Observer.cs b/src/gaemstone/ECS/Observer.cs index f50bb87..398b1ab 100644 --- a/src/gaemstone/ECS/Observer.cs +++ b/src/gaemstone/ECS/Observer.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using gaemstone.Utility; using gaemstone.Utility.IL; @@ -18,12 +19,12 @@ public class ObserverAttribute : Attribute public static class ObserverExtensions { public static unsafe EntityRef RegisterObserver(this Universe universe, - string name, Entity @event, FilterDesc filter, Action callback) + FilterDesc filter, Entity @event, Action callback) { - filter.Name = name; + using var alloc = TempAllocator.Lock(); var desc = new ecs_observer_desc_t { - filter = filter.ToFlecs(), - entity = universe.New(name).Build(), + filter = filter.ToFlecs(alloc), + 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 } }, }; @@ -37,24 +38,24 @@ public static class ObserverExtensions { var attr = method.Get() ?? throw new ArgumentException( "Observer must specify ObserverAttribute", nameof(method)); - var filter = new FilterDesc(); + FilterDesc filter; Action iterAction; var param = method.GetParameters(); if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { - filter.Expression = attr.Expression ?? throw new Exception( - "Observer must specify ObserverAttribute.Expression"); + filter = new(attr.Expression ?? throw new Exception( + "Observer must specify ObserverAttribute.Expression")); if (method.IsStatic) instance = null; iterAction = (Action)Delegate.CreateDelegate( typeof(Action), instance, method); } else { var gen = IterActionGenerator.GetOrBuild(universe, method); - if (attr.Expression == null) filter.Terms = gen.Terms; - else filter.Expression = attr.Expression; + filter = (attr.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray()); iterAction = iter => gen.RunWithTryCatch(instance, iter); } + filter.Name = method.Name; var @event = universe.LookupOrThrow(attr.Event); - return universe.RegisterObserver(method.Name, @event, filter, iterAction); + return universe.RegisterObserver(filter, @event, iterAction); } } diff --git a/src/gaemstone/ECS/Query.cs b/src/gaemstone/ECS/Query.cs index 550e430..482a408 100644 --- a/src/gaemstone/ECS/Query.cs +++ b/src/gaemstone/ECS/Query.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -12,13 +13,13 @@ public unsafe sealed class Query public Universe Universe { get; } public ecs_query_t* Handle { get; } - private Query(Universe universe, ecs_query_t* handle) - { Universe = universe; Handle = handle; } - private Query(Universe universe, ecs_query_desc_t desc) - : this(universe, ecs_query_init(universe, &desc)) { } - public Query(Universe universe, QueryDesc desc) - : this(universe, desc.ToFlecs()) { } + { + using var alloc = TempAllocator.Lock(); + var flecsDesc = desc.ToFlecs(alloc); + Universe = universe; + Handle = ecs_query_init(universe, &flecsDesc); + } public void Dispose() => ecs_query_fini(this); @@ -36,13 +37,13 @@ public unsafe sealed class Query public class QueryDesc : FilterDesc { - public QueryDesc(string? name = null, params Term[] terms) - : base(name, terms) { } + public QueryDesc(string expression) : base(expression) { } + public QueryDesc(params Term[] terms) : base(terms) { } - public new unsafe ecs_query_desc_t ToFlecs() + public new unsafe ecs_query_desc_t ToFlecs(IAllocator allocator) { var desc = new ecs_query_desc_t { - filter = base.ToFlecs(), + filter = base.ToFlecs(allocator), // TODO: Implement more Query features. }; return desc; diff --git a/src/gaemstone/ECS/Rule.cs b/src/gaemstone/ECS/Rule.cs index cb4545f..bcc430a 100644 --- a/src/gaemstone/ECS/Rule.cs +++ b/src/gaemstone/ECS/Rule.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -12,13 +13,13 @@ public unsafe sealed class Rule public Universe Universe { get; } public ecs_rule_t* Handle { get; } - private Rule(Universe universe, ecs_rule_t* handle) - { Universe = universe; Handle = handle; } - private Rule(Universe universe, ecs_filter_desc_t desc) - : this(universe, ecs_rule_init(universe, &desc)) { } - public Rule(Universe universe, FilterDesc desc) - : this(universe, desc.ToFlecs()) { } + { + using var alloc = TempAllocator.Lock(); + var flecsDesc = desc.ToFlecs(alloc); + Universe = universe; + Handle = ecs_rule_init(universe, &flecsDesc); + } public void Dispose() => ecs_rule_fini(this); diff --git a/src/gaemstone/ECS/System.cs b/src/gaemstone/ECS/System.cs index 0474234..5a18fc6 100644 --- a/src/gaemstone/ECS/System.cs +++ b/src/gaemstone/ECS/System.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using gaemstone.Flecs; @@ -22,63 +23,63 @@ public class SystemAttribute : Attribute public static class SystemExtensions { public static unsafe EntityRef RegisterSystem(this Universe universe, - string name, Entity phase, QueryDesc query, Action callback) + QueryDesc query, Entity phase, Action callback) { - query.Name = name; + var entity = universe.New((query.Name != null) ? new(query.Name) : null) + .Add(phase) + .Add(phase) + .Build(); + using var alloc = TempAllocator.Lock(); var desc = new ecs_system_desc_t { - query = query.ToFlecs(), - entity = universe.New(name) - .Add(phase) - .Add(phase) - .Build(), + query = query.ToFlecs(alloc), + entity = entity, binding_ctx = (void*)CallbackContextHelper.Create((universe, callback)), callback = new() { Data = new() { Pointer = &Callback } }, }; - var entity = ecs_system_init(universe, &desc); - return new(universe, new(entity)); + return new(universe, new(ecs_system_init(universe, &desc))); } public static EntityRef RegisterSystem(this Universe universe, Delegate action) { - var attr = action.Method.Get(); - var query = new QueryDesc(); + var attr = action.Method.Get(); + QueryDesc query; if (action is Action iterAction) { - query.Expression = attr?.Expression ?? throw new ArgumentException( - "System must specify SystemAttribute.Expression", nameof(action)); + query = new(attr?.Expression ?? throw new ArgumentException( + "System must specify SystemAttribute.Expression", nameof(action))); } else { var method = action.GetType().GetMethod("Invoke")!; var gen = IterActionGenerator.GetOrBuild(universe, method); - if (attr?.Expression != null) query.Expression = attr.Expression; - else query.Terms = gen.Terms; + query = (attr?.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray()); iterAction = iter => gen.RunWithTryCatch(action.Target, iter); } + query.Name = action.Method.Name; var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return universe.RegisterSystem(action.Method.Name, phase, query, iterAction); + return universe.RegisterSystem(query, phase, iterAction); } public static EntityRef RegisterSystem(this Universe universe, object? instance, MethodInfo method) { - var attr = method.Get(); - var query = new QueryDesc(); + var attr = method.Get(); + QueryDesc query; Action iterAction; var param = method.GetParameters(); if (param.Length == 1 && param[0].ParameterType == typeof(Iterator)) { - query.Expression = attr?.Expression ?? throw new ArgumentException( - "System must specify SystemAttribute.Expression", nameof(method)); + query = new(attr?.Expression ?? throw new ArgumentException( + "System must specify SystemAttribute.Expression", nameof(method))); iterAction = (Action)Delegate.CreateDelegate(typeof(Action), instance, method); } else { var gen = IterActionGenerator.GetOrBuild(universe, method); - if (attr?.Expression == null) query.Terms = gen.Terms; - else query.Expression = attr.Expression; + query = (attr?.Expression != null) ? new(attr.Expression) : new(gen.Terms.ToArray()); iterAction = iter => gen.RunWithTryCatch(instance, iter); } + query.Name = method.Name; var phase = universe.LookupOrThrow(attr?.Phase ?? typeof(SystemPhase.OnUpdate)); - return universe.RegisterSystem(method.Name, phase, query, iterAction); + return universe.RegisterSystem(query, phase, iterAction); } [UnmanagedCallersOnly] diff --git a/src/gaemstone/ECS/Term.cs b/src/gaemstone/ECS/Term.cs index 5094244..9d117db 100644 --- a/src/gaemstone/ECS/Term.cs +++ b/src/gaemstone/ECS/Term.cs @@ -1,4 +1,5 @@ using System; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -15,14 +16,12 @@ public class Term public Term() { } public Term(Identifier id) => ID = id; - public Term(string name) => First = name; public Term(TermID first, TermID second) { First = first; Second = second; } public static implicit operator Term(EntityRef entity) => new(entity); public static implicit operator Term(Entity entity) => new(entity); public static implicit operator Term(IdentifierRef id) => new(id); public static implicit operator Term(Identifier id) => new(id); - public static implicit operator Term(string name) => new(name); public Term None { get { InOut = TermInOutKind.None; return this; } } public Term In { get { InOut = TermInOutKind.In; return this; } } @@ -32,11 +31,11 @@ public class Term public Term Not { get { Oper = TermOperKind.Not; return this; } } public Term Optional { get { Oper = TermOperKind.Optional; return this; } } - public ecs_term_t ToFlecs() => new() { + public ecs_term_t ToFlecs(IAllocator allocator) => new() { id = ID, - src = Source?.ToFlecs() ?? default, - first = First?.ToFlecs() ?? default, - second = Second?.ToFlecs() ?? default, + src = Source?.ToFlecs(allocator) ?? default, + first = First?.ToFlecs(allocator) ?? default, + second = Second?.ToFlecs(allocator) ?? default, inout = (ecs_inout_kind_t)InOut, oper = (ecs_oper_kind_t)Oper, id_flags = (ecs_id_t)(ulong)Flags, @@ -86,9 +85,9 @@ public class TermID public static implicit operator TermID(Entity entity) => new(entity); public static implicit operator TermID(string name) => new(name); - public ecs_term_id_t ToFlecs() => new() { + public ecs_term_id_t ToFlecs(IAllocator allocator) => new() { id = ID, - name = Name.FlecsToCString(), + name = allocator.AllocateCString(Name), trav = Traverse, flags = (ecs_flags32_t)(uint)Flags }; diff --git a/src/gaemstone/ECS/Universe+Lookup.cs b/src/gaemstone/ECS/Universe+Lookup.cs index 82f5e92..798a812 100644 --- a/src/gaemstone/ECS/Universe+Lookup.cs +++ b/src/gaemstone/ECS/Universe+Lookup.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.ECS; @@ -26,24 +27,24 @@ public unsafe partial class Universe public EntityRef? Lookup(Entity value) => GetOrNull(new(ecs_get_alive(this, value))); - public EntityRef? Lookup(string path) + public EntityRef? Lookup(EntityPath path) => Lookup(default, path); - public EntityRef? Lookup(Entity parent, string path) - => GetOrNull(new(ecs_lookup_path_w_sep( - this, parent, path.FlecsToCString(), ".", default, true))); + public EntityRef? Lookup(Entity parent, EntityPath path) + => GetOrNull(EntityPathExtensions.Lookup(this, parent, path)); public EntityRef? LookupSymbol(string symbol) - => GetOrNull(new(ecs_lookup_symbol( - this, symbol.FlecsToCString(), false))); - + { + using var alloc = TempAllocator.Lock(); + return GetOrNull(new(ecs_lookup_symbol(this, alloc.AllocateCString(symbol), false))); + } public EntityRef LookupOrThrow() => LookupOrThrow(typeof(T)); public EntityRef LookupOrThrow(Type type) => Lookup(type) ?? throw new EntityNotFoundException($"Entity of type {type} not found"); public EntityRef LookupOrThrow(Entity entity) => Lookup(entity) ?? throw new EntityNotFoundException($"Entity {entity} not alive"); - public EntityRef LookupOrThrow(string path) => Lookup(default, path) + public EntityRef LookupOrThrow(EntityPath path) => Lookup(default, path) ?? throw new EntityNotFoundException($"Entity '{path}' not found"); - public EntityRef LookupOrThrow(Entity parent, string path) => Lookup(parent, path) + public EntityRef LookupOrThrow(Entity parent, EntityPath path) => Lookup(parent, path) ?? throw new EntityNotFoundException($"Child entity of {parent} '{path}' not found"); public EntityRef LookupSymbolOrThrow(string symbol) => LookupSymbol(symbol) ?? throw new EntityNotFoundException($"Entity with symbol '{symbol}' not found"); diff --git a/src/gaemstone/ECS/Universe+Modules.cs b/src/gaemstone/ECS/Universe+Modules.cs index d0c5b02..fea1808 100644 --- a/src/gaemstone/ECS/Universe+Modules.cs +++ b/src/gaemstone/ECS/Universe+Modules.cs @@ -25,8 +25,6 @@ public class ModuleManager if (type.Get() is not ModuleAttribute attr) throw new Exception( $"Module {type} must be marked with ModuleAttribute"); - var moduleName = type.Get()?.Name ?? type.FullName!; - // Check if module type is static. if (type.IsAbstract && type.IsSealed) { @@ -34,32 +32,29 @@ public class ModuleManager // create entities, only look up existing ones to add type lookups // for use with the Lookup(Type) method. - if (attr.Name == null) throw new Exception( + if (attr.Path == null) throw new Exception( $"Existing module {type} must have ModuleAttribute.Name set"); - var entity = Universe.Lookup(attr.Name) ?? throw new Exception( - $"Existing module {type} with name '{attr.Name}' not found"); + var entity = Universe.Lookup(new EntityPath(true, attr.Path)) ?? throw new Exception( + $"Existing module {type} with name '{attr.Path}' not found"); // This implementation is pretty naive. It simply gets all nested // types which are tagged with [Entity] attribute or a subtype // thereof and creates a lookup mapping. No sanity checking. - foreach (var nested in type.GetNestedTypes()) { - if (nested.Get() is not EntityAttribute nestedAttr) continue; - if (nestedAttr.Name?.Contains('.') == true) throw new Exception( - $"EntityAttribute.Name for {type} must not contain a dot (path separator)"); - Universe.LookupOrThrow($"{moduleName}.{nestedAttr.Name ?? nested.Name}") - .CreateLookup(nested); - } + foreach (var nested in type.GetNestedTypes()) + if (nested.Get() is EntityAttribute nestedAttr) + Universe.LookupOrThrow(entity, nestedAttr.Name ?? nested.Name) + .CreateLookup(nested); return entity; } else { - var name = GetModuleName(type); - var module = new ModuleInfo(Universe, type, name); - _modules.Add(module.Entity, module); + var path = GetModulePath(type); + var module = new ModuleInfo(Universe, type, path); + _modules.Add(module.ModuleEntity, module); TryEnableModule(module); - return module.Entity; + return module.ModuleEntity; } } @@ -72,22 +67,36 @@ public class ModuleManager // Find other modules that might be missing this module as a dependency. foreach (var other in _modules.Values) { if (!other.IsActive) continue; - if (!other.UnmetDependencies.Contains(module.Entity)) continue; + if (!other.UnmetDependencies.Contains(module.ModuleEntity)) continue; // Move the just enabled module from unmet to met depedencies. - other.UnmetDependencies.Remove(module.Entity); + other.UnmetDependencies.Remove(module.ModuleEntity); other.MetDependencies.Add(module); TryEnableModule(other); } } - public static string GetModuleName(Type type) + public static EntityPath GetModulePath(Type type) { var attr = type.Get(); if (attr == null) throw new ArgumentException( $"Module {type} must be marked with ModuleAttribute", nameof(type)); - return attr.Name ?? type.FullName!; + + // 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); + } + + var fullPath = new EntityPath(true, attr.Path); + if (!fullPath.IsAbsolute) throw new ArgumentException( + $"Module {type} must have an absolute path (if specified)", nameof(type)); + return fullPath; } } @@ -95,49 +104,50 @@ internal class ModuleInfo { public Universe Universe { get; } public Type ModuleType { get; } - public string ModuleName { get; } + public EntityPath ModulePath { get; } - public EntityRef Entity { get; } + public EntityRef ModuleEntity { get; } public object? Instance { get; internal set; } public bool IsActive => Instance != null; public HashSet MetDependencies { get; } = new(); public HashSet UnmetDependencies { get; } = new(); - public ModuleInfo(Universe universe, Type type, string name) + public ModuleInfo(Universe universe, Type type, EntityPath path) { Universe = universe; ModuleType = type; - ModuleName = name; + ModulePath = path; if (ModuleType.IsAbstract || ModuleType.IsSealed) throw new Exception( $"Module {ModuleType} must not be abstract or sealed"); if (ModuleType.GetConstructor(Type.EmptyTypes) == null) throw new Exception( $"Module {ModuleType} must define public parameterless constructor"); - var entity = Universe.New(name).Add(); + var module = Universe.New(path).Add(); // Add module dependencies from [DependsOn] attributes. foreach (var dependsAttr in ModuleType.GetMultiple()) { - var dependency = Universe.Lookup(dependsAttr.Name) ?? - Universe.New(dependsAttr.Name).Add().Disable().Build(); + var dependsPath = ModuleManager.GetModulePath(dependsAttr.Type); + var dependency = Universe.Lookup(dependsPath) ?? + Universe.New(dependsPath).Add().Disable().Build(); var depModule = Universe.Modules.Lookup(dependency); if (depModule?.IsActive == true) MetDependencies.Add(depModule); - else { UnmetDependencies.Add(dependency); entity.Disable(); } + else { UnmetDependencies.Add(dependency); module.Disable(); } - entity.Add(dependency); + module.Add(dependency); } - Entity = entity.Build().CreateLookup(type); + ModuleEntity = module.Build().CreateLookup(type); } public void Enable() { - Entity.Enable(); + ModuleEntity.Enable(); Instance = Activator.CreateInstance(ModuleType)!; RegisterNestedTypes(); - (Instance as IModuleInitializer)?.Initialize(Entity); + (Instance as IModuleInitializer)?.Initialize(ModuleEntity); RegisterMethods(Instance); } @@ -145,8 +155,10 @@ internal class ModuleInfo { foreach (var type in ModuleType.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}'"); var name = attr.Name ?? type.Name; - var entity = Universe.New($"{ModuleName}.{name}", name); + var entity = ModuleEntity.NewChild(name).Symbol(name); switch (attr) { case TagAttribute: @@ -173,9 +185,9 @@ internal class ModuleInfo { foreach (var method in ModuleType.GetMethods()) { if (method.Has()) - Universe.RegisterSystem(instance, method).ChildOf(Entity); + Universe.RegisterSystem(instance, method).ChildOf(ModuleEntity); if (method.Has()) - Universe.RegisterObserver(instance, method).ChildOf(Entity); + Universe.RegisterObserver(instance, method).ChildOf(ModuleEntity); } } } diff --git a/src/gaemstone/ECS/Universe.cs b/src/gaemstone/ECS/Universe.cs index 8f49a06..494a63a 100644 --- a/src/gaemstone/ECS/Universe.cs +++ b/src/gaemstone/ECS/Universe.cs @@ -1,6 +1,5 @@ using System; using static flecs_hub.flecs; -using static flecs_hub.flecs.Runtime; namespace gaemstone.ECS; @@ -13,20 +12,18 @@ public unsafe partial class Universe public Universe(params string[] args) { - var argv = CStrings.CStringArray(args); - Handle = ecs_init_w_args(args.Length, argv); - CStrings.FreeCStrings(argv, args.Length); + Handle = ecs_init_w_args(args.Length, null); Modules = new(this); Modules.Register(typeof(Flecs.Core)); Modules.Register(typeof(Flecs.ObserverEvent)); Modules.Register(typeof(Flecs.SystemPhase)); - New("Game", "Game").Build().CreateLookup(); + New("Game").Symbol("Game").Build().CreateLookup(); } - public EntityBuilder New(string? name = null, string? symbol = null) - => new(this, name, symbol); + public EntityBuilder New(EntityPath? path = null) + => new(this, path); public bool Progress(TimeSpan delta) => ecs_progress(this, (float)delta.TotalSeconds); diff --git a/src/gaemstone/Flecs/Core.cs b/src/gaemstone/Flecs/Core.cs index c4618b8..39d6099 100644 --- a/src/gaemstone/Flecs/Core.cs +++ b/src/gaemstone/Flecs/Core.cs @@ -2,7 +2,7 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module(Name = "flecs.core")] +[Module("flecs", "core")] public static class Core { // Entity Tags diff --git a/src/gaemstone/Flecs/ObserverEvent.cs b/src/gaemstone/Flecs/ObserverEvent.cs index ec4c740..941552c 100644 --- a/src/gaemstone/Flecs/ObserverEvent.cs +++ b/src/gaemstone/Flecs/ObserverEvent.cs @@ -2,7 +2,7 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module(Name = "flecs.core")] +[Module("flecs", "core")] public static class ObserverEvent { [Entity] public struct OnAdd { } diff --git a/src/gaemstone/Flecs/SystemPhase.cs b/src/gaemstone/Flecs/SystemPhase.cs index 34f0dfb..808e1f0 100644 --- a/src/gaemstone/Flecs/SystemPhase.cs +++ b/src/gaemstone/Flecs/SystemPhase.cs @@ -2,7 +2,7 @@ using gaemstone.ECS; namespace gaemstone.Flecs; -[Module(Name = "flecs.pipeline")] +[Module("flecs", "pipeline")] public static class SystemPhase { [Entity] public struct PreFrame { } diff --git a/src/gaemstone/Flecs/Systems/Monitor.cs b/src/gaemstone/Flecs/Systems/Monitor.cs index dae75a8..bf4b5d0 100644 --- a/src/gaemstone/Flecs/Systems/Monitor.cs +++ b/src/gaemstone/Flecs/Systems/Monitor.cs @@ -1,17 +1,19 @@ using System.Runtime.InteropServices; using gaemstone.ECS; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module(Name = "flecs.monitor")] +[Module("flecs", "monitor")] public unsafe class Monitor : IModuleInitializer { - public void Initialize(EntityRef entity) + public void Initialize(EntityRef module) { - ecs_import_c(entity.Universe, new() { Data = new() { - Pointer = &MonitorImport } }, "FlecsMonitor"); + using var alloc = TempAllocator.Lock(); + ecs_import_c(module.Universe, new() { Data = new() { + Pointer = &MonitorImport } }, alloc.AllocateCString("FlecsMonitor")); } [UnmanagedCallersOnly] diff --git a/src/gaemstone/Flecs/Systems/Rest.cs b/src/gaemstone/Flecs/Systems/Rest.cs index 96481fc..8b39ac6 100644 --- a/src/gaemstone/Flecs/Systems/Rest.cs +++ b/src/gaemstone/Flecs/Systems/Rest.cs @@ -1,19 +1,23 @@ using System.Runtime.InteropServices; using gaemstone.ECS; +using gaemstone.Utility; using static flecs_hub.flecs; namespace gaemstone.Flecs.Systems; -[Module(Name = "flecs.rest")] +[Module("flecs", "rest")] public unsafe class Rest : IModuleInitializer { - public void Initialize(EntityRef entity) + public void Initialize(EntityRef module) { - ecs_import_c(entity.Universe, new() { Data = new() { - Pointer = &RestImport } }, "FlecsRest"); - entity.NewChild("Rest").Build() - .CreateLookup().Set(new EcsRest { port = 27750 }); + using (var alloc = TempAllocator.Lock()) + ecs_import_c(module.Universe, new() { Data = new() { + Pointer = &RestImport } }, alloc.AllocateCString("FlecsRest")); + + module.NewChild("Rest").Build() + .CreateLookup() + .Set(new EcsRest { port = 27750 }); } [UnmanagedCallersOnly] diff --git a/src/gaemstone/Utility/Allocators.cs b/src/gaemstone/Utility/Allocators.cs new file mode 100644 index 0000000..5dd009d --- /dev/null +++ b/src/gaemstone/Utility/Allocators.cs @@ -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 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 span = allocator.Allocate(orig.Length); orig.CopyTo(span); return span; } + + 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; + 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])); +} + +public sealed class TempAllocator + : IAllocator + , IDisposable +{ + public const int Capacity = 1024 * 1024; // 1 MB + private static readonly ThreadLocal _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 */ } +} diff --git a/src/gaemstone/Utility/CStringExtensions.cs b/src/gaemstone/Utility/CStringExtensions.cs index f7735ef..acb0ed1 100644 --- a/src/gaemstone/Utility/CStringExtensions.cs +++ b/src/gaemstone/Utility/CStringExtensions.cs @@ -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); } diff --git a/src/gaemstone/Utility/CollectionExtensions.cs b/src/gaemstone/Utility/CollectionExtensions.cs new file mode 100644 index 0000000..ac6a901 --- /dev/null +++ b/src/gaemstone/Utility/CollectionExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace gaemstone.Utility; + +public static class CollectionExtensions +{ + // public static TValue GetOrAdd(this IDictionary dict, + // TKey key, Func valueFactory) { } + + public static T? MaybeGet(this Span span, int index) + where T : struct => MaybeGet((ReadOnlySpan)span, index); + public static T? MaybeGet(this ReadOnlySpan span, int index) + where T : struct => (index >= 0 && index < span.Length) ? span[index] : null; +} diff --git a/src/gaemstone/Utility/IL/IterActionGenerator.cs b/src/gaemstone/Utility/IL/IterActionGenerator.cs index 0f3120b..4c82e0f 100644 --- a/src/gaemstone/Utility/IL/IterActionGenerator.cs +++ b/src/gaemstone/Utility/IL/IterActionGenerator.cs @@ -17,7 +17,7 @@ public unsafe class IterActionGenerator private static readonly PropertyInfo _iteratorDeltaTimeProp = typeof(Iterator).GetProperty(nameof(Iterator.DeltaTime))!; private static readonly PropertyInfo _iteratorCountProp = typeof(Iterator).GetProperty(nameof(Iterator.Count))!; private static readonly MethodInfo _iteratorFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.Field))!; - private static readonly MethodInfo _iteratorFieldIsSetMethod = typeof(Iterator).GetMethod(nameof(Iterator.FieldIsSet))!; + private static readonly MethodInfo _iteratorMaybeFieldMethod = typeof(Iterator).GetMethod(nameof(Iterator.MaybeField))!; private static readonly MethodInfo _iteratorEntityMethod = typeof(Iterator).GetMethod(nameof(Iterator.Entity))!; private static readonly MethodInfo _handleFromIntPtrMethod = typeof(GCHandle).GetMethod(nameof(GCHandle.FromIntPtr))!; @@ -111,23 +111,11 @@ public unsafe class IterActionGenerator IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); IL.Store(fieldLocals[i]); } else { - IL.Comment($"field_{i} = iterator.FieldIsSet({terms.Count}) " + - "? iterator.Field<{p.FieldType.Name}>({terms.Count}) : default"); - var elseLabel = IL.DefineLabel(); - var doneLabel = IL.DefineLabel(); + IL.Comment($"field_{i} = iterator.MaybeField<{p.FieldType.Name}>({terms.Count})"); IL.Load(iteratorArg); IL.LoadConst(terms.Count); - IL.Call(_iteratorFieldIsSetMethod); - IL.GotoIfFalse(elseLabel); - IL.Load(iteratorArg); - IL.LoadConst(terms.Count); - IL.Call(_iteratorFieldMethod.MakeGenericMethod(p.FieldType)); - IL.Store(fieldLocals[i]); - IL.Goto(doneLabel); - IL.MarkLabel(elseLabel); - IL.LoadAddr(fieldLocals[i]); - IL.Init(spanType); - IL.MarkLabel(doneLabel); + IL.Call(_iteratorMaybeFieldMethod.MakeGenericMethod(p.FieldType)); + IL.Store(fieldLocals[i]); } if (p.Kind == ParamKind.Nullable) { diff --git a/src/gaemstone/gaemstone.csproj b/src/gaemstone/gaemstone.csproj index 92bd2a3..af362dc 100644 --- a/src/gaemstone/gaemstone.csproj +++ b/src/gaemstone/gaemstone.csproj @@ -1,6 +1,7 @@ + preview net6.0 disable enable