illusion: ILLU-M6 Ecs helpers

This commit is contained in:
hermes-agent committed 2026-10-30 09:18:37 -09:00
1 parent 60141fba9f
commit 6a265a3263
2 files changed
+140

No files matched your search

+6
View File
@@ -88,3 +88,9 @@ Chunk_Spike_Mover.g.cs(37,65): error CS1503: Argument 3: cannot convert from 'de
- `dotnet run --project tools/abi_codegen -c Release -- .` prints `abi_codegen: 44 entries`, exit 0. Second run: `git diff` on NativeApi.g.cs unchanged (idempotent).
- TDD red: pre-test-edit `dotnet run --project managed/Engine.Tests` showed `AbiLayoutTests: 2 FAILED` (minor==1, ApiTable 312B) against the new 360B table. Applied 4 AbiLayoutTests edits (minor 1->2, ApiTable 312->360B + component_register @312 + systems_run @352 + ChunkColumns 120B, columns offsets 96/104/108/112, system_register Cdecl assertion). Post-edit: `AbiLayoutTests: all checks passed`, `grep -c FAIL` = 0.
- `grep -c SuppressGCTransition NativeApi.g.cs` = 3 (unchanged); systems_run entry is plain Cdecl; `Size = 120` appears once.
## M6 Ecs helpers (ILLU-M6)
- `managed/Engine.Runtime/Ecs.cs` created verbatim from `docs/IMPLEMENTATION_PROMPT.md:1919-2067` doc fence (programmatic fence diff: VERBATIM, 5043 bytes).
- `dotnet build managed/Engine.Runtime -c Release`: `Build succeeded. 0 Warning(s) 0 Error(s)`.
- TDD spike `build/spikes/ecs6/` (git-ignored, DELETED after run): called `Ecs.ComponentId<Mover>` twice on a live `EngineWorldCreate` world — `GREEN` (same non-zero id). Note: `ApiLoader.Load()` resolves `engine_core` by SONAME only, so the spike preloaded `/home/hermes/game-engine/build/ci-linux/native/libengine_core.so` via `NativeLibrary.Load(absolute)` and bound `engine_get_api_table` by export instead of using `ApiLoader.Load()`.
- `dotnet build Engine.slnx -c Release --nologo`: `Build succeeded. 0 Warning(s) 0 Error(s)`.
+134
View File
@@ -0,0 +1,134 @@
// Ecs.cs — ABI 4.2: typed helpers over the generic component/system tail.
//
// Every IComponent type T is registered with the native world by NAME:
// typeof(T).FullName (UTF-8, NUL-terminated), sizeof(T) and AlignOf<T>().
// The name bytes are built once per T (init-time allocation, cached in a
// static generic class); the frame path never touches this file.
//
// Generated systems (Engine.Analyzers) call ComponentId<T> while
// registering, so the names here and in generated code always agree.
using System.Text;
using Engine.Bindings;
namespace Engine.Runtime;
/// <summary>
/// Typed access to native components and entities through the ABI 4.2
/// generic tail. Init-time API: not for use inside system callbacks.
/// </summary>
public static unsafe class Ecs
{
/// <summary>
/// Registers <typeparamref name="T"/> with the world (idempotent) and
/// returns its native component id. Throws when the native world
/// already holds a component with this name but a different layout.
/// </summary>
public static ulong ComponentId<T>(NativeApi.ApiTable* table, NativeApi.EngineWorld* world)
where T : unmanaged
{
ArgumentNullException.ThrowIfNull(table);
ArgumentNullException.ThrowIfNull(world);
var name = ComponentName<T>.Utf8;
ulong id;
fixed (byte* p = name)
{
id = table->EngineComponentRegister(world, p, (uint)sizeof(T), (uint)Layout.AlignOf<T>());
}
if (id == 0)
{
throw new InvalidOperationException(
$"component_register failed for {typeof(T).FullName} (size {sizeof(T)}, align {Layout.AlignOf<T>()}): " +
"a component with this name and a different layout already exists, or the layout is invalid.");
}
return id;
}
/// <summary>Creates one empty entity. Throws on failure.</summary>
public static Entity CreateEntity(NativeApi.ApiTable* table, NativeApi.EngineWorld* world)
{
ArgumentNullException.ThrowIfNull(table);
ArgumentNullException.ThrowIfNull(world);
var id = table->EngineEntityCreate(world);
if (id == 0)
{
throw new InvalidOperationException("entity_create failed (null world, or called inside systems_run).");
}
return new Entity(id);
}
/// <summary>Adds or overwrites component <typeparamref name="T"/> on an entity.</summary>
public static void Set<T>(NativeApi.ApiTable* table, NativeApi.EngineWorld* world, Entity entity, in T value)
where T : unmanaged
{
var component = ComponentId<T>(table, world);
int rc;
fixed (T* p = &value)
{
rc = table->EngineEntitySet(world, entity.Id, component, p, (uint)sizeof(T));
}
if (rc != 0)
{
throw new InvalidOperationException($"entity_set<{typeof(T).FullName}> failed with code {rc}.");
}
}
/// <summary>Reads component <typeparamref name="T"/> of an entity. Throws when absent.</summary>
public static T Get<T>(NativeApi.ApiTable* table, NativeApi.EngineWorld* world, Entity entity)
where T : unmanaged
{
var component = ComponentId<T>(table, world);
T value = default;
var rc = table->EngineEntityGet(world, entity.Id, component, &value, (uint)sizeof(T));
if (rc != 0)
{
throw new InvalidOperationException($"entity_get<{typeof(T).FullName}> failed with code {rc}.");
}
return value;
}
/// <summary>Runs every generic system once with frame delta <paramref name="dt"/>.</summary>
public static void RunSystems(NativeApi.ApiTable* table, NativeApi.EngineWorld* world, float dt)
{
ArgumentNullException.ThrowIfNull(table);
ArgumentNullException.ThrowIfNull(world);
var dispatch = new NativeApi.FrameDispatch { Dt = dt };
var rc = table->EngineSystemsRun(world, &dispatch);
if (rc != 0)
{
throw new InvalidOperationException($"systems_run failed with code {rc}.");
}
}
private static class ComponentName<T>
where T : unmanaged
{
// FullName is non-null for every closed, non-generic-parameter type.
internal static readonly byte[] Utf8 = Encoding.UTF8.GetBytes(typeof(T).FullName + "\0");
}
}
/// <summary>Managed layout queries for blittable component types.</summary>
public static class Layout
{
/// <summary>
/// Alignment of <typeparamref name="T"/> as the runtime lays it out
/// inside another struct (the offset of a T placed after one byte).
/// </summary>
public static unsafe int AlignOf<T>()
where T : unmanaged
=> sizeof(AlignProbe<T>) - sizeof(T);
private struct AlignProbe<T>
where T : unmanaged
{
#pragma warning disable CS0169, IDE0051, CA1823 // fields exist only to be laid out
private byte _pad;
private T _value;
#pragma warning restore CS0169, IDE0051, CA1823
}
}