Files

164 lines
5.8 KiB
C#

// Program.cs — M5-04 ExportSample: minimal NativeAOT game entry.
//
// Cold-boot proof: preload native libs by absolute sibling path, load the
// ABI table, create a world, bulk-create a scene, run one frame through
// the real MovementSystem kernel, print timings, exit. The exporter
// measures wall time of THIS binary as the <1.0s cold-boot gate.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Engine.Bindings;
using Engine.Systems;
internal static class Program
{
internal static unsafe int Main(string[] args)
{
var total = Stopwatch.StartNew();
// Sibling-preload: the exporter stages libengine_core + libflecs
// next to this binary. NativeLibrary.Load by absolute path pins
// the handle, but bare-SONAME NativeLibrary.TryLoad("engine_core")
// still dlopens by SONAME — so register a DllImportResolver on the
// Bindings assembly that maps "engine_core" to the staged file.
// (NativeAOT-compatible: no DllImport stub needed; the resolver
// runs for NativeLibrary loads from the target assembly.)
var dir = AppContext.BaseDirectory;
string? corePath = null;
foreach (var f in new[] { "libengine_core.so.0.1.0", "libengine_core.so", "libengine_core.dylib", "engine_core.dll", })
{
var p = Path.Combine(dir, f);
if (File.Exists(p))
{
corePath = p;
break;
}
}
if (corePath == null)
{
Console.Error.WriteLine("export-sample: no staged engine_core next to binary.");
return 1;
}
string? flecsPath = null;
foreach (var f in new[] { "libflecs.so", "libflecs.dylib", "flecs.dll" })
{
var p = Path.Combine(dir, f);
if (File.Exists(p))
{
flecsPath = p;
break;
}
}
if (flecsPath != null)
{
try
{
NativeLibrary.Load(flecsPath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"export-sample: flecs preload failed: {ex.Message}");
return 1;
}
}
NativeApi.ApiTable* table;
{
// Direct-from-handle load (Benchmark1M LiveApiLoader precedent):
// bare-SONAME TryLoad does not probe the app dir, so resolve the
// export off the staged handle we just pinned by absolute path.
nint coreHandle;
try
{
coreHandle = NativeLibrary.Load(corePath);
}
catch (Exception ex)
{
Console.Error.WriteLine($"export-sample: core handle failed: {ex.GetType().Name}: {ex.Message}");
return 1;
}
if (!NativeLibrary.TryGetExport(coreHandle, "engine_get_api_table", out var addr))
{
Console.Error.WriteLine("export-sample: engine_get_api_table not exported.");
return 1;
}
var getTable = (delegate* unmanaged[Cdecl]<NativeApi.ApiTable*>)addr;
table = getTable();
if (table == null)
{
Console.Error.WriteLine("export-sample: engine_get_api_table returned null.");
return 1;
}
if (table->AbiVersionMajor != NativeApi.AbiVersionMajor
|| table->AbiVersionMinor != NativeApi.AbiVersionMinor
|| table->AbiVersionPatch != NativeApi.AbiVersionPatch)
{
Console.Error.WriteLine(
$"export-sample: ABI mismatch native {table->AbiVersionMajor}.{table->AbiVersionMinor}.{table->AbiVersionPatch} " +
$"!= managed {NativeApi.AbiVersionMajor}.{NativeApi.AbiVersionMinor}.{NativeApi.AbiVersionPatch}.");
return 1;
}
Console.WriteLine($"export-sample: ABI {table->AbiVersionMajor}.{table->AbiVersionMinor}.{table->AbiVersionPatch} table={table->Size}B");
}
{
var world = table->EngineWorldCreate();
if (world == null)
{
Console.Error.WriteLine("export-sample: world_create returned null.");
return 1;
}
try
{
// Scene: 10k entities, one registered script system.
var n = table->EngineWorldBulkCreate(world, 10_000);
Console.WriteLine($"export-sample: bulk_created={n}");
var rc = table->EngineRegisterSystem(
world,
null,
&MovementSystem.MoveUnrolled,
null);
if (rc != 0)
{
Console.Error.WriteLine($"export-sample: register_system failed: {rc}.");
return 1;
}
var frame = new NativeApi.FrameDispatch
{
Dt = 1.0f / 60.0f,
};
var t0 = Stopwatch.GetTimestamp();
var runRc = table->EngineRunSystems(world, &frame);
var frameUs = (Stopwatch.GetTimestamp() - t0) * 1_000_000.0 / Stopwatch.Frequency;
if (runRc != 0)
{
Console.Error.WriteLine($"export-sample: run_systems failed: {runRc}.");
return 1;
}
Console.WriteLine($"export-sample: frame_10k_1sys_us={frameUs:F1}");
}
finally
{
table->EngineWorldDestroy(world);
}
}
total.Stop();
Console.WriteLine($"export-sample: cold_boot_ms={total.Elapsed.TotalMilliseconds:F1}");
Console.WriteLine("EXPORT-SAMPLE-OK");
return 0;
}
}