EntityPath, allocator and resources refactor

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.Client
wip/source-generators
copygirl 2 years ago
parent c21726dd01
commit ff9e376f27
  1. 2
      src/Immersion/ObserverTest.cs
  2. 26
      src/Immersion/Program.cs
  3. 2
      src/flecs-cs
  4. 8
      src/gaemstone.Bloxel/Client/Systems/ChunkMeshGenerator.cs
  5. 4
      src/gaemstone.Bloxel/Utility/ChunkedOctree.cs
  6. 8
      src/gaemstone.Client/Components/RenderingComponents.cs
  7. 22
      src/gaemstone.Client/Components/ResourceComponents.cs
  8. 44
      src/gaemstone.Client/Resources.cs
  9. 0
      src/gaemstone.Client/Resources/default.fs.glsl
  10. 0
      src/gaemstone.Client/Resources/default.vs.glsl
  11. 8
      src/gaemstone.Client/Systems/MeshManager.cs
  12. 57
      src/gaemstone.Client/Systems/Renderer.cs
  13. 63
      src/gaemstone.Client/Systems/TextureManager.cs
  14. 78
      src/gaemstone.Client/TextureManager.cs
  15. 4
      src/gaemstone.Client/gaemstone.Client.csproj
  16. 25
      src/gaemstone/ECS/EntityBuilder.cs
  17. 203
      src/gaemstone/ECS/EntityPath.cs
  18. 12
      src/gaemstone/ECS/EntityRef.cs
  19. 1
      src/gaemstone/ECS/EntityType.cs
  20. 44
      src/gaemstone/ECS/Filter.cs
  21. 3
      src/gaemstone/ECS/Identifier.cs
  22. 1
      src/gaemstone/ECS/IdentifierRef.cs
  23. 6
      src/gaemstone/ECS/Iterator.cs
  24. 18
      src/gaemstone/ECS/Module.cs
  25. 21
      src/gaemstone/ECS/Observer.cs
  26. 21
      src/gaemstone/ECS/Query.cs
  27. 13
      src/gaemstone/ECS/Rule.cs
  28. 47
      src/gaemstone/ECS/System.cs
  29. 15
      src/gaemstone/ECS/Term.cs
  30. 19
      src/gaemstone/ECS/Universe+Lookup.cs
  31. 82
      src/gaemstone/ECS/Universe+Modules.cs
  32. 11
      src/gaemstone/ECS/Universe.cs
  33. 2
      src/gaemstone/Flecs/Core.cs
  34. 2
      src/gaemstone/Flecs/ObserverEvent.cs
  35. 2
      src/gaemstone/Flecs/SystemPhase.cs
  36. 10
      src/gaemstone/Flecs/Systems/Monitor.cs
  37. 16
      src/gaemstone/Flecs/Systems/Rest.cs
  38. 150
      src/gaemstone/Utility/Allocators.cs
  39. 37
      src/gaemstone/Utility/CStringExtensions.cs
  40. 14
      src/gaemstone/Utility/CollectionExtensions.cs
  41. 20
      src/gaemstone/Utility/IL/IterActionGenerator.cs
  42. 1
      src/gaemstone/gaemstone.csproj

@ -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!");
}

