mirror of
https://github.com/Heretek-AI/game-engine.git
synced 2026-11-07 10:55:58 -10:00
* parity: 136 Android player engine-side: touch input, lifecycle, ABI 4.28 * fix(input,ci): address CodeRabbit & SonarCloud findings in touch input and Android CI * parity: P7-07 Android CI review (independent probes, -no-window, read-only perms) * parity: P7-07 notes renumber ABI 4.24 -> 4.28 * parity: P7-07 ABI layout checks to 4.28 (1384B table, 1744B input) --------- Co-authored-by: Hermes <hermes@local> Co-authored-by: John Smith <[email protected]> Co-authored-by: Hermes <[email protected]>
1723 lines
87 KiB
C#
1723 lines
87 KiB
C#
// abi_codegen — M1-04: parses native/bridge/engine_abi.h and emits
|
|
// managed/Engine.Bindings/NativeApi.g.cs.
|
|
//
|
|
// The tool is intentionally a SMALL line parser, not a C parser: the ABI
|
|
// header follows a strict machine-friendly subset (see FORMAT below) so
|
|
// that adding an entry is one line in the header + rebuild. Anything the
|
|
// parser does not understand is a hard error (fail-closed: never emit a
|
|
// half-correct binding).
|
|
//
|
|
// FORMAT (subset of C the parser accepts):
|
|
// #define ENGINE_ABI_VERSION_MAJOR <int> (likewise MINOR/PATCH)
|
|
// #define ENGINE_ABI_COLUMN_STRIDE <int>
|
|
// typedef struct EngineApiTable { uint64_t size; int32_t abi_version_*;
|
|
// int32_t column_stride; <FnTypedef> <field>; ... } EngineApiTable;
|
|
// -- the field ORDER is the emission order; every Fn typedef must be
|
|
// referenced (stale typedefs are a hard error).
|
|
// typedef <ret> (*<Name>Fn)(<params>); -- function-pointer entries
|
|
// where <ret>/<params> use only: void, int32_t, uint64_t, float,
|
|
// const char*, EngineWorld*, const EcsChunkView*, const FrameDispatch*,
|
|
// ManagedSystemCallback, void*.
|
|
// typedef struct EcsChunkView { <ctype> <name>; ... } EcsChunkView;
|
|
// ctype := const-qualified pointer | int32_t | void* (user_data)
|
|
// Component structs (EngineLocalTransform/EngineWorldTransform) may use
|
|
// only float scalars and float arrays; emission preserves header order.
|
|
// ManagedSystemCallback typedef line is recognized and skipped
|
|
// (the C# side declares the UnmanagedCallersOnly signature by hand).
|
|
//
|
|
// EMIT: NativeApi.g.cs with
|
|
// * version constants + Expected* asserts,
|
|
// * blittable LocalTransform / WorldTransform / EcsChunkView structs with
|
|
// explicit Sequential layout + Pack, and a compile-time size guard,
|
|
// * an unsafe ApiTable struct mirroring the header's field order, whose
|
|
// function pointers use SELECTIVE suppression: only register_system
|
|
// (a guaranteed non-blocking store) is
|
|
// `delegate* unmanaged[Cdecl, SuppressGCTransition]`; run/propagate/
|
|
// lifecycle calls are plain `Cdecl` because they may block on worker
|
|
// completion, and suppressing the GC transition across a blocking
|
|
// native call can stall the runtime,
|
|
// * a loader that resolves engine_get_api_table via NativeLibrary and
|
|
// validates size/version/stride before first use.
|
|
//
|
|
// Usage:
|
|
// abi_codegen <repo-root> (defaults to repo root inferred from CWD)
|
|
// Reads native/bridge/include/engine/bridge/engine_abi.h (or the legacy
|
|
// native/bridge/engine_abi.h), writes managed/Engine.Bindings/NativeApi.g.cs.
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace AbiCodegen;
|
|
|
|
internal sealed record AbiVersion(int Major, int Minor, int Patch);
|
|
internal sealed record StructField(string CType, string Name, int ArrayLen, int ArrayLen2 = 0);
|
|
internal sealed record StructDef(string Name, List<StructField> Fields);
|
|
internal sealed record FnParam(string CType, string Name);
|
|
internal sealed record FnDef(string Name, string Return, List<FnParam> Params);
|
|
|
|
internal static class Program
|
|
{
|
|
private static readonly Regex DefineRe = new(@"^#define\s+(ENGINE_ABI_VERSION_MAJOR|ENGINE_ABI_VERSION_MINOR|ENGINE_ABI_VERSION_PATCH|ENGINE_ABI_COLUMN_STRIDE)\s+(\d+)\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex FnTypedefRe = new(@"^typedef\s+(.+?)\s*\(\*(\w+)\)\((.*)\)\s*;\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex GenericDefineRe = new(@"^#define\s+ENGINE_ABI_(MAX_TERMS|ACCESS_IN|ACCESS_INOUT|ERR_NULL_ARG|ERR_BAD_ARG|ERR_SIZE_MISMATCH|ERR_NOT_FOUND|ERR_IN_RUN|ERR_INTERNAL)\s+(-?\d+)\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex FieldRe = new(@"^([\w\s\*]+?)\s+(\w+)((?:\s*\[\s*\d+\s*\]){1,2})?\s*$", RegexOptions.Compiled);
|
|
private static readonly Regex DimRe = new(@"\[\s*(\d+)\s*\]", RegexOptions.Compiled);
|
|
|
|
internal static int Main(string[] args)
|
|
{
|
|
try
|
|
{
|
|
Run(args.Length > 0 ? args[0] : FindRepoRoot());
|
|
return 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine($"abi_codegen: error: {ex.Message}");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
|
|
while (dir != null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "Engine.slnx")))
|
|
{
|
|
return dir.FullName;
|
|
}
|
|
|
|
dir = dir.Parent;
|
|
}
|
|
|
|
throw new InvalidOperationException("cannot locate repo root (no Engine.slnx found); pass <repo-root> explicitly.");
|
|
}
|
|
|
|
private static void Run(string repoRoot)
|
|
{
|
|
var header = Path.Combine(repoRoot, "native", "bridge", "include", "engine", "bridge", "engine_abi.h");
|
|
if (!File.Exists(header))
|
|
{
|
|
header = Path.Combine(repoRoot, "native", "bridge", "engine_abi.h");
|
|
}
|
|
|
|
if (!File.Exists(header))
|
|
{
|
|
throw new InvalidOperationException($"ABI header not found: {header}");
|
|
}
|
|
|
|
var lines = File.ReadAllLines(header);
|
|
var version = ParseVersion(lines, header);
|
|
var stride = ParseStride(lines, header);
|
|
var structs = ParseStructs(lines, header);
|
|
var fns = ParseFns(lines, header);
|
|
|
|
var local = structs.Single(s => s.Name == "EngineLocalTransform");
|
|
var world = structs.Single(s => s.Name == "EngineWorldTransform");
|
|
var chunk = structs.Single(s => s.Name == "EcsChunkView");
|
|
AssertComponentLayout(local, world);
|
|
|
|
var generic = ParseGenericDefines(lines, header);
|
|
var cs = Emit(header, version, stride, structs, chunk, fns, lines, generic);
|
|
var outPath = Path.Combine(repoRoot, "managed", "Engine.Bindings", "NativeApi.g.cs");
|
|
File.WriteAllText(outPath, cs, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
|
Console.WriteLine($"abi_codegen: {fns.Count} entries -> {outPath}");
|
|
}
|
|
|
|
private static AbiVersion ParseVersion(string[] lines, string header)
|
|
{
|
|
var vals = new Dictionary<string, int>();
|
|
foreach (var line in lines)
|
|
{
|
|
var m = DefineRe.Match(line.Trim());
|
|
if (m.Success && m.Groups[1].Value.StartsWith("ENGINE_ABI_VERSION_", StringComparison.Ordinal))
|
|
{
|
|
vals[m.Groups[1].Value] = int.Parse(m.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
|
|
foreach (var key in new[] { "ENGINE_ABI_VERSION_MAJOR", "ENGINE_ABI_VERSION_MINOR", "ENGINE_ABI_VERSION_PATCH" })
|
|
{
|
|
if (!vals.TryGetValue(key, out _))
|
|
{
|
|
throw new InvalidOperationException($"{header}: missing {key}");
|
|
}
|
|
}
|
|
|
|
return new AbiVersion(vals["ENGINE_ABI_VERSION_MAJOR"], vals["ENGINE_ABI_VERSION_MINOR"], vals["ENGINE_ABI_VERSION_PATCH"]);
|
|
}
|
|
|
|
// ABI 4.2: the generic-tail #defines (MAX_TERMS, ACCESS_*, ERR_*). All
|
|
// nine are required; emission order is fixed by GenericConstNames.
|
|
private static Dictionary<string, int> ParseGenericDefines(string[] lines, string header)
|
|
{
|
|
var vals = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
foreach (var line in lines)
|
|
{
|
|
var m = GenericDefineRe.Match(line.Trim());
|
|
if (m.Success)
|
|
{
|
|
vals[m.Groups[1].Value] = int.Parse(m.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
|
|
foreach (var (key, _) in GenericConstNames)
|
|
{
|
|
if (!vals.ContainsKey(key))
|
|
{
|
|
throw new InvalidOperationException($"{header}: missing #define ENGINE_ABI_{key}");
|
|
}
|
|
}
|
|
|
|
if (vals["MAX_TERMS"] != 8)
|
|
{
|
|
throw new InvalidOperationException($"{header}: ENGINE_ABI_MAX_TERMS must be 8 (EngineChunkColumns arrays are [8]).");
|
|
}
|
|
|
|
return vals;
|
|
}
|
|
|
|
private static readonly (string Key, string CsName)[] GenericConstNames =
|
|
[
|
|
("MAX_TERMS", "MaxTerms"),
|
|
("ACCESS_IN", "AccessIn"),
|
|
("ACCESS_INOUT", "AccessInOut"),
|
|
("ERR_NULL_ARG", "ErrNullArg"),
|
|
("ERR_BAD_ARG", "ErrBadArg"),
|
|
("ERR_SIZE_MISMATCH", "ErrSizeMismatch"),
|
|
("ERR_NOT_FOUND", "ErrNotFound"),
|
|
("ERR_IN_RUN", "ErrInRun"),
|
|
("ERR_INTERNAL", "ErrInternal"),
|
|
];
|
|
|
|
private static int ParseStride(string[] lines, string header)
|
|
{
|
|
foreach (var line in lines)
|
|
{
|
|
var m = DefineRe.Match(line.Trim());
|
|
if (m.Success && m.Groups[1].Value == "ENGINE_ABI_COLUMN_STRIDE")
|
|
{
|
|
return int.Parse(m.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
}
|
|
|
|
throw new InvalidOperationException($"{header}: missing ENGINE_ABI_COLUMN_STRIDE");
|
|
}
|
|
|
|
private static List<StructDef> ParseStructs(string[] lines, string header)
|
|
{
|
|
var structs = new List<StructDef>();
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
var t = lines[i].Trim();
|
|
if (!t.StartsWith("typedef struct", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Multi-line: "typedef struct EcsChunkView {" ... "} Name;".
|
|
// A struct body ends at the line containing "} Name;" — a bare
|
|
// ';' also terminates single-field inline declarations, so scan
|
|
// for the closing brace instead.
|
|
// Opaque forward declarations ("typedef struct EngineWorld EngineWorld;")
|
|
// carry no body: skip them before the brace scan.
|
|
if (System.Text.RegularExpressions.Regex.IsMatch(
|
|
t, @"^typedef\s+struct\s+\w+\s+\w+\s*;$"))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var sb = new StringBuilder(StripComment(t));
|
|
while (!sb.ToString().Contains('}') && i + 1 < lines.Length)
|
|
{
|
|
sb.Append(' ').Append(StripComment(lines[++i].Trim()));
|
|
}
|
|
var closed = sb.ToString();
|
|
var braceAt = closed.IndexOf('}');
|
|
if (braceAt >= 0)
|
|
{
|
|
// Keep through the terminating "; " of THIS struct; later
|
|
// same-line typedefs belong to the next iteration.
|
|
var semiAt = closed.IndexOf(';', braceAt);
|
|
if (semiAt >= 0)
|
|
{
|
|
sb.Length = 0;
|
|
sb.Append(closed[..(semiAt + 1)]);
|
|
}
|
|
}
|
|
|
|
if (sb.ToString().Contains('}') && !sb.ToString().TrimEnd().EndsWith(';'))
|
|
{
|
|
// The closing "} Name;" may sit on the line after '}'.
|
|
while (i + 1 < lines.Length)
|
|
{
|
|
sb.Append(' ').Append(StripComment(lines[++i].Trim()));
|
|
if (sb.ToString().TrimEnd().EndsWith(';'))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Strip an optional alignas(...) between `struct` and the tag: the
|
|
// parser only needs the trailing typedef name.
|
|
var body = Regex.Replace(
|
|
sb.ToString(),
|
|
@"^typedef\s+struct\s+(?:\w+\s*\(\s*\d+\s*\)\s*)?",
|
|
"typedef struct ",
|
|
RegexOptions.Singleline);
|
|
var m = Regex.Match(body, @"^typedef\s+struct\s+(\w+)?\s*\{(.*)\}\s*(\w+)\s*;\s*$", RegexOptions.Singleline);
|
|
if (!m.Success)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: cannot parse struct: {body}");
|
|
}
|
|
|
|
var name = m.Groups[3].Value;
|
|
var fields = new List<StructField>();
|
|
foreach (var raw in m.Groups[2].Value.Split(';'))
|
|
{
|
|
var f = raw.Trim();
|
|
if (f.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var fm = FieldRe.Match(f);
|
|
if (!fm.Success)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: cannot parse field: {f}");
|
|
}
|
|
|
|
var dims = DimRe.Matches(fm.Groups[3].Value).Select(d => int.Parse(d.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture)).ToArray();
|
|
if (dims.Length > 2)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: field {fm.Groups[2].Value} has {dims.Length} dimensions (max 2).");
|
|
}
|
|
var arr = dims.Length > 0 ? dims[0] : 0;
|
|
var arr2 = dims.Length > 1 ? dims[1] : 0;
|
|
fields.Add(new StructField(NormType(fm.Groups[1].Value), fm.Groups[2].Value, arr, arr2));
|
|
}
|
|
|
|
structs.Add(new StructDef(name, fields));
|
|
}
|
|
|
|
foreach (var want in new[] { "EngineLocalTransform", "EngineWorldTransform", "EcsChunkView", "FrameDispatch", "EngineChunkColumns", "EngineInputState" })
|
|
{
|
|
if (structs.All(s => s.Name != want))
|
|
{
|
|
throw new InvalidOperationException($"{header}: missing struct {want}");
|
|
}
|
|
}
|
|
|
|
var dispatch = structs.Single(s => s.Name == "FrameDispatch");
|
|
AssertFrameDispatch(dispatch, header);
|
|
|
|
return structs;
|
|
}
|
|
|
|
private static string StripComment(string line)
|
|
{
|
|
var idx = line.IndexOf("//", StringComparison.Ordinal);
|
|
return idx < 0 ? line : line[..idx].TrimEnd();
|
|
}
|
|
|
|
private static string NormType(string c) => Regex.Replace(c.Trim(), @"\s+", " ");
|
|
|
|
private static List<FnDef> ParseFns(string[] lines, string header)
|
|
{
|
|
var fns = new List<FnDef>();
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
var t = lines[i].Trim();
|
|
if (!t.StartsWith("typedef", StringComparison.Ordinal) || !t.Contains("(*", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var fb = new StringBuilder(StripComment(t));
|
|
while (i + 1 < lines.Length && !FnTypedefRe.IsMatch(fb.ToString()))
|
|
{
|
|
fb.Append(' ').Append(StripComment(lines[++i].Trim()));
|
|
if (fb.Length > 2000)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
var m = FnTypedefRe.Match(fb.ToString());
|
|
if (!m.Success)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: cannot parse fn typedef: {fb}");
|
|
}
|
|
|
|
var name = m.Groups[2].Value;
|
|
if (name == "ManagedSystemCallback")
|
|
{
|
|
continue; // declared by hand on the C# side (UnmanagedCallersOnly).
|
|
}
|
|
|
|
var ret = NormType(m.Groups[1].Value);
|
|
var plist = m.Groups[3].Value.Trim();
|
|
var ps = new List<FnParam>();
|
|
if (plist != "void" && plist.Length != 0)
|
|
{
|
|
foreach (var raw in plist.Split(','))
|
|
{
|
|
var p = raw.Trim();
|
|
// Split "const EcsChunkView* view" -> type + last ident.
|
|
var pm = Regex.Match(p, @"^(.*\S)\s+(\w+)$");
|
|
if (!pm.Success)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: cannot parse param: {p}");
|
|
}
|
|
|
|
ps.Add(new FnParam(NormType(pm.Groups[1].Value), pm.Groups[2].Value));
|
|
}
|
|
}
|
|
|
|
ValidateCType(ret, header, i + 1);
|
|
foreach (var p in ps)
|
|
{
|
|
ValidateCType(p.CType, header, i + 1);
|
|
}
|
|
|
|
fns.Add(new FnDef(name, ret, ps));
|
|
}
|
|
|
|
var required = new[]
|
|
{
|
|
"EngineWorldCreateFn", "EngineWorldDestroyFn", "EngineWorldBulkCreateFn",
|
|
"EngineWorldPropagateFn", "EngineRegisterSystemFn", "EngineRunSystemsFn",
|
|
"EngineRenderPassCreateFn", "EngineRenderPassDestroyFn",
|
|
"EngineRenderPassAddImageFn", "EngineRenderPassAddPassFn",
|
|
"EngineRenderPassCompileFn", "EngineRenderPassRunFrameFn",
|
|
"EngineRenderPassPassResultFn", "EngineRenderPassFailedCountFn",
|
|
"EnginePhysicsRaycastFn", "EnginePhysicsSphereCastFn",
|
|
"EnginePhysicsOverlapSphereFn", "EnginePhysicsRaycastBatchFn",
|
|
"EngineEditorCreateFn", "EngineEditorDestroyFn",
|
|
"EngineEditorSpawnFn", "EngineEditorSetNameFn",
|
|
"EngineEditorSetParentFn", "EngineEditorAddLocalFn",
|
|
"EngineEditorSetLocalFn", "EngineEditorGetLocalFn",
|
|
"EngineEditorCountChildrenFn", "EngineEditorGetChildFn",
|
|
"EngineEditorCountRootsFn", "EngineEditorGetRootFn",
|
|
"EngineEditorIsPrefabInstanceFn", "EngineEditorBeginEditFn",
|
|
"EngineEditorEndEditFn", "EngineEditorSnapshotTakeFn",
|
|
"EngineEditorSnapshotRestoreFn", "EngineEditorSnapshotDiscardFn",
|
|
"EnginePhysics2DCreateFn", "EnginePhysics2DDestroyFn",
|
|
"EnginePhysics2DBodyBoxFn", "EnginePhysics2DBodyCircleFn",
|
|
"EnginePhysics2DBodyPolygonFn", "EnginePhysics2DBodyDestroyFn",
|
|
"EnginePhysics2DStepFn", "EnginePhysics2DDrainFn",
|
|
"EngineComponentRegisterFn", "EngineEntityCreateFn",
|
|
"EngineEntitySetFn", "EngineEntityGetFn",
|
|
"EngineEntityRemoveFn",
|
|
"EngineSystemRegisterFn", "EngineSystemsRunFn",
|
|
"EngineInputGetStateFn",
|
|
"EngineRendererCreateFn", "EngineRendererDestroyFn",
|
|
"EngineRendererFrameFn", "EngineRendererMeshCreateFn",
|
|
"EngineRendererScreenshotFn",
|
|
"EngineNavCreateFn", "EngineNavDestroyFn",
|
|
"EngineNavBakeFn", "EngineNavAgentAddFn",
|
|
"EngineNavAgentGotoFn", "EngineNavAgentUpdateFn",
|
|
"EngineNavAgentPosFn", "EngineNavAgentReachedFn",
|
|
"EngineRendererCreateOffscreenFn",
|
|
"EngineRendererRenderToBufferFn", "EngineRendererPickFn",
|
|
"EngineProjectDropAssetFn", "EngineEditorDeleteEntityFn",
|
|
"EngineRendererGpuTimeFn",
|
|
"EngineLogWriteFn", "EngineLogSetSinkFn", "EngineLogReadFn",
|
|
"EngineLogCountFn", "EngineLogClearFn", "EngineProfileResetFn",
|
|
"EngineProfileFrameFn", "EngineProfileCountFn",
|
|
"EngineProfileFrameTimeFn", "EngineProfileSystemTimeFn",
|
|
"EngineProfileSystemCountFn",
|
|
};
|
|
foreach (var r in required)
|
|
{
|
|
if (fns.All(f => f.Name != r))
|
|
{
|
|
throw new InvalidOperationException($"{header}: missing ABI entry {r}");
|
|
}
|
|
}
|
|
|
|
return fns;
|
|
}
|
|
|
|
private static void ValidateCType(string ctype, string header, int line)
|
|
{
|
|
var ok = ctype is "void" or "int32_t" or "uint32_t" or "uint64_t" or "void*" or "float"
|
|
or "const char*" or "const char**" or "const float*" or "float*"
|
|
or "uint32_t*" or "uint64_t*" or "EngineWorld*" or "const EngineWorld*"
|
|
or "EngineRenderPassRegistry*" or "const EngineRenderPassRegistry*"
|
|
or "EnginePhysicsWorld*" or "const EnginePhysicsWorld*"
|
|
or "const EngineRay*" or "EngineHit*"
|
|
or "EnginePhysics2DWorld*" or "const EnginePhysics2DWorld*"
|
|
or "const EngineRigidbody2D*" or "const EngineBoxCollider2D*"
|
|
or "const EngineCircleCollider2D*"
|
|
or "const EnginePolygonCollider2D*"
|
|
or "EngineCollisionEvent2D*"
|
|
or "EngineCollisionEvent*"
|
|
or "EngineEditorSession*" or "const EngineEditorSession*"
|
|
or "EngineEditorSnapshot*"
|
|
or "EngineLocalTransform*"
|
|
or "const EcsChunkView*" or "const FrameDispatch*"
|
|
or "ManagedSystemCallback" or "EngineRenderPassCallback"
|
|
or "const void*" or "const uint64_t*" or "const uint32_t*"
|
|
or "const EngineChunkColumns*" or "EngineSystemCallback"
|
|
or "EngineRenderer*" or "const EngineInputState*" or "uint8_t"
|
|
or "EngineNavWorld*" or "const EngineNavWorld*"
|
|
or "const EngineNavBox*" or "const EngineNavBakeConfig*"
|
|
or "EngineNavStats*" or "uint8_t*"
|
|
or "EngineCharacter*" or "const EngineCharacter*"
|
|
or "const EngineCharacterConfig*" or "EngineCharacterState*"
|
|
or "int32_t*" or "char*"
|
|
or "EngineLogSinkCallback"
|
|
or "EngineAudioWorld*" or "const EngineAudioWorld*"
|
|
or "double" or "double*" or "EngineUi*" or "const EngineUi*"
|
|
or "EngineTimeline*" or "const EngineTimelineClip*" or "const EngineTimeline*"
|
|
or "EngineTimelineClip*" or "EngineTimelineActiveClip*" or "EngineTimelinePlayer*"
|
|
or "const EngineTimelinePlayer*" or "EngineAnimator*" or "const EngineAnimator*"
|
|
or "EngineFieldDesc*" or "const EngineFieldDesc*"
|
|
or "uint16_t"
|
|
or "EngineNetSession*" or "const EngineNetSession*"
|
|
or "EngineNetTransformRow*" or "const EngineNetTransformRow*";
|
|
if (!ok)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{line}: unsupported ABI type '{ctype}' (extend the generator's type map deliberately).");
|
|
}
|
|
}
|
|
|
|
private static void AssertComponentLayout(StructDef local, StructDef world)
|
|
{
|
|
// EngineLocalTransform must be exactly 16 floats = 64 bytes.
|
|
var floats = 0;
|
|
foreach (var f in local.Fields)
|
|
{
|
|
if (!f.CType.EndsWith("float", StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException($"EngineLocalTransform: field {f.Name} must be float, got {f.CType}");
|
|
}
|
|
|
|
floats += f.ArrayLen == 0 ? 1 : f.ArrayLen;
|
|
}
|
|
|
|
if (floats != 16)
|
|
{
|
|
throw new InvalidOperationException($"EngineLocalTransform: must total 16 floats (64B), got {floats}");
|
|
}
|
|
|
|
if (world.Fields.Count != 1 || world.Fields[0].CType != "float" || world.Fields[0].ArrayLen != 16)
|
|
{
|
|
throw new InvalidOperationException("EngineWorldTransform: must be exactly 'float m[16]'");
|
|
}
|
|
}
|
|
|
|
private static void AssertFrameDispatch(StructDef dispatch, string header)
|
|
{
|
|
// FrameDispatch is exactly one float (dt) = 4 bytes. A hand-emitted
|
|
// 4B mirror covers it; anything wider is a deliberate ABI decision.
|
|
if (dispatch.Fields.Count != 1 || dispatch.Fields[0].CType != "float" || dispatch.Fields[0].ArrayLen != 0)
|
|
{
|
|
throw new InvalidOperationException($"{header}: FrameDispatch must be exactly 'float dt'");
|
|
}
|
|
}
|
|
|
|
// C type -> C# type (return/param position; void stays void).
|
|
private static string CsType(string ctype) => ctype switch
|
|
{
|
|
"void" => "void",
|
|
"int32_t" => "int",
|
|
"uint32_t" => "uint",
|
|
"uint64_t" => "ulong",
|
|
"float" => "float",
|
|
"void*" => "void*",
|
|
"const char*" => "byte*",
|
|
"const char**" => "byte**",
|
|
"EngineWorld*" => "EngineWorld*",
|
|
"const EngineWorld*" => "EngineWorld*",
|
|
"EngineRenderPassRegistry*" => "EngineRenderPassRegistry*",
|
|
"const EngineRenderPassRegistry*" => "EngineRenderPassRegistry*",
|
|
"const EcsChunkView*" => "EcsChunkView*",
|
|
"const FrameDispatch*" => "FrameDispatch*",
|
|
"EnginePhysicsWorld*" => "EnginePhysicsWorld*",
|
|
"const EnginePhysicsWorld*" => "EnginePhysicsWorld*",
|
|
"EnginePhysics2DWorld*" => "EnginePhysics2DWorld*",
|
|
"const EnginePhysics2DWorld*" => "EnginePhysics2DWorld*",
|
|
"const EngineRigidbody2D*" => "Rigidbody2D*",
|
|
"const EngineBoxCollider2D*" => "BoxCollider2D*",
|
|
"const EngineCircleCollider2D*" => "CircleCollider2D*",
|
|
"const EnginePolygonCollider2D*" => "PolygonCollider2D*",
|
|
"EngineCollisionEvent2D*" => "CollisionEvent2D*",
|
|
"EngineCollisionEvent*" => "CollisionEvent*",
|
|
"const EngineRay*" => "Ray*",
|
|
"EngineHit*" => "RaycastHit*",
|
|
"uint32_t*" => "uint*",
|
|
"uint64_t*" => "ulong*",
|
|
"int32_t*" => "int*",
|
|
"char*" => "byte*",
|
|
"ManagedSystemCallback" => "delegate* unmanaged[Cdecl]<EcsChunkView*, float, void>",
|
|
"EngineRenderPassCallback" => "delegate* unmanaged[Cdecl]<uint, void*, int>",
|
|
"const float*" => "float*",
|
|
"float*" => "float*",
|
|
"EngineEditorSession*" => "EngineEditorSession*",
|
|
"const EngineEditorSession*" => "EngineEditorSession*",
|
|
"EngineEditorSnapshot*" => "EngineEditorSnapshot*",
|
|
"EngineLocalTransform*" => "LocalTransform*",
|
|
"const void*" => "void*",
|
|
"const uint64_t*" => "ulong*",
|
|
"const uint32_t*" => "uint*",
|
|
"const EngineChunkColumns*" => "EngineChunkColumns*",
|
|
"EngineSystemCallback" => "delegate* unmanaged[Cdecl]<EngineChunkColumns*, float, void>",
|
|
"EngineLogSinkCallback" => "delegate* unmanaged[Cdecl]<int, byte*, void*, void>",
|
|
"EngineRenderer*" => "EngineRenderer*",
|
|
"const EngineInputState*" => "InputState*",
|
|
"EngineNavWorld*" => "EngineNavWorld*",
|
|
"const EngineNavWorld*" => "EngineNavWorld*",
|
|
"const EngineNavBox*" => "NavBox*",
|
|
"const EngineNavBakeConfig*" => "NavBakeConfig*",
|
|
"EngineNavStats*" => "NavStats*",
|
|
"EngineCharacter*" => "EngineCharacter*",
|
|
"const EngineCharacter*" => "EngineCharacter*",
|
|
"const EngineCharacterConfig*" => "CharacterConfig*",
|
|
"EngineCharacterState*" => "CharacterState*",
|
|
"uint8_t*" => "byte*",
|
|
"EngineAudioWorld*" => "EngineAudioWorld*",
|
|
"const EngineAudioWorld*" => "EngineAudioWorld*",
|
|
"double" => "double",
|
|
"double*" => "double*",
|
|
"EngineUi*" => "EngineUi*",
|
|
"const EngineUi*" => "EngineUi*",
|
|
"EngineTimeline*" => "EngineTimeline*",
|
|
"const EngineTimelineClip*" => "TimelineClip*",
|
|
"const EngineTimeline*" => "EngineTimeline*",
|
|
"EngineTimelineClip*" => "TimelineClip*",
|
|
"EngineTimelineActiveClip*" => "TimelineActiveClip*",
|
|
"EngineTimelinePlayer*" => "EngineTimelinePlayer*",
|
|
"const EngineTimelinePlayer*" => "EngineTimelinePlayer*",
|
|
"EngineAnimator*" => "EngineAnimator*",
|
|
"const EngineAnimator*" => "EngineAnimator*",
|
|
"EngineFieldDesc*" => "FieldDesc*",
|
|
"const EngineFieldDesc*" => "FieldDesc*",
|
|
"uint16_t" => "ushort",
|
|
"EngineNetSession*" => "EngineNetSession*",
|
|
"const EngineNetSession*" => "EngineNetSession*",
|
|
"EngineNetTransformRow*" => "NetTransformRow*",
|
|
"const EngineNetTransformRow*" => "NetTransformRow*",
|
|
_ => throw new InvalidOperationException($"no C# mapping for '{ctype}'"),
|
|
};
|
|
|
|
private static string Pascal(string snake)
|
|
{
|
|
var parts = snake.Split('_', StringSplitOptions.RemoveEmptyEntries);
|
|
var sb = new StringBuilder();
|
|
foreach (var p in parts)
|
|
{
|
|
sb.Append(char.ToUpperInvariant(p[0]));
|
|
if (p.Length > 1)
|
|
{
|
|
sb.Append(p[1..]);
|
|
}
|
|
}
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string Emit(string header, AbiVersion v, int stride, List<StructDef> structs, StructDef chunk, List<FnDef> fns, string[] lines, Dictionary<string, int> generic)
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("// <auto-generated>");
|
|
sb.AppendLine("// NativeApi.g.cs — GENERATED by tools/abi_codegen from");
|
|
sb.AppendLine("// native/bridge/include/engine/bridge/engine_abi.h. DO NOT EDIT.");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $"// ABI {v.Major}.{v.Minor}.{v.Patch}, column stride {stride}.");
|
|
sb.AppendLine("// Regenerate: dotnet run --project tools/abi_codegen -- <repo-root>");
|
|
sb.AppendLine("// </auto-generated>");
|
|
sb.AppendLine("#nullable enable");
|
|
sb.AppendLine("using System.Runtime.CompilerServices;");
|
|
sb.AppendLine("using System.Runtime.InteropServices;");
|
|
sb.AppendLine();
|
|
sb.AppendLine("namespace Engine.Bindings;");
|
|
sb.AppendLine();
|
|
sb.AppendLine("/// <summary>");
|
|
sb.AppendLine("/// Managed mirror of the C99 ABI (engine_abi.h). Function");
|
|
sb.AppendLine("/// pointers use selective GC-transition suppression: only");
|
|
sb.AppendLine("/// <c>register_system</c> (a guaranteed non-blocking store) is");
|
|
sb.AppendLine("/// <c>delegate* unmanaged[Cdecl, SuppressGCTransition]</c>. Run,");
|
|
sb.AppendLine("/// propagate, and lifecycle calls use plain <c>Cdecl</c> because");
|
|
sb.AppendLine("/// they may block on worker completion, and suppressing the GC");
|
|
sb.AppendLine("/// transition across a blocking native call can stall the runtime.");
|
|
sb.AppendLine("/// Each entry JIT-compiles to a single raw <c>call</c> instruction");
|
|
sb.AppendLine("/// with no P/Invoke stub and no marshalling.");
|
|
sb.AppendLine("/// Blittable throughout: usable inside [UnmanagedCallersOnly] callbacks.");
|
|
sb.AppendLine("/// </summary>");
|
|
sb.AppendLine("public static unsafe class NativeApi");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public const int AbiVersionMajor = {v.Major};");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public const int AbiVersionMinor = {v.Minor};");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public const int AbiVersionPatch = {v.Patch};");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public const int ColumnStride = {stride};");
|
|
sb.AppendLine(" public const string NativeLibraryName = \"engine_core\";");
|
|
foreach (var (key, csName) in GenericConstNames)
|
|
{
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public const int {csName} = {generic[key]};");
|
|
}
|
|
|
|
sb.AppendLine();
|
|
var table = structs.Single(s => s.Name == "EngineApiTable");
|
|
EmitOpaqueHandles(sb);
|
|
EmitFieldDesc(sb, structs);
|
|
EmitPhysicsTypes(sb, structs);
|
|
EmitNavTypes(sb, structs);
|
|
EmitLocalTransform(sb, structs.Single(s => s.Name == "EngineLocalTransform"));
|
|
EmitWorldTransform(sb, structs.Single(s => s.Name == "EngineWorldTransform"));
|
|
EmitChunkView(sb, chunk);
|
|
EmitChunkColumns(sb, structs.Single(s => s.Name == "EngineChunkColumns"));
|
|
EmitInputState(sb, structs.Single(s => s.Name == "EngineInputState"));
|
|
EmitNetTransformRow(sb, structs.Single(s => s.Name == "EngineNetTransformRow"));
|
|
EmitCallback(sb, fns, header, lines);
|
|
EmitApiTable(sb, table, fns);
|
|
EmitLoader(sb);
|
|
sb.AppendLine("}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static void EmitLocalTransform(StringBuilder sb, StructDef local)
|
|
{
|
|
sb.AppendLine(" /// <summary>Mirror of EngineLocalTransform: 16 floats, 64 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 64)]");
|
|
sb.AppendLine(" public struct LocalTransform");
|
|
sb.AppendLine(" {");
|
|
EmitComponentFields(sb, local, "LocalTransform");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitWorldTransform(StringBuilder sb, StructDef world)
|
|
{
|
|
sb.AppendLine(" /// <summary>Mirror of EngineWorldTransform: row-major 4x4, 64 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 64)]");
|
|
sb.AppendLine(" public unsafe struct WorldTransform");
|
|
sb.AppendLine(" {");
|
|
EmitComponentFields(sb, world, "WorldTransform");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitComponentFields(StringBuilder sb, StructDef def, string owner)
|
|
{
|
|
// One declaration per field, header order preserved. Single-field
|
|
// lines keep future header diffs one-to-one with binding diffs.
|
|
foreach (var f in def.Fields)
|
|
{
|
|
if (f.CType == "float" && f.ArrayLen == 0)
|
|
{
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public float {Pascal(f.Name)};");
|
|
continue;
|
|
}
|
|
|
|
if (f.CType == "float" && f.ArrayLen > 0)
|
|
{
|
|
// Field-name rule: keep the header name when already public
|
|
// (WorldTransform.m -> M); single-letter buffers read wrong in
|
|
// lowercase. Everything else is private padding (_pad, _m).
|
|
var csName = f.Name switch
|
|
{
|
|
"m" => "M",
|
|
_ => f.Name.StartsWith('_') ? f.Name : "_" + f.Name.ToLowerInvariant(),
|
|
};
|
|
var vis = csName == "M" ? "public" : "private";
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" {vis} fixed float {csName}[{f.ArrayLen}];");
|
|
continue;
|
|
}
|
|
|
|
throw new InvalidOperationException($"{owner}.{f.Name}: unsupported component field type '{f.CType}' (components are float scalars/arrays only — extend deliberately).");
|
|
}
|
|
}
|
|
|
|
private static void EmitChunkView(StringBuilder sb, StructDef chunk)
|
|
{
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Mirror of EcsChunkView. The pointers alias native Flecs table");
|
|
sb.AppendLine(" /// columns owned by the native world; valid only for the callback.");
|
|
sb.AppendLine(" /// </summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EcsChunkView");
|
|
sb.AppendLine(" {");
|
|
foreach (var f in chunk.Fields)
|
|
{
|
|
var decl = f.Name switch
|
|
{
|
|
"local" => "public LocalTransform* Local;",
|
|
"world" => "public WorldTransform* World;",
|
|
"entities" => "public ulong* Entities;",
|
|
"count" => "public int Count;",
|
|
"stride_local" => "public int StrideLocal;",
|
|
"stride_world" => "public int StrideWorld;",
|
|
"user_data" => "public void* UserData;",
|
|
_ => throw new InvalidOperationException($"EcsChunkView: unknown field {f.Name} (update the emitter)."),
|
|
};
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" {decl}");
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitChunkColumns(StringBuilder sb, StructDef cols)
|
|
{
|
|
// Field list is ABI: fail closed on any drift from the 120-byte shape.
|
|
string[] wantNames = ["columns", "sizes", "entities", "count", "term_count", "user_data"];
|
|
string[] wantTypes = ["void*", "int32_t", "const uint64_t*", "int32_t", "int32_t", "void*"];
|
|
int[] wantArr = [8, 8, 0, 0, 0, 0];
|
|
if (cols.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineChunkColumns must have {wantNames.Length} fields (got {cols.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
var f = cols.Fields[i];
|
|
if (f.Name != wantNames[i] || f.CType != wantTypes[i] || f.ArrayLen != wantArr[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineChunkColumns field {i} must be '{wantTypes[i]} {wantNames[i]}{(wantArr[i] > 0 ? $"[{wantArr[i]}]" : string.Empty)}', got '{f.CType} {f.Name}{(f.ArrayLen > 0 ? $"[{f.ArrayLen}]" : string.Empty)}'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Mirror of EngineChunkColumns (ABI 4.2): one matched table slice for a");
|
|
sb.AppendLine(" /// generic system. Columns[i] is the base address of term i's column");
|
|
sb.AppendLine(" /// (cast to the term's component pointer); valid only for the callback.");
|
|
sb.AppendLine(" /// </summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 120)]");
|
|
sb.AppendLine(" public unsafe struct EngineChunkColumns");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public fixed ulong Columns[8];");
|
|
sb.AppendLine(" public fixed int Sizes[8];");
|
|
sb.AppendLine(" public ulong* Entities;");
|
|
sb.AppendLine(" public int Count;");
|
|
sb.AppendLine(" public int TermCount;");
|
|
sb.AppendLine(" public void* UserData;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
// P2-02: EngineInputState mirror. Field list is ABI: fail closed on any
|
|
// drift from the 1744-byte shape (3x512 scancode tables + mouse floats +
|
|
// button mask + 4x6 pad axes + 4 pad button masks + P7-07 touch tables
|
|
// + lifecycle latches).
|
|
private static void EmitInputState(StringBuilder sb, StructDef state)
|
|
{
|
|
string[] wantNames = ["keys", "pressed", "released", "mouse_x", "mouse_y", "mouse_dx", "mouse_dy", "mouse_buttons", "pad_axes", "pad_buttons", "touch_x", "touch_y", "touch_pressed", "paused", "surface_lost", "touch_count"];
|
|
string[] wantTypes = ["uint8_t", "uint8_t", "uint8_t", "float", "float", "float", "float", "uint32_t", "float", "uint32_t", "float", "float", "uint8_t", "uint8_t", "uint8_t", "uint8_t"];
|
|
int[] wantArr = [512, 512, 512, 0, 0, 0, 0, 0, 4, 4, 8, 8, 8, 0, 0, 0];
|
|
int[] wantArr2 = [0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0];
|
|
if (state.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineInputState must have {wantNames.Length} fields (got {state.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
var f = state.Fields[i];
|
|
if (f.Name != wantNames[i] || f.CType != wantTypes[i] || f.ArrayLen != wantArr[i] || f.ArrayLen2 != wantArr2[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineInputState field {i} must be '{wantTypes[i]} {wantNames[i]}" + (wantArr[i] > 0 ? $"[{wantArr[i]}]" : string.Empty) + (wantArr2[i] > 0 ? $"[{wantArr2[i]}]" : string.Empty) + $"', got '{f.CType} {f.Name}" + (f.ArrayLen > 0 ? $"[{f.ArrayLen}]" : string.Empty) + (f.ArrayLen2 > 0 ? $"[{f.ArrayLen2}]" : string.Empty) + "'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Mirror of EngineInputState (ABI 4.24): event-fed input snapshot, 1744 bytes.");
|
|
sb.AppendLine(" /// Keys/pressed/released index by SDL scancode (512 slots); pad_axes is");
|
|
sb.AppendLine(" /// 4 pads x 6 axes normalised to [-1, 1]. P7-07 tail: touch_x/touch_y");
|
|
sb.AppendLine(" /// (8 slots, normalised 0..1) + touch_pressed + paused/surface_lost");
|
|
sb.AppendLine(" /// latches + touch_count. Native owns the memory;");
|
|
sb.AppendLine(" /// managed views it zero-copy via input_get_state.");
|
|
sb.AppendLine(" /// </summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 1744)]");
|
|
sb.AppendLine(" public unsafe struct InputState");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public fixed byte Keys[512];");
|
|
sb.AppendLine(" public fixed byte Pressed[512];");
|
|
sb.AppendLine(" public fixed byte Released[512];");
|
|
sb.AppendLine(" public float MouseX;");
|
|
sb.AppendLine(" public float MouseY;");
|
|
sb.AppendLine(" public float MouseDx;");
|
|
sb.AppendLine(" public float MouseDy;");
|
|
sb.AppendLine(" public uint MouseButtons;");
|
|
sb.AppendLine(" public fixed float PadAxes[4 * 6];");
|
|
sb.AppendLine(" public fixed uint PadButtons[4];");
|
|
sb.AppendLine(" public fixed float TouchX[8];");
|
|
sb.AppendLine(" public fixed float TouchY[8];");
|
|
sb.AppendLine(" public fixed byte TouchPressed[8];");
|
|
sb.AppendLine(" public byte Paused;");
|
|
sb.AppendLine(" public byte SurfaceLost;");
|
|
sb.AppendLine(" public byte TouchCount;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
// P7-04: EngineNetTransformRow mirror (u64 entity + 10 replicated
|
|
// LocalTransform words = 48 bytes). Field list is ABI: fail closed.
|
|
private static void EmitNetTransformRow(StringBuilder sb, StructDef def)
|
|
{
|
|
string[] wantNames = ["entity", "words"];
|
|
string[] wantTypes = ["uint64_t", "float"];
|
|
int[] wantArr = [0, 10];
|
|
AssertStructFields(def, "EngineNetTransformRow", wantNames, wantTypes, wantArr);
|
|
sb.AppendLine(" /// <summary>Mirror of EngineNetTransformRow: entity + 10 replicated words, 48 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 48)]");
|
|
sb.AppendLine(" public unsafe struct NetTransformRow");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public ulong Entity;");
|
|
sb.AppendLine(" public fixed float Words[10];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitCallback(StringBuilder sb, List<FnDef> fns, string header, string[] lines)
|
|
{
|
|
var cb = ParseManagedCallback(lines, header);
|
|
var ps = string.Join(", ", cb.Params.Select(p => CsType(p.CType)));
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Managed per-chunk system signature (ABI 2: view + frame dt by value).");
|
|
sb.AppendLine(" /// Implement as a static method marked");
|
|
sb.AppendLine(" /// [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])];");
|
|
sb.AppendLine(" /// the body must not allocate, must not throw, and is thread-safe");
|
|
sb.AppendLine(" /// (runs on native worker threads).");
|
|
sb.AppendLine(" /// </summary>");
|
|
sb.AppendLine(" public unsafe struct ManagedSystem");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public delegate* unmanaged[Cdecl]<{ps}, void> Fn;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of FrameDispatch: per-dispatch parameters (frame dt).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 4)]");
|
|
sb.AppendLine(" public struct FrameDispatch");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Dt;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
_ = fns;
|
|
}
|
|
|
|
// ManagedSystemCallback is skipped by ParseFns (C# declares it by hand),
|
|
// but its shape is ABI: verify the header typedef matches exactly
|
|
// (const EcsChunkView*, float dt) so a silent header drift fails closed.
|
|
private static FnDef ParseManagedCallback(string[] lines, string header)
|
|
{
|
|
for (var i = 0; i < lines.Length; i++)
|
|
{
|
|
var t = lines[i].Trim();
|
|
if (!t.StartsWith("typedef", StringComparison.Ordinal) || !t.Contains("ManagedSystemCallback", StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var fb = new StringBuilder(StripComment(t));
|
|
while (i + 1 < lines.Length && !FnTypedefRe.IsMatch(fb.ToString()))
|
|
{
|
|
fb.Append(' ').Append(StripComment(lines[++i].Trim()));
|
|
if (fb.Length > 2000)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
var m = FnTypedefRe.Match(fb.ToString());
|
|
if (!m.Success || m.Groups[2].Value != "ManagedSystemCallback")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var ps = new List<FnParam>();
|
|
foreach (var raw in m.Groups[3].Value.Split(','))
|
|
{
|
|
var p = raw.Trim();
|
|
var pm = Regex.Match(p, @"^(.*\S)\s+(\w+)$");
|
|
if (!pm.Success)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: cannot parse ManagedSystemCallback param: {p}");
|
|
}
|
|
|
|
ps.Add(new FnParam(NormType(pm.Groups[1].Value), pm.Groups[2].Value));
|
|
}
|
|
|
|
var want = new[] { "const EcsChunkView*", "float" };
|
|
if (ps.Count != want.Length)
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: ManagedSystemCallback must take (const EcsChunkView*, float), got {ps.Count} params");
|
|
}
|
|
|
|
for (var k = 0; k < want.Length; k++)
|
|
{
|
|
if (ps[k].CType != want[k])
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: ManagedSystemCallback param {k} must be '{want[k]}', got '{ps[k].CType}'");
|
|
}
|
|
}
|
|
|
|
// The emitter hardcodes a void return in the delegate* shape, so
|
|
// a non-void header typedef must fail closed here, not emit a
|
|
// silently mismatched function pointer.
|
|
var ret = NormType(m.Groups[1].Value);
|
|
if (ret != "void")
|
|
{
|
|
throw new InvalidOperationException($"{header}:{i + 1}: ManagedSystemCallback must return void, got '{ret}'");
|
|
}
|
|
|
|
return new FnDef("ManagedSystemCallback", ret, ps);
|
|
}
|
|
|
|
throw new InvalidOperationException($"{header}: missing ManagedSystemCallback typedef");
|
|
}
|
|
|
|
private static void EmitPhysicsTypes(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// EngineRay (24B) and EngineHit (48B) mirror Engine.Runtime.Physics
|
|
// field order exactly; the loader pins sizes at startup.
|
|
var ray = structs.Single(s => s.Name == "EngineRay");
|
|
var hit = structs.Single(s => s.Name == "EngineHit");
|
|
if (ray.Fields.Count != 6 || ray.Fields.Any(f => f.CType != "float"))
|
|
{
|
|
throw new InvalidOperationException("EngineRay must be exactly 6 floats (ox,oy,oz,dx,dy,dz).");
|
|
}
|
|
|
|
string[] wantNames = ["px", "py", "pz", "fraction", "user_data", "nx", "ny", "nz", "body_id", "hit"];
|
|
string[] wantTypes = ["float", "float", "float", "float", "uint64_t", "float", "float", "float", "uint32_t", "uint32_t"];
|
|
if (hit.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineHit must have {wantNames.Length} fields (got {hit.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
if (hit.Fields[i].Name != wantNames[i] || hit.Fields[i].CType != wantTypes[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineHit field {i} must be '{wantTypes[i]} {wantNames[i]}', got '{hit.Fields[i].CType} {hit.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>Opaque native physics world handle (caller-owned engine::physics::PhysicsWorld, not the bare JPH::PhysicsSystem).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EnginePhysicsWorld");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
EmitCollisionEvent(sb, structs);
|
|
EmitPhysics2DTypes(sb, structs);
|
|
sb.AppendLine(" /// <summary>Mirror of EngineRay: origin + direction*length, 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct Ray");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Ox;");
|
|
sb.AppendLine(" public float Oy;");
|
|
sb.AppendLine(" public float Oz;");
|
|
sb.AppendLine(" public float Dx;");
|
|
sb.AppendLine(" public float Dy;");
|
|
sb.AppendLine(" public float Dz;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineHit: query hit, 48 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 48)]");
|
|
sb.AppendLine(" public struct RaycastHit");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Px;");
|
|
sb.AppendLine(" public float Py;");
|
|
sb.AppendLine(" public float Pz;");
|
|
sb.AppendLine(" public float Fraction;");
|
|
sb.AppendLine(" public ulong UserData;");
|
|
sb.AppendLine(" public float Nx;");
|
|
sb.AppendLine(" public float Ny;");
|
|
sb.AppendLine(" public float Nz;");
|
|
sb.AppendLine(" public uint BodyId;");
|
|
sb.AppendLine(" public uint Hit;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitFieldDesc(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// P3-04 field-descriptor mirror (ABI 4.24): EngineFieldDesc (16B).
|
|
// Field order must match engine_abi.h exactly; AbiLayoutTests pins
|
|
// offsets (name @0, offset @8, type @12) and the loader pins size.
|
|
var desc = structs.Single(s => s.Name == "EngineFieldDesc");
|
|
string[] wantNames = ["name", "offset", "type"];
|
|
string[] wantTypes = ["const char*", "uint32_t", "uint32_t"];
|
|
if (desc.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineFieldDesc must have {wantNames.Length} fields (got {desc.Fields.Count}).");
|
|
}
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
if (desc.Fields[i].Name != wantNames[i] || desc.Fields[i].CType != wantTypes[i] || desc.Fields[i].ArrayLen != 0)
|
|
{
|
|
throw new InvalidOperationException($"EngineFieldDesc field {i} must be '{wantTypes[i]} {wantNames[i]}', got '{desc.Fields[i].CType} {desc.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
sb.AppendLine(" /// <summary>Mirror of EngineFieldDesc: one described component field, 16 bytes (P3-04).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 16)]");
|
|
sb.AppendLine(" public unsafe struct FieldDesc");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public byte* Name;");
|
|
sb.AppendLine(" public uint Offset;");
|
|
sb.AppendLine(" public uint Type;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Field type enum (ENGINE_FIELD_*): 0 f32, 1 i32, 2 u32, 3 bool8, 4 vec2, 5 vec3, 6 vec4, 7 quat, 8 color4, 9 entity, 10 asset-ref.</summary>");
|
|
sb.AppendLine(" public enum FieldType : uint");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" F32 = 0,");
|
|
sb.AppendLine(" I32 = 1,");
|
|
sb.AppendLine(" U32 = 2,");
|
|
sb.AppendLine(" Bool8 = 3,");
|
|
sb.AppendLine(" Vec2 = 4,");
|
|
sb.AppendLine(" Vec3 = 5,");
|
|
sb.AppendLine(" Vec4 = 6,");
|
|
sb.AppendLine(" Quat = 7,");
|
|
sb.AppendLine(" Color4 = 8,");
|
|
sb.AppendLine(" Entity = 9,");
|
|
sb.AppendLine(" AssetRef = 10,");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitTimelineTypes(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// P5-07 sequencer mirrors (ABI 4.20): EngineTimelineClip (32B).
|
|
// Field order must match engine_abi.h exactly; the loader pins
|
|
// sizes at startup.
|
|
var clip = structs.Single(s => s.Name == "EngineTimelineClip");
|
|
string[] wantNames = ["start", "duration", "source_start", "reserved", "target_entity", "asset_id"];
|
|
string[] wantTypes = ["float", "float", "float", "uint32_t", "uint64_t", "uint64_t"];
|
|
if (clip.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineTimelineClip must have {wantNames.Length} fields (got {clip.Fields.Count}).");
|
|
}
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
if (clip.Fields[i].Name != wantNames[i] || clip.Fields[i].CType != wantTypes[i] || clip.Fields[i].ArrayLen != 0)
|
|
{
|
|
throw new InvalidOperationException($"EngineTimelineClip field {i} must be '{wantTypes[i]} {wantNames[i]}', got '{clip.Fields[i].CType} {clip.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
sb.AppendLine(" /// <summary>Mirror of EngineTimelineClip: clip placement + payload binding, 32 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 32)]");
|
|
sb.AppendLine(" public struct TimelineClip");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Start;");
|
|
sb.AppendLine(" public float Duration;");
|
|
sb.AppendLine(" public float SourceStart;");
|
|
sb.AppendLine(" public uint Reserved;");
|
|
sb.AppendLine(" public ulong TargetEntity;");
|
|
sb.AppendLine(" public ulong AssetId;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
// P5-07 sequencing mirror (ABI 4.21): EngineTimelineActiveClip
|
|
// (16B: track + clip handles, local time, blend weight).
|
|
var active = structs.Single(s => s.Name == "EngineTimelineActiveClip");
|
|
string[] wantActiveNames = ["track", "clip", "local", "weight"];
|
|
string[] wantActiveTypes = ["uint32_t", "uint32_t", "float", "float"];
|
|
if (active.Fields.Count != wantActiveNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineTimelineActiveClip must have {wantActiveNames.Length} fields (got {active.Fields.Count}).");
|
|
}
|
|
for (var i = 0; i < wantActiveNames.Length; i++)
|
|
{
|
|
if (active.Fields[i].Name != wantActiveNames[i] || active.Fields[i].CType != wantActiveTypes[i] || active.Fields[i].ArrayLen != 0)
|
|
{
|
|
throw new InvalidOperationException($"EngineTimelineActiveClip field {i} must be '{wantActiveTypes[i]} {wantActiveNames[i]}', got '{active.Fields[i].CType} {active.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
sb.AppendLine(" /// <summary>Mirror of EngineTimelineActiveClip: winning clip per track at time t, 16 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 16)]");
|
|
sb.AppendLine(" public struct TimelineActiveClip");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public uint Track;");
|
|
sb.AppendLine(" public uint Clip;");
|
|
sb.AppendLine(" public float Local;");
|
|
sb.AppendLine(" public float Weight;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitNavTypes(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
EmitCharacterTypes(sb, structs);
|
|
EmitTimelineTypes(sb, structs);
|
|
// Navigation mirrors (P6-03): EngineNavBox (24B), EngineNavBakeConfig
|
|
// (48B), EngineNavStats (8B), EngineNavAgent (64B). Field order must
|
|
// match engine_abi.h exactly; the loader pins sizes at startup.
|
|
var box = structs.Single(s => s.Name == "EngineNavBox");
|
|
if (box.Fields.Count != 6 || box.Fields.Any(f => f.CType != "float"))
|
|
{
|
|
throw new InvalidOperationException("EngineNavBox must be exactly 6 floats (cx,cy,cz,hx,hy,hz).");
|
|
}
|
|
var cfg = structs.Single(s => s.Name == "EngineNavBakeConfig");
|
|
if (cfg.Fields.Count != 12 || cfg.Fields.Any(f => f.CType != "float"))
|
|
{
|
|
throw new InvalidOperationException("EngineNavBakeConfig must be exactly 12 floats.");
|
|
}
|
|
var stats = structs.Single(s => s.Name == "EngineNavStats");
|
|
string[] wantStatsNames = ["polys", "detail_tris"];
|
|
if (stats.Fields.Count != wantStatsNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineNavStats must have {wantStatsNames.Length} fields (got {stats.Fields.Count}).");
|
|
}
|
|
for (var i = 0; i < wantStatsNames.Length; i++)
|
|
{
|
|
if (stats.Fields[i].Name != wantStatsNames[i] || stats.Fields[i].CType != "uint32_t")
|
|
{
|
|
throw new InvalidOperationException($"EngineNavStats field {i} must be 'uint32_t {wantStatsNames[i]}', got '{stats.Fields[i].CType} {stats.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
var agent = structs.Single(s => s.Name == "EngineNavAgent");
|
|
string[] wantAgentNames = ["gx", "gy", "gz", "max_speed", "stop_radius", "arrived", "idle", "pad"];
|
|
string[] wantAgentTypes = ["float", "float", "float", "float", "float", "int32_t", "int32_t", "float"];
|
|
int[] wantAgentArr = [0, 0, 0, 0, 0, 0, 0, 9];
|
|
if (agent.Fields.Count != wantAgentNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineNavAgent must have {wantAgentNames.Length} fields (got {agent.Fields.Count}).");
|
|
}
|
|
for (var i = 0; i < wantAgentNames.Length; i++)
|
|
{
|
|
if (agent.Fields[i].Name != wantAgentNames[i] || agent.Fields[i].CType != wantAgentTypes[i] || agent.Fields[i].ArrayLen != wantAgentArr[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineNavAgent field {i} must be '{wantAgentTypes[i]} {wantAgentNames[i]}', got '{agent.Fields[i].CType} {agent.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
sb.AppendLine(" /// <summary>Mirror of EngineNavBox: one static box obstacle, 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct NavBox");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Cx;");
|
|
sb.AppendLine(" public float Cy;");
|
|
sb.AppendLine(" public float Cz;");
|
|
sb.AppendLine(" public float Hx;");
|
|
sb.AppendLine(" public float Hy;");
|
|
sb.AppendLine(" public float Hz;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineNavBakeConfig: floor + raster params, 48 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 48)]");
|
|
sb.AppendLine(" public struct NavBakeConfig");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float FloorMinX;");
|
|
sb.AppendLine(" public float FloorMinY;");
|
|
sb.AppendLine(" public float FloorMinZ;");
|
|
sb.AppendLine(" public float FloorMaxX;");
|
|
sb.AppendLine(" public float FloorMaxY;");
|
|
sb.AppendLine(" public float FloorMaxZ;");
|
|
sb.AppendLine(" public float CellSize;");
|
|
sb.AppendLine(" public float CellHeight;");
|
|
sb.AppendLine(" public float AgentHeight;");
|
|
sb.AppendLine(" public float AgentRadius;");
|
|
sb.AppendLine(" public float AgentMaxClimb;");
|
|
sb.AppendLine(" public float AgentMaxSlopeDeg;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineNavStats: bake poly counts, 8 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 8)]");
|
|
sb.AppendLine(" public struct NavStats");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public uint Polys;");
|
|
sb.AppendLine(" public uint DetailTris;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineNavAgent: goal + speed + arrival flag, 64 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 64)]");
|
|
sb.AppendLine(" public struct NavAgentData");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Gx;");
|
|
sb.AppendLine(" public float Gy;");
|
|
sb.AppendLine(" public float Gz;");
|
|
sb.AppendLine(" public float MaxSpeed;");
|
|
sb.AppendLine(" public float StopRadius;");
|
|
sb.AppendLine(" public int Arrived;");
|
|
sb.AppendLine(" public int Idle;");
|
|
sb.AppendLine(" private fixed float _pad[9];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitCharacterTypes(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// P6-04 character-controller mirrors (ABI 4.12):
|
|
// EngineCharacterConfig (24B) + EngineCharacterState (24B). Field
|
|
// order must match engine_abi.h exactly; the loader pins sizes at
|
|
// startup.
|
|
var cfg = structs.Single(s => s.Name == "EngineCharacterConfig");
|
|
string[] wantCfgNames = ["half_height", "radius", "max_slope_deg", "max_step_height", "skin_width", "stick_to_floor", "reserved"];
|
|
string[] wantCfgTypes = ["float", "float", "float", "float", "float", "uint8_t", "uint8_t"];
|
|
int[] wantCfgArr = [0, 0, 0, 0, 0, 0, 3];
|
|
if (cfg.Fields.Count != wantCfgNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineCharacterConfig must have {wantCfgNames.Length} fields (got {cfg.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantCfgNames.Length; i++)
|
|
{
|
|
if (cfg.Fields[i].Name != wantCfgNames[i] || cfg.Fields[i].CType != wantCfgTypes[i] || cfg.Fields[i].ArrayLen != wantCfgArr[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineCharacterConfig field {i} must be '{wantCfgTypes[i]} {wantCfgNames[i]}', got '{cfg.Fields[i].CType} {cfg.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
|
|
var st = structs.Single(s => s.Name == "EngineCharacterState");
|
|
string[] wantStNames = ["px", "py", "pz", "grounded", "ground_y", "vy"];
|
|
string[] wantStTypes = ["float", "float", "float", "uint32_t", "float", "float"];
|
|
if (st.Fields.Count != wantStNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineCharacterState must have {wantStNames.Length} fields (got {st.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantStNames.Length; i++)
|
|
{
|
|
if (st.Fields[i].Name != wantStNames[i] || st.Fields[i].CType != wantStTypes[i] || st.Fields[i].ArrayLen != 0)
|
|
{
|
|
throw new InvalidOperationException($"EngineCharacterState field {i} must be '{wantStTypes[i]} {wantStNames[i]}', got '{st.Fields[i].CType} {st.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>Mirror of EngineCharacterConfig: capsule + limits, 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct CharacterConfig");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float HalfHeight;");
|
|
sb.AppendLine(" public float Radius;");
|
|
sb.AppendLine(" public float MaxSlopeDeg;");
|
|
sb.AppendLine(" public float MaxStepHeight;");
|
|
sb.AppendLine(" public float SkinWidth;");
|
|
sb.AppendLine(" public byte StickToFloor;");
|
|
sb.AppendLine(" private byte _r0;");
|
|
sb.AppendLine(" private byte _r1;");
|
|
sb.AppendLine(" private byte _r2;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineCharacterState: feet position + grounded flag + support height + vertical velocity, 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct CharacterState");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Px;");
|
|
sb.AppendLine(" public float Py;");
|
|
sb.AppendLine(" public float Pz;");
|
|
sb.AppendLine(" public uint Grounded;");
|
|
sb.AppendLine(" public float GroundY;");
|
|
sb.AppendLine(" public float Vy;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitCollisionEvent(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// P2-05 3D collision-event mirror (ABI 4.16): validated field-by-field
|
|
// so a header drift fails closed here, not as a silent C# layout skew.
|
|
// Same 32-byte record as the 2D path; the managed Collision.cs binds
|
|
// to this struct (CollisionEvent) while CollisionEvent2D stays for 2D.
|
|
var ev = structs.Single(s => s.Name == "EngineCollisionEvent");
|
|
string[] evNames = ["entity_a", "entity_b", "point_x", "point_y", "point_z", "kind", "sensor", "reserved"];
|
|
string[] evTypes = ["uint64_t", "uint64_t", "float", "float", "float", "uint8_t", "uint8_t", "uint8_t"];
|
|
int[] evArr = [0, 0, 0, 0, 0, 0, 0, 2];
|
|
if (ev.Fields.Count != evNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineCollisionEvent must have {evNames.Length} fields (got {ev.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < evNames.Length; i++)
|
|
{
|
|
if (ev.Fields[i].Name != evNames[i] || ev.Fields[i].CType != evTypes[i] || ev.Fields[i].ArrayLen != evArr[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineCollisionEvent field {i} must be '{evTypes[i]} {evNames[i]}', got '{ev.Fields[i].CType} {ev.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>Mirror of EngineCollisionEvent: 3D contact record, 32 bytes, sensor + kind packed as bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 32)]");
|
|
sb.AppendLine(" public struct CollisionEvent");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public ulong EntityA;");
|
|
sb.AppendLine(" public ulong EntityB;");
|
|
sb.AppendLine(" public float PointX;");
|
|
sb.AppendLine(" public float PointY;");
|
|
sb.AppendLine(" public float PointZ;");
|
|
sb.AppendLine(" public byte Kind;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" private fixed byte _reserved[2];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitPhysics2DTypes(StringBuilder sb, List<StructDef> structs)
|
|
{
|
|
// P6-07 2D physics mirrors (ABI 4.9): validated field-by-field so a
|
|
// header drift fails closed here, not as a silent C# layout skew.
|
|
var ev = structs.Single(s => s.Name == "EngineCollisionEvent2D");
|
|
string[] evNames = ["entity_a", "entity_b", "point_x", "point_y", "point_z", "kind", "sensor", "reserved"];
|
|
string[] evTypes = ["uint64_t", "uint64_t", "float", "float", "float", "uint8_t", "uint8_t", "uint8_t"];
|
|
int[] evArr = [0, 0, 0, 0, 0, 0, 0, 2];
|
|
if (ev.Fields.Count != evNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"EngineCollisionEvent2D must have {evNames.Length} fields (got {ev.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < evNames.Length; i++)
|
|
{
|
|
if (ev.Fields[i].Name != evNames[i] || ev.Fields[i].CType != evTypes[i] || ev.Fields[i].ArrayLen != evArr[i])
|
|
{
|
|
throw new InvalidOperationException($"EngineCollisionEvent2D field {i} must be '{evTypes[i]} {evNames[i]}', got '{ev.Fields[i].CType} {ev.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
|
|
sb.AppendLine(" /// <summary>Opaque native 2D physics world handle (caller-owned engine::physics::World2D).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EnginePhysics2DWorld");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Mirror of EngineCollisionEvent2D: same 32-byte record as the 3D path, sensor + kind packed as bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8, Size = 32)]");
|
|
sb.AppendLine(" public struct CollisionEvent2D");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public ulong EntityA;");
|
|
sb.AppendLine(" public ulong EntityB;");
|
|
sb.AppendLine(" public float PointX;");
|
|
sb.AppendLine(" public float PointY;");
|
|
sb.AppendLine(" public float PointZ;");
|
|
sb.AppendLine(" public byte Kind;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" private fixed byte _reserved[2];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
EmitRigidbody2D(sb, structs.Single(s => s.Name == "EngineRigidbody2D"));
|
|
EmitBox2D(sb, structs.Single(s => s.Name == "EngineBoxCollider2D"));
|
|
EmitCircle2D(sb, structs.Single(s => s.Name == "EngineCircleCollider2D"));
|
|
EmitPolygon2D(sb, structs.Single(s => s.Name == "EnginePolygonCollider2D"));
|
|
}
|
|
|
|
private static void EmitRigidbody2D(StringBuilder sb, StructDef def)
|
|
{
|
|
// 24 bytes: 3 floats + 6 bytes flags + type + 2 reserved.
|
|
string[] wantNames = ["gravity_scale", "linear_damping", "angular_damping", "fixed_rotation", "bullet", "allow_sleeping", "sensor", "active", "type", "reserved"];
|
|
string[] wantTypes = ["float", "float", "float", "uint8_t", "uint8_t", "uint8_t", "uint8_t", "uint8_t", "uint8_t", "uint8_t"];
|
|
int[] wantArr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 4];
|
|
AssertStructFields(def, "EngineRigidbody2D", wantNames, wantTypes, wantArr);
|
|
sb.AppendLine(" /// <summary>Mirror of EngineRigidbody2D: 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct Rigidbody2D");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float GravityScale;");
|
|
sb.AppendLine(" public float LinearDamping;");
|
|
sb.AppendLine(" public float AngularDamping;");
|
|
sb.AppendLine(" public byte FixedRotation;");
|
|
sb.AppendLine(" public byte Bullet;");
|
|
sb.AppendLine(" public byte AllowSleeping;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" public byte Active;");
|
|
sb.AppendLine(" public byte Type;");
|
|
sb.AppendLine(" private fixed byte _reserved[4];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitBox2D(StringBuilder sb, StructDef def)
|
|
{
|
|
// 24 bytes: 5 floats + sensor + 3 reserved.
|
|
string[] wantNames = ["hx", "hy", "density", "friction", "restitution", "sensor", "reserved"];
|
|
string[] wantTypes = ["float", "float", "float", "float", "float", "uint8_t", "uint8_t"];
|
|
int[] wantArr = [0, 0, 0, 0, 0, 0, 3];
|
|
AssertStructFields(def, "EngineBoxCollider2D", wantNames, wantTypes, wantArr);
|
|
sb.AppendLine(" /// <summary>Mirror of EngineBoxCollider2D: 24 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 24)]");
|
|
sb.AppendLine(" public struct BoxCollider2D");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Hx;");
|
|
sb.AppendLine(" public float Hy;");
|
|
sb.AppendLine(" public float Density;");
|
|
sb.AppendLine(" public float Friction;");
|
|
sb.AppendLine(" public float Restitution;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" private fixed byte _reserved[3];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitCircle2D(StringBuilder sb, StructDef def)
|
|
{
|
|
// 28 bytes: 6 floats + sensor + 3 reserved.
|
|
string[] wantNames = ["cx", "cy", "radius", "density", "friction", "restitution", "sensor", "reserved"];
|
|
string[] wantTypes = ["float", "float", "float", "float", "float", "float", "uint8_t", "uint8_t"];
|
|
int[] wantArr = [0, 0, 0, 0, 0, 0, 0, 3];
|
|
AssertStructFields(def, "EngineCircleCollider2D", wantNames, wantTypes, wantArr);
|
|
sb.AppendLine(" /// <summary>Mirror of EngineCircleCollider2D: 28 bytes.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 28)]");
|
|
sb.AppendLine(" public struct CircleCollider2D");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public float Cx;");
|
|
sb.AppendLine(" public float Cy;");
|
|
sb.AppendLine(" public float Radius;");
|
|
sb.AppendLine(" public float Density;");
|
|
sb.AppendLine(" public float Friction;");
|
|
sb.AppendLine(" public float Restitution;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" private fixed byte _reserved[3];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitPolygon2D(StringBuilder sb, StructDef def)
|
|
{
|
|
// 64 bytes: vx[6] + vy[6] + density + friction + count + sensor + 2 reserved.
|
|
string[] wantNames = ["vx", "vy", "density", "friction", "count", "sensor", "reserved"];
|
|
string[] wantTypes = ["float", "float", "float", "float", "uint8_t", "uint8_t", "uint8_t"];
|
|
int[] wantArr = [6, 6, 0, 0, 0, 0, 6];
|
|
AssertStructFields(def, "EnginePolygonCollider2D", wantNames, wantTypes, wantArr);
|
|
sb.AppendLine(" /// <summary>Mirror of EnginePolygonCollider2D: 64 bytes, up to 6 vertices.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 64)]");
|
|
sb.AppendLine(" public unsafe struct PolygonCollider2D");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public fixed float Vx[6];");
|
|
sb.AppendLine(" public fixed float Vy[6];");
|
|
sb.AppendLine(" public float Density;");
|
|
sb.AppendLine(" public float Friction;");
|
|
sb.AppendLine(" public byte Count;");
|
|
sb.AppendLine(" public byte Sensor;");
|
|
sb.AppendLine(" private fixed byte _reserved[6];");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void AssertStructFields(StructDef def, string name, string[] wantNames, string[] wantTypes, int[] wantArr)
|
|
{
|
|
if (def.Fields.Count != wantNames.Length)
|
|
{
|
|
throw new InvalidOperationException($"{name} must have {wantNames.Length} fields (got {def.Fields.Count}).");
|
|
}
|
|
|
|
for (var i = 0; i < wantNames.Length; i++)
|
|
{
|
|
if (def.Fields[i].Name != wantNames[i] || def.Fields[i].CType != wantTypes[i] || def.Fields[i].ArrayLen != wantArr[i])
|
|
{
|
|
throw new InvalidOperationException($"{name} field {i} must be '{wantTypes[i]} {wantNames[i]}', got '{def.Fields[i].CType} {def.Fields[i].Name}'.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void EmitOpaqueHandles(StringBuilder sb)
|
|
{
|
|
sb.AppendLine(" /// <summary>Opaque native world handle (owns the Flecs world).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineWorld");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque game UI world (owns one RmlUi context + CPU frame, P5-04).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineUi");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque audio mixer world (owns one miniaudio engine + buses + effects, P5-06).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineAudioWorld");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque editor session (owns queries bound to one world).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineEditorSession");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque PIE snapshot (owns one scene checkpoint).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineEditorSnapshot");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque native renderer handle (P1-09 owns lifecycle; P2-02 only reads its input member).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineRenderer");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque navmesh world (owns one engine::navigation::NavWorld).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineNavWorld");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque sequencer timeline (owns tracks + clips, P5-07).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineTimeline");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque sequencer player (binds one timeline, owns the transport cursor, P5-07).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineTimelinePlayer");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque character handle (owns one engine::physics::CharacterBody, borrows the caller's PhysicsWorld).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineCharacter");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque render-pass registry (owns the native Compositor).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineRenderPassRegistry");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque animator handle (owns the native per-entity animator state, P5-03).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineAnimator");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>Opaque network session (owns one ENet host + baselines + queues, P7-04).</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct EngineNetSession");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" public void* Handle;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static void EmitApiTable(StringBuilder sb, StructDef table, List<FnDef> fns)
|
|
{
|
|
sb.AppendLine(" /// <summary>Mirror of EngineApiTable. One raw call per entry.</summary>");
|
|
sb.AppendLine(" [StructLayout(LayoutKind.Sequential, Pack = 8)]");
|
|
sb.AppendLine(" public unsafe struct ApiTable");
|
|
sb.AppendLine(" {");
|
|
var used = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var f in table.Fields)
|
|
{
|
|
// Header (non-function) fields: explicit C-type map, fail closed.
|
|
if (f.Name is "size" or "abi_version_major" or "abi_version_minor" or "abi_version_patch" or "column_stride")
|
|
{
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" {ApiScalarDecl(f)}");
|
|
continue;
|
|
}
|
|
|
|
// Function fields: resolve the field's C typedef to its FnDef.
|
|
var def = fns.FirstOrDefault(d => d.Name == f.CType);
|
|
if (def == null)
|
|
{
|
|
throw new InvalidOperationException($"EngineApiTable.{f.Name}: unknown field type '{f.CType}' (no such Fn typedef — extend the header and generator together).");
|
|
}
|
|
|
|
used.Add(def.Name);
|
|
var ps = new List<string>();
|
|
foreach (var p in def.Params)
|
|
{
|
|
ps.Add(CsType(p.CType));
|
|
}
|
|
|
|
ps.Add(CsType(def.Return));
|
|
// Selective suppression: only registration is a guaranteed
|
|
// non-blocking store. Run/propagate/lifecycle calls may block on
|
|
// worker completion — suppressing the GC transition across a
|
|
// blocking native call can stall the runtime.
|
|
var conv = def.Name == "EngineRegisterSystemFn" ? "Cdecl, SuppressGCTransition" : "Cdecl";
|
|
sb.AppendLine(System.Globalization.CultureInfo.InvariantCulture, $" public delegate* unmanaged[{conv}]<{string.Join(", ", ps)}> {TrimFn(def.Name)};");
|
|
}
|
|
|
|
var unused = fns.Where(d => !used.Contains(d.Name)).Select(d => d.Name).ToList();
|
|
// Callback typedefs ride inside other entries' params (ManagedSystemCallback,
|
|
// EngineRenderPassCallback, EngineLogSinkCallback) — used, just not as
|
|
// table fields.
|
|
unused.Remove("ManagedSystemCallback");
|
|
unused.Remove("EngineRenderPassCallback");
|
|
unused.Remove("EngineSystemCallback");
|
|
unused.Remove("EngineLogSinkCallback");
|
|
if (unused.Count != 0)
|
|
{
|
|
throw new InvalidOperationException($"Fn typedef(s) not used by EngineApiTable: {string.Join(", ", unused)} (remove the typedef or wire it into the table).");
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
private static string ApiScalarDecl(StructField f) => (f.Name, f.CType, f.ArrayLen) switch
|
|
{
|
|
("size", "uint64_t", 0) => "public ulong Size;",
|
|
("abi_version_major", "int32_t", 0) => "public int AbiVersionMajor;",
|
|
("abi_version_minor", "int32_t", 0) => "public int AbiVersionMinor;",
|
|
("abi_version_patch", "int32_t", 0) => "public int AbiVersionPatch;",
|
|
("column_stride", "int32_t", 0) => "public int ColumnStride;",
|
|
_ => throw new InvalidOperationException($"EngineApiTable.{f.Name}: unknown header field type '{f.CType}' (extend ApiScalarDecl deliberately)."),
|
|
};
|
|
|
|
private static string TrimFn(string name) => name.EndsWith("Fn", StringComparison.Ordinal) ? name[..^2] : name;
|
|
|
|
private static void EmitLoader(StringBuilder sb)
|
|
{
|
|
sb.AppendLine(" /// <summary>Resolves engine_get_api_table and validates the ABI.</summary>");
|
|
sb.AppendLine(" public static unsafe class ApiLoader");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" private static ApiTable* _table;");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" public static ApiTable* Table => _table != null ? _table : throw new InvalidOperationException(\"NativeApi not loaded. Call ApiLoader.Load() first.\");");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" public static ApiTable* Load()");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" if (_table != null)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" return _table;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (!NativeLibrary.TryLoad(NativeLibraryName, out var handle))");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new DllNotFoundException($\"Unable to load {NativeLibraryName}.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (!NativeLibrary.TryGetExport(handle, \"engine_get_api_table\", out var addr))");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new EntryPointNotFoundException(\"engine_get_api_table not exported.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" var getTable = (delegate* unmanaged[Cdecl, SuppressGCTransition]<ApiTable*>)addr;");
|
|
sb.AppendLine(" var table = getTable();");
|
|
sb.AppendLine(" if (table == null)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new InvalidOperationException(\"engine_get_api_table returned null.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (table->Size < (ulong)sizeof(ApiTable))");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new InvalidOperationException($\"Native ABI table too small: {table->Size} < {(ulong)sizeof(ApiTable)}.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (table->AbiVersionMajor != AbiVersionMajor || table->AbiVersionMinor != AbiVersionMinor || table->AbiVersionPatch != AbiVersionPatch)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new InvalidOperationException($\"ABI version mismatch: native {table->AbiVersionMajor}.{table->AbiVersionMinor}.{table->AbiVersionPatch} != managed {AbiVersionMajor}.{AbiVersionMinor}.{AbiVersionPatch}.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (table->ColumnStride != ColumnStride)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new InvalidOperationException($\"ABI column stride mismatch: native {table->ColumnStride} != managed {ColumnStride}.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" if (sizeof(LocalTransform) != 64 || sizeof(WorldTransform) != 64 || sizeof(Ray) != 24 || sizeof(RaycastHit) != 48 || sizeof(EngineChunkColumns) != 120 || sizeof(InputState) != 1744 || sizeof(NavBox) != 24 || sizeof(NavBakeConfig) != 48 || sizeof(NavStats) != 8 || sizeof(NavAgentData) != 64 || sizeof(FieldDesc) != 16 || sizeof(NetTransformRow) != 48)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" throw new InvalidOperationException($\"Component layout drift: LocalTransform={sizeof(LocalTransform)} WorldTransform={sizeof(WorldTransform)} Ray={sizeof(Ray)} Hit={sizeof(RaycastHit)} ChunkColumns={sizeof(EngineChunkColumns)} InputState={sizeof(InputState)} NavBox={sizeof(NavBox)} NavConfig={sizeof(NavBakeConfig)} NavStats={sizeof(NavStats)} NavAgent={sizeof(NavAgentData)} FieldDesc={sizeof(FieldDesc)} NetRow={sizeof(NetTransformRow)}.\");");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" _table = table;");
|
|
sb.AppendLine(" return table;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine(" }");
|
|
}
|
|
}
|