illusion: ILLU-M9 illusion sample green

This commit is contained in:
hermes-agent committed 2026-10-30 09:18:37 -09:00
1 parent f09dd63b34
commit 7f6ac551cc
6 files changed
+307

No files matched your search

+1
View File
@@ -12,6 +12,7 @@
<Project Path="samples/Benchmark1M/Benchmark1M.csproj" />
<Project Path="samples/HotReloadBench/HotReloadBench.csproj" />
<Project Path="samples/ExportSample/ExportSample.csproj" />
<Project Path="samples/IllusionSample/IllusionSample.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/abi_codegen/abi_codegen.csproj" />
+6
View File
@@ -112,3 +112,9 @@ Chunk_Spike_Mover.g.cs(37,65): error CS1503: Argument 3: cannot convert from 'de
- `dotnet build Engine.slnx -c Release`: `Build succeeded. 0 Warning(s) 0 Error(s)`. `dotnet run --project managed/Engine.Tests -c Release --no-build`: `GeneratorDriverTests: all checks passed`, no FAIL.
- Sabotage check (`? "in dt" : "dt"` → `? "in dt" : "0f"`, rebuild, rerun): exact doc-expected 4 lines — `[FAIL] Mover row call forwards in Health + dt`, `[FAIL] Mover RunChunk: X += Health*dt, Y += Speed*dt for 4 rows`, `[FAIL] Mover RunChunk ignores a view with the wrong term count`, `GeneratorDriverTests: 3 FAILED`. Reverted + rebuilt green.
- Worktree note: this worktree's branch was stale at M5B (M6/M7A/M7B lived on sibling dispatch branches); fast-forwarded with `git merge --ff-only efd55cb` (merge-base confirmed HEAD was ancestor) per the reuse-`feat/illusion-layer-abi-4.2` convention before writing the test.
## M9 sample + CTest gate (ILLU-M9)
- Created `samples/IllusionSample/` verbatim from `docs/IMPLEMENTATION_PROMPT.md:3139-3443` fences (csproj 1442B, Components.cs 1887B, Program.cs 7836B); applied both doc edits (`Engine.slnx` IllusionSample line, `native/tests/CMakeLists.txt` engine_illusion_sample gate with `PASS_REGULAR_EXPRESSION "ILLUSION-SAMPLE-OK"`).
- Worktree note: this worktree's branch was stale at M5B; merged M6/M7A/M7B/M8 branch (`game-engine/t_4cce9280-illu-m8-generator-driver-test`, HEAD 61e99f6) into this worktree before writing files, per the reuse-`feat/illusion-layer-abi-4.2` convention.
- `dotnet build Engine.slnx -c Release`: `Build succeeded. 0 Warning(s) 0 Error(s)`. Direct run `dotnet samples/IllusionSample/bin/Release/net10.0/IllusionSample.dll <libengine_core.so>`: all 21 checks `[PASS]`, `0 managed bytes over 100 frames x 100k rows (got 0)`, `[info] 100k Mover rows: 0.176 ms/frame`, `ILLUSION-SAMPLE-OK`, exit 0.
- `ctest --preset ci-linux -R engine_illusion_sample --output-on-failure -V`: `ILLUSION-SAMPLE-OK`, `100% tests passed, 0 tests failed out of 2`. Full `ctest --preset ci-linux`: `100% tests passed, 0 tests failed out of 42`.
+13
View File
@@ -391,6 +391,19 @@ set_tests_properties(engine_hostfxr_smoke PROPERTIES
TIMEOUT 300
FIXTURES_REQUIRED benchmark1m_fixtures)
# ABI 4.2: Illusion Layer end-to-end gate. samples/IllusionSample is built
# by the same `dotnet build Engine.slnx` fixture; it loads engine_core by
# absolute path, runs Engine.Analyzers-generated systems on native Flecs
# columns, and prints ILLUSION-SAMPLE-OK (exit 0) when every check passes.
add_test(NAME engine_illusion_sample
COMMAND ${DOTNET_EXE}
"${CMAKE_SOURCE_DIR}/samples/IllusionSample/bin/Release/net10.0/IllusionSample.dll"
"$<TARGET_FILE:engine_core>")
set_tests_properties(engine_illusion_sample PROPERTIES
TIMEOUT 300
FIXTURES_REQUIRED benchmark1m_fixtures
PASS_REGULAR_EXPRESSION "ILLUSION-SAMPLE-OK")
# M5-04: one-click export fixtures — NativeAOT game + .pak staging.
# Publishes samples/ExportSample with PublishAot (build --no-build reuses
# the hostfxr fixture build above, so ILCompiler runs once per machine),
+62
View File
@@ -0,0 +1,62 @@
// Components.cs — user-style IComponent scripts. Engine.Analyzers emits
// Register/RunChunk for every struct that declares a Tick.
using Engine.Runtime;
namespace IllusionSample;
/// <summary>Read-only input for <see cref="Mover"/>; no Tick of its own.</summary>
public partial struct Health : IComponent
{
/// <summary>Hit points; doubles as an x-velocity in this sample.</summary>
public float Value;
}
/// <summary>Moves along x by Health.Value and along y by Speed.</summary>
public partial struct Mover : IComponent
{
/// <summary>Units per second along y.</summary>
public float Speed;
/// <summary>Position x.</summary>
public float X;
/// <summary>Position y.</summary>
public float Y;
/// <summary>Per-row update: two terms (self inout, Health in) plus dt.</summary>
public readonly void Tick(ref Mover self, in Health health, float dt)
{
self.X += health.Value * dt;
self.Y += self.Speed * dt;
}
}
/// <summary>Single-term system with an <c>in float</c> dt.</summary>
public partial struct Spinner : IComponent
{
/// <summary>Accumulated angle.</summary>
public float Angle;
/// <summary>Per-row update.</summary>
public readonly void Tick(ref Spinner self, in float dt) => self.Angle += dt;
}
/// <summary>Throws on its second increment of any row: proves fault isolation.</summary>
public partial struct Bomb : IComponent
{
/// <summary>Times this row ticked.</summary>
public int Ticks;
// Pre-allocated: ENG0001 forbids `new` inside Tick (hot path).
private static readonly InvalidOperationException Boom = new("Bomb fixture fault");
/// <summary>Per-row update that throws once Ticks reaches 2.</summary>
public readonly void Tick(ref Bomb self)
{
self.Ticks++;
if (self.Ticks == 2)
{
throw Boom;
}
}
}
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ABI 4.2: end-to-end gate for the Illusion Layer. User IComponent
structs are compiled by Engine.Analyzers into chunk runners, bound
through component_register/system_register, and run by the native
world. Run: dotnet IllusionSample.dll <path-to-engine_core-library> -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>IllusionSample</RootNamespace>
<AssemblyName>IllusionSample</AssemblyName>
<Description>Generated IComponent systems running on native Flecs columns (ABI 4.2).</Description>
<!-- The IComponent contract (M2-01) IS public blittable fields plus an
instance Tick(ref Self self, ...): CA1051/CA1822 fire on exactly
that shape, so user-script projects suppress them. -->
<NoWarn>$(NoWarn);CA1051;CA1822</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\managed\Engine.Bindings\Engine.Bindings.csproj" />
<ProjectReference Include="..\..\managed\Engine.Runtime\Engine.Runtime.csproj" />
<ProjectReference Include="..\..\managed\Engine.Analyzers\Engine.Analyzers.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
+195
View File
@@ -0,0 +1,195 @@
// Program.cs — ABI 4.2 Illusion Layer end-to-end gate.
//
// 1. Load engine_core from the absolute path in args[0] and validate the
// ABI table (same checks as the generated ApiLoader).
// 2. Register three GENERATED systems (Mover, Spinner, Bomb), spawn
// entities with Ecs.Set, run 3 frames, and check every row.
// 3. Fault isolation: Bomb throws inside Tick; the frame keeps running,
// Bomb.Faulted turns true, the other systems keep their results.
// 4. Zero-GC: 100,000 Movers, 100 frames, 0 managed bytes allocated.
// Prints ILLUSION-SAMPLE-OK and exits 0 on success; exits 1 on any failure.
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using Engine.Bindings;
using Engine.Runtime;
namespace IllusionSample;
internal static unsafe class Program
{
private static int s_failures;
internal static int Main(string[] args)
{
if (args.Length != 1 || !File.Exists(args[0]))
{
Console.Error.WriteLine("usage: IllusionSample <absolute path to the engine_core shared library>");
return 2;
}
try
{
var table = LoadApiTable(args[0]);
CheckLayout();
CheckBehaviour(table);
CheckZeroGc(table);
}
catch (Exception ex)
{
Console.Error.WriteLine($"IllusionSample FAILED: {ex}");
return 1;
}
Console.WriteLine(s_failures == 0 ? "ILLUSION-SAMPLE-OK" : $"IllusionSample: {s_failures} FAILURES");
return s_failures == 0 ? 0 : 1;
}
private static void CheckLayout()
{
Check(Layout.AlignOf<byte>() == 1, "AlignOf<byte> == 1");
Check(Layout.AlignOf<float>() == 4, "AlignOf<float> == 4");
Check(Layout.AlignOf<double>() == 8, "AlignOf<double> == 8");
Check(Layout.AlignOf<Vector128<float>>() == 16, "AlignOf<Vector128<float>> == 16");
Check(Layout.AlignOf<Mover>() == 4 && sizeof(Mover) == 12, "Mover is 12 bytes, align 4");
Check(Mover.ComponentSize == 12, "generated Mover.ComponentSize == 12");
}
private static void CheckBehaviour(NativeApi.ApiTable* table)
{
var world = table->EngineWorldCreate();
Check(world != null, "world_create");
try
{
Check(Mover.Register(table, world, null) == 0, "Mover.Register -> system 0");
Check(Spinner.Register(table, world, null) == 1, "Spinner.Register -> system 1");
Check(Bomb.Register(table, world, null) == 2, "Bomb.Register -> system 2");
const int movers = 1000;
var ids = new Entity[movers];
for (var i = 0; i < movers; i++)
{
ids[i] = Ecs.CreateEntity(table, world);
Ecs.Set(table, world, ids[i], new Mover { Speed = i, X = 0, Y = 0 });
if (i % 2 == 0)
{
Ecs.Set(table, world, ids[i], new Health { Value = 2.0f });
}
}
var spinner = Ecs.CreateEntity(table, world);
Ecs.Set(table, world, spinner, new Spinner { Angle = 0 });
var bombs = new Entity[3];
for (var i = 0; i < bombs.Length; i++)
{
bombs[i] = Ecs.CreateEntity(table, world);
Ecs.Set(table, world, bombs[i], new Bomb { Ticks = 0 });
}
for (var frame = 0; frame < 3; frame++)
{
Ecs.RunSystems(table, world, 0.5f);
}
var moverOk = true;
for (var i = 0; i < movers; i++)
{
var m = Ecs.Get<Mover>(table, world, ids[i]);
var wantX = i % 2 == 0 ? 3.0f : 0.0f;
// Rows without Health do not match the (Mover, Health) system.
var wantY = i % 2 == 0 ? i * 1.5f : 0.0f;
if (m.X != wantX || m.Y != wantY || m.Speed != i)
{
moverOk = false;
Console.Error.WriteLine($"row {i}: X={m.X} Y={m.Y} Speed={m.Speed} (want X={wantX} Y={wantY} Speed={i})");
break;
}
}
Check(moverOk, "Mover: 3 frames x dt 0.5 -> rows with Health X=3 Y=Speed*1.5; rows without untouched");
Check(Ecs.Get<Spinner>(table, world, spinner).Angle == 1.5f, "Spinner: in float dt -> Angle 1.5");
Check(Bomb.Faulted, "Bomb.Faulted after Tick threw");
Check(!Mover.Faulted && !Spinner.Faulted, "Mover/Spinner not faulted");
var bombTicks = string.Join(",", bombs.Select(b => Ecs.Get<Bomb>(table, world, b).Ticks));
Check(bombTicks == "2,1,1", $"Bomb ticks after fault == 2,1,1 (got {bombTicks})");
}
finally
{
table->EngineWorldDestroy(world);
}
}
private static void CheckZeroGc(NativeApi.ApiTable* table)
{
var world = table->EngineWorldCreate();
Check(world != null, "world_create (gc world)");
try
{
Check(Mover.Register(table, world, null) == 0, "Mover.Register on gc world");
for (var i = 0; i < 100_000; i++)
{
var e = Ecs.CreateEntity(table, world);
Ecs.Set(table, world, e, new Mover { Speed = 1 });
Ecs.Set(table, world, e, new Health { Value = 1 });
}
var dispatch = new NativeApi.FrameDispatch { Dt = 1.0f / 60.0f };
for (var i = 0; i < 10; i++)
{
_ = table->EngineSystemsRun(world, &dispatch);
}
GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true);
GC.WaitForPendingFinalizers();
var before = GC.GetTotalAllocatedBytes(precise: true);
var t0 = Stopwatch.GetTimestamp();
var rc = 0;
for (var i = 0; i < 100; i++)
{
rc |= table->EngineSystemsRun(world, &dispatch);
}
var t1 = Stopwatch.GetTimestamp();
var allocated = GC.GetTotalAllocatedBytes(precise: true) - before;
Check(rc == 0, "systems_run x100 returned 0");
Check(allocated == 0, $"0 managed bytes over 100 frames x 100k rows (got {allocated})");
Console.WriteLine($"[info] 100k Mover rows: {(t1 - t0) * 1000.0 / Stopwatch.Frequency / 100:F3} ms/frame");
}
finally
{
table->EngineWorldDestroy(world);
}
}
private static NativeApi.ApiTable* LoadApiTable(string libPath)
{
var handle = NativeLibrary.Load(Path.GetFullPath(libPath));
if (!NativeLibrary.TryGetExport(handle, "engine_get_api_table", out var addr))
{
throw new EntryPointNotFoundException("engine_get_api_table not exported.");
}
var table = ((delegate* unmanaged[Cdecl]<NativeApi.ApiTable*>)addr)();
if (table == null || table->Size < (ulong)sizeof(NativeApi.ApiTable))
{
throw new InvalidOperationException("engine_get_api_table returned null or a table older than the bindings.");
}
if (table->AbiVersionMajor != NativeApi.AbiVersionMajor || table->AbiVersionMinor != NativeApi.AbiVersionMinor)
{
throw new InvalidOperationException($"ABI mismatch: native {table->AbiVersionMajor}.{table->AbiVersionMinor} != managed {NativeApi.AbiVersionMajor}.{NativeApi.AbiVersionMinor}.");
}
Check(sizeof(NativeApi.EngineChunkColumns) == 120, "EngineChunkColumns is 120 bytes");
return table;
}
private static void Check(bool ok, string name)
{
Console.WriteLine($"[{(ok ? "PASS" : "FAIL")}] {name}");
if (!ok)
{
s_failures++;
}
}
}