@ -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<Game>();
@ -35,7 +33,7 @@ window.Initialize();
window.Center();
universe.Modules.Register<gaemstone.Client.Systems.Windowing>();
game.Set(new Canvas(window.CreateOpenGL()));
game.Set(new Canvas(Silk.NET.OpenGL.ContextSourceExtensions.CreateOpenGL(window)));
game.Set(new GameWindow(window));
universe.Modules.Register<gaemstone.Components.TransformComponents>();
@ -43,7 +41,8 @@ universe.Modules.Register<gaemstone.Components.TransformComponents>();
universe.Modules.Register<gaemstone.Client.Components.RenderingComponents>();
universe.Modules.Register<gaemstone.Client.Systems.Renderer>();
TextureManager.Initialize(universe);
universe.Modules.Register<gaemstone.Client.Components.ResourceComponents>();
universe.Modules.Register<gaemstone.Client.Systems.TextureManager>();
universe.Modules.Register<gaemstone.Client.Systems.Input>();
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<gaemstone.Bloxel.Components.CoreComponents>();
universe.Modules.Register<gaemstone.Bloxel.Systems.BasicWorldGenerator>();
universe.Modules.Register<gaemstone.Bloxel.Client.Systems.ChunkMeshGenerator>();
var texture = TextureManager.Load(universe, "terrain.png");
var texture = universe.New("/Immersion/Resources/terrain.png").Add<Texture>().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>(texture)
.Build();
}

@ -1 +1 @@
Subproject commit f5eea6704075601a674e3d759fdadc306b75573d
Subproject commit 3f9cf9c3793337eabf8647db6a4ac44017f20cc3

@ -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];

@ -72,7 +72,7 @@ public class ChunkedOctree<T>
while (enumerator.MoveNext()) yield return enumerator.Current;
}
public class Enumerator
public sealed class Enumerator
: IEnumerator<(ChunkPos ChunkPos, T Value, float Weight)>
{
private readonly ChunkedOctree<T> _octree;
@ -107,7 +107,7 @@ public class ChunkedOctree<T>
}
public void Reset() => throw new NotSupportedException();
public void Dispose() { }
public void Dispose() { }
internal void SearchFrom(ZOrder nodePos)

@ -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);
}

@ -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());
}
}

@ -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<ushort> indices, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
@ -77,7 +77,7 @@ public static class MeshManager
return new(vao, indices.Length);
}
public static Mesh Create(Universe universe, ReadOnlySpan<Vector3D<float>> vertices,
public static MeshHandle Create(Universe universe, ReadOnlySpan<Vector3D<float>> vertices,
ReadOnlySpan<Vector3D<float>> normals, ReadOnlySpan<Vector2D<float>> uvs)
{
var GL = universe.LookupOrThrow<Game>().Get<Canvas>().GL;

@ -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<Game>().Get<Canvas>().GL;
var GL = module.Universe.LookupOrThrow<Game>().Get<Canvas>().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<float>(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<float>(transform, out var cameraTransform);
Matrix4X4.Invert<float>(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<GlobalTransform>(1);
var meshes = iter.Field<MeshHandle>(2);
// var texPairs = iter.MaybeField<Identifier>(3);
var textures = iter.MaybeField<TextureHandle>(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();

@ -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);
}

@ -7,6 +7,10 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../flecs-cs/src/cs/production/Flecs/Flecs.csproj" />
<ProjectReference Include="../gaemstone/gaemstone.csproj" />

@ -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; }
/// <summary>
/// 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.
/// </summary>
public string? Name { get; set; }
public EntityPath? Path { get; set; }
/// <summary>
/// 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.
/// </summary>
public string? Symbol { get; set; }
public EntityBuilder Symbol(string symbol) { _symbol = symbol; return this; }
private string? _symbol = null;
/// <summary>
/// When set to true, a low id (typically reserved for components)
@ -43,8 +45,8 @@ public class EntityBuilder
/// <summary> Actions to run once the entity has been created. </summary>
private readonly List<Action<EntityRef>> _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;

@ -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;
}
}
}

@ -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<Entity> 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; }

@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;

@ -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<Term> Terms { get; set; }
public IReadOnlyList<Term> Terms { get; }
public string? Expression { get; }
/// <summary>
/// Optional name of filter, used for debugging. If a filter is created
@ -55,9 +58,6 @@ public class FilterDesc
/// </summary>
public string? Name { get; set; }
/// <summary> Filter expression. Should not be set at the same time as terms. </summary>
public string? Expression { get; set; }
/// <summary>
/// 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
/// </summary>
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<ecs_term_t>(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;
}
}

