mirror of
https://github.com/Heretek-AI/game-engine.git
synced 2026-11-07 10:55:58 -10:00
illusion: ILLU-M9 illusion sample green
This commit is contained in:
1 parent
f09dd63b34
commit
7f6ac551cc
6 files changed
+307
No files matched your search
@@ -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" />
|
||||
|
||||
@@ -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`.
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user