mirror of
https://github.com/Heretek-AI/game-engine.git
synced 2026-11-07 10:55:58 -10:00
illusion: ILLU-M6 Ecs helpers
This commit is contained in:
1 parent
60141fba9f
commit
6a265a3263
2 files changed
+140
No files matched your search
@@ -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)`.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user