@ -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();

@ -1,4 +1,5 @@
using System;
using gaemstone.Utility;
using static flecs_hub.flecs;
namespace gaemstone.ECS;

@ -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<T> MaybeField<T>(int index)
where T : unmanaged => FieldIsSet(index) ? Field<T>(index) : default;
public SpanToRef<T> FieldRef<T>(int index)
where T : class => new(Field<nint>(index));

@ -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);
}

@ -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<Iterator> callback)
FilterDesc filter, Entity @event, Action<Iterator> 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<ObserverAttribute>() ?? throw new ArgumentException(
"Observer must specify ObserverAttribute", nameof(method));
var filter = new FilterDesc();
FilterDesc filter;
Action<Iterator> 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<Iterator>)Delegate.CreateDelegate(
typeof(Action<Iterator>), 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);
}
}

@ -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;

@ -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);

@ -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<Iterator> callback)
QueryDesc query, Entity phase, Action<Iterator> callback)
{
query.Name = name;
var entity = universe.New((query.Name != null) ? new(query.Name) : null)
.Add<DependsOn>(phase)
.Add(phase)
.Build();
using var alloc = TempAllocator.Lock();
var desc = new ecs_system_desc_t {
query = query.ToFlecs(),
entity = universe.New(name)
.Add<DependsOn>(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<SystemAttribute>();
var query = new QueryDesc();
var attr = action.Method.Get<SystemAttribute>();
QueryDesc query;
if (action is Action<Iterator> 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<SystemAttribute>();
var query = new QueryDesc();
var attr = method.Get<SystemAttribute>();
QueryDesc query;
Action<Iterator> 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<Iterator>)Delegate.CreateDelegate(typeof(Action<Iterator>), 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]

@ -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
};

@ -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<T>() => 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");

@ -25,8 +25,6 @@ public class ModuleManager
if (type.Get<ModuleAttribute>() is not ModuleAttribute attr) throw new Exception(
$"Module {type} must be marked with ModuleAttribute");
var moduleName = type.Get<ModuleAttribute>()?.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<EntityAttribute>() 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<EntityAttribute>() 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<ModuleAttribute>();
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<ModuleInfo> MetDependencies { get; } = new();
public HashSet<Entity> 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<Module>();
var module = Universe.New(path).Add<Module>();
// Add module dependencies from [DependsOn] attributes.
foreach (var dependsAttr in ModuleType.GetMultiple<DependsOnAttribute>()) {
var dependency = Universe.Lookup(dependsAttr.Name) ??
Universe.New(dependsAttr.Name).Add<Module>().Disable().Build();
var dependsPath = ModuleManager.GetModulePath(dependsAttr.Type);
var dependency = Universe.Lookup(dependsPath) ??
Universe.New(dependsPath).Add<Module>().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<DependsOn>(dependency);
module.Add<DependsOn>(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<EntityAttribute>() 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<SystemAttribute>())
Universe.RegisterSystem(instance, method).ChildOf(Entity);
Universe.RegisterSystem(instance, method).ChildOf(ModuleEntity);
if (method.Has<ObserverAttribute>())
Universe.RegisterObserver(instance, method).ChildOf(Entity);
Universe.RegisterObserver(instance, method).ChildOf(ModuleEntity);
}
}
}

@ -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<Game>();
New("Game").Symbol("Game").Build().CreateLookup<Game>();
}
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);

@ -2,7 +2,7 @@ using gaemstone.ECS;
namespace gaemstone.Flecs;
[Module(Name = "flecs.core")]
[Module("flecs", "core")]
public static class Core
{
// Entity Tags

@ -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 { }

@ -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 { }

@ -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]

@ -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<EcsRest>().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<EcsRest>()
.Set(new EcsRest { port = 27750 });
}
[UnmanagedCallersOnly]

@ -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;
}

@ -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) {

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<LangVersion>preview</LangVersion>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>

Loading…
Cancel
Save