mirror of
https://github.com/Heretek-AI/game-engine.git
synced 2026-11-07 10:55:58 -10:00
P2-03: run FixedUpdate systems per fixed step from one native clock
The generator mapped Phase.FixedUpdate to Update and nothing dispatched by phase, so FixedUpdate systems never ran in the fixed step. There were also two clocks: the renderer advanced a native Timestep nobody read, while Time.cs kept its own copy of the math. - ABI 4.12 frame-clock tail: system_set_phase, world_frame, time_set_scale, time_get_f64, time_get_u64. - The world owns the only clock; world_frame advances it and runs generic systems by phase (FixedUpdate once per fixed step, dt 1/50 s). - Time.cs reads the native clock; the renderer no longer advances one. - Generator emits FixedUpdate and registers each phase natively. - Tests: native engine_world_frame; IllusionSample runs a generated FixedUpdate system through Time.Frame. HelloCube uses Time.Frame. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VjcGdfRk1pMqciEq3aZUEZ
This commit is contained in:
27 files changed
+779
-185
No files matched your search
Whitespace-only changes.
@@ -20,3 +20,38 @@
|
||||
clamp_count == 1, frame_count == 4, exit 0.
|
||||
- 2026-11-01: `SDL_VIDEO_DRIVER=offscreen ENGINE_REQUIRE_GPU=1 ctest
|
||||
--preset ci-linux -R engine_timestep --output-on-failure` 1/1 PASS.
|
||||
|
||||
## 2026-11-02 follow-up: FixedUpdate dispatch and a single clock
|
||||
|
||||
Review of #187 found two gaps: the generator mapped `Phase.FixedUpdate`
|
||||
(5) to `"Update"`, and nothing dispatched by phase at all (phases were
|
||||
only a doc comment on the runner); and there were two clocks (the
|
||||
renderer advanced a native `Timestep` nobody read, while `Time.cs`
|
||||
re-implemented the math with its own state). Fixed as follows:
|
||||
|
||||
- ABI 4.12 frame-clock tail (5 entries after the 4.11 audio tail):
|
||||
`system_set_phase`, `world_frame`, `time_set_scale`, `time_get_f64`,
|
||||
`time_get_u64`. `ENGINE_PHASE_*` values equal the C# `Phase` enum.
|
||||
If another tail lands on main first, renumber this one at merge time.
|
||||
- The `EngineWorld` owns the only clock. `world_frame(world, wall_dt)`
|
||||
advances it and runs generic systems by phase: Startup (first frame
|
||||
only, dt 0), PreUpdate, Update, FixedUpdate once per fixed step with
|
||||
dt = 1/50 s, PostUpdate, Render. `systems_run` (4.2) is unchanged and
|
||||
still runs every system once.
|
||||
- The renderer no longer advances its own timestep (`renderer_bridge.cpp`
|
||||
is back to main).
|
||||
- `Time.cs` is now a view over the native clock: `Time.Frame(table,
|
||||
world, wallDt)` calls `world_frame`; every property reads native state.
|
||||
- Generator: `PhaseName` maps 5 to `FixedUpdate`; `Register` emits one
|
||||
native system per phase plus `system_set_phase` (a plain Update system
|
||||
still emits the single `system_register` call).
|
||||
- Tests: `engine_world_frame` (native, fake clock `[0.016, 0.016, 0.2,
|
||||
0.001]` gives FixedUpdate calls `[0, 1, 5, 0]` with dt 1/50 s, Update
|
||||
once per frame with the clamped dt, pipeline order, time_scale 0/2);
|
||||
`engine_illusion_sample` gains a generated `[UpdateInPhase(Phase.
|
||||
FixedUpdate)]` system driven through `Time.Frame`. HelloCube now runs
|
||||
its frame loop through `Time.Frame` with a fixed 1/60 s wall delta, so
|
||||
its golden is unchanged.
|
||||
- No .NET SDK in the authoring sandbox: `NativeApi.g.cs` was edited to
|
||||
match the generator output by hand; the native files were syntax-checked
|
||||
with clang 18. CI is the full build and test run.
|
||||
@@ -428,6 +428,7 @@ public sealed class ComponentSystemGenerator : IIncrementalGenerator
|
||||
1 => "PreUpdate",
|
||||
3 => "PostUpdate",
|
||||
4 => "Render",
|
||||
5 => "FixedUpdate",
|
||||
_ => "Update",
|
||||
};
|
||||
}
|
||||
@@ -435,6 +436,40 @@ public sealed class ComponentSystemGenerator : IIncrementalGenerator
|
||||
return v.Value?.ToString() ?? "Update";
|
||||
}
|
||||
|
||||
// Native phase ids (ENGINE_PHASE_*, same values as Engine.Runtime.Phase).
|
||||
private const int PhaseUpdate = 2;
|
||||
|
||||
// Distinct native phase ids for the Tick (method attributes win over
|
||||
// struct attributes; none means Update), in first-seen order.
|
||||
private static List<int> PhaseIds(TickSignatureSnapshot tick, SystemSpec spec)
|
||||
{
|
||||
var names = tick.PhaseNames.Length != 0 ? tick.PhaseNames : spec.StructPhaseNames;
|
||||
var ids = new List<int>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
var id = name switch
|
||||
{
|
||||
"Startup" => 0,
|
||||
"PreUpdate" => 1,
|
||||
"PostUpdate" => 3,
|
||||
"Render" => 4,
|
||||
"FixedUpdate" => 5,
|
||||
_ => PhaseUpdate,
|
||||
};
|
||||
if (!ids.Contains(id))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
ids.Add(PhaseUpdate);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
// --- Stage 3: emit -----------------------------------------------------
|
||||
|
||||
private static void EmitOne(SourceProductionContext spc, SystemSpec spec)
|
||||
@@ -592,7 +627,44 @@ public sealed class ComponentSystemGenerator : IIncrementalGenerator
|
||||
sb.AppendLine(" delegate* unmanaged[Cdecl]<NativeApi.EngineChunkColumns*, float, void> fn = &RunChunk;");
|
||||
sb.AppendLine($" fixed (byte* name = \"{systemName}\\0\"u8)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" return table->EngineSystemRegister(world, name, ids, access, {components.Count}u, fn, userData);");
|
||||
var phaseIds = PhaseIds(tick, spec);
|
||||
if (phaseIds.Count == 1 && phaseIds[0] == PhaseUpdate)
|
||||
{
|
||||
// Native systems start in Update: no phase call needed.
|
||||
sb.AppendLine($" return table->EngineSystemRegister(world, name, ids, access, {components.Count}u, fn, userData);");
|
||||
}
|
||||
else
|
||||
{
|
||||
// P2-03: one native system per phase ([UpdateInPhase] is
|
||||
// AllowMultiple); world_frame runs each phase in pipeline order
|
||||
// (FixedUpdate once per fixed step). Returns the first index.
|
||||
sb.AppendLine(" int first = -1;");
|
||||
foreach (var phaseId in phaseIds)
|
||||
{
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" int index = table->EngineSystemRegister(world, name, ids, access, {components.Count}u, fn, userData);");
|
||||
sb.AppendLine(" if (index < 0)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return index;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($" int phaseRc = table->EngineSystemSetPhase(world, index, {phaseId}u);");
|
||||
sb.AppendLine(" if (phaseRc != 0)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" return phaseRc;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" if (first < 0)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" first = index;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" return first;");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// <auto-generated>
|
||||
// NativeApi.g.cs — GENERATED by tools/abi_codegen from
|
||||
// native/bridge/include/engine/bridge/engine_abi.h. DO NOT EDIT.
|
||||
// ABI 4.11.0, column stride 64.
|
||||
// ABI 4.12.0, column stride 64.
|
||||
// Regenerate: dotnet run --project tools/abi_codegen -- <repo-root>
|
||||
// </auto-generated>
|
||||
#nullable enable
|
||||
@@ -25,7 +25,7 @@ namespace Engine.Bindings;
|
||||
public static unsafe class NativeApi
|
||||
{
|
||||
public const int AbiVersionMajor = 4;
|
||||
public const int AbiVersionMinor = 11;
|
||||
public const int AbiVersionMinor = 12;
|
||||
public const int AbiVersionPatch = 0;
|
||||
public const int ColumnStride = 64;
|
||||
public const string NativeLibraryName = "engine_core";
|
||||
@@ -452,6 +452,11 @@ public static unsafe class NativeApi
|
||||
public delegate* unmanaged[Cdecl]<EngineAudioWorld*, float, float, float, float, int> EngineAudioSetDelay;
|
||||
public delegate* unmanaged[Cdecl]<EngineAudioWorld*, int, int> EngineAudioSetDelayEnabled;
|
||||
public delegate* unmanaged[Cdecl]<EngineAudioWorld*, uint, float, float, float*, uint, uint> EngineAudioRenderSineDb;
|
||||
public delegate* unmanaged[Cdecl]<EngineWorld*, int, uint, int> EngineSystemSetPhase;
|
||||
public delegate* unmanaged[Cdecl]<EngineWorld*, double, int> EngineWorldFrame;
|
||||
public delegate* unmanaged[Cdecl]<EngineWorld*, double, int> EngineTimeSetScale;
|
||||
public delegate* unmanaged[Cdecl]<EngineWorld*, uint, double*, int> EngineTimeGetF64;
|
||||
public delegate* unmanaged[Cdecl]<EngineWorld*, uint, ulong*, int> EngineTimeGetU64;
|
||||
}
|
||||
|
||||
/// <summary>Resolves engine_get_api_table and validates the ABI.</summary>
|
||||
|
||||
@@ -32,7 +32,7 @@ public enum Phase
|
||||
/// <summary>
|
||||
/// Fixed-step simulation (exactly 1/50 s per step, up to 5 steps per
|
||||
/// frame). Systems in this phase tick once per fixed step with
|
||||
/// <see cref="Time.FixedDeltaTime"/> via <see cref="Time.RunFixedSteps"/>.
|
||||
/// <see cref="Time.FixedDeltaTime"/>; <see cref="Time.Frame"/> runs them.
|
||||
/// </summary>
|
||||
FixedUpdate = 5,
|
||||
}
|
||||
|
||||
+121
-138
@@ -1,169 +1,152 @@
|
||||
// Time.cs — P2-03: C# view of the native frame-loop timestep.
|
||||
// Time.cs — P2-03: C# view of the native world clock.
|
||||
//
|
||||
// Static frame clock over the native engine::renderer::Timestep advanced
|
||||
// once per renderer_frame: variable dt clamped to 0.1 s, fixed step
|
||||
// exactly 1/50 s with an accumulator, at most 5 fixed steps per frame.
|
||||
// Systems marked [UpdateInPhase(Phase.FixedUpdate)] run in the fixed
|
||||
// step (fixed-dt ticks); all other phases run once per frame with the
|
||||
// variable dt. Frame state only — never config; the constants live in
|
||||
// native (kTimestepFixedDt / kTimestepMaxDt / kTimestepMaxFixedSteps).
|
||||
//
|
||||
// Test seam: Advance feeds a fake wall-clock delta (same path as the
|
||||
// native advance: clamp, scale, accumulate, count steps). Production
|
||||
// callers advance once per frame with the wall delta; readers then see
|
||||
// DeltaTime/FixedStepsThisFrame.
|
||||
// The native EngineWorld owns the only frame clock (ABI 4.12 frame-clock
|
||||
// tail): variable dt clamped to 0.1 s and scaled by time_scale, a fixed
|
||||
// step of exactly 1/50 s drained from an accumulator, at most 5 fixed
|
||||
// steps per frame. Time.Frame calls world_frame, which advances that
|
||||
// clock and runs the generic systems phase by phase; systems marked
|
||||
// [UpdateInPhase(Phase.FixedUpdate)] tick once per fixed step with the
|
||||
// fixed dt. Every property reads native state, so there is no second
|
||||
// clock to drift.
|
||||
using Engine.Bindings;
|
||||
|
||||
namespace Engine.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Frame clock for gameplay code. Advance once per frame with the
|
||||
/// wall-clock delta (seconds); readers then see the variable
|
||||
/// <see cref="DeltaTime"/>, the fixed-step count, and the fixed dt
|
||||
/// FixedUpdate systems tick with. Backed by the same math as the
|
||||
/// native timestep (clamp 0.1 s, fixed step 1/50 s, max 5 steps/frame,
|
||||
/// time_scale): no allocation, no throw on the frame path.
|
||||
/// Frame clock for gameplay code, backed by the native world clock.
|
||||
/// Call <see cref="Frame"/> once per frame with the wall-clock delta;
|
||||
/// the properties then read the bound world's clock.
|
||||
/// </summary>
|
||||
public static class Time
|
||||
public static unsafe class Time
|
||||
{
|
||||
/// <summary>Fixed step size: exactly 1/50 s (matches native kTimestepFixedDt).</summary>
|
||||
/// <summary>Fixed step size: exactly 1/50 s (native kTimestepFixedDt).</summary>
|
||||
public const double FixedStepSeconds = 1.0 / 50.0;
|
||||
|
||||
/// <summary>Variable dt clamp: 0.1 s (matches native kTimestepMaxDt).</summary>
|
||||
/// <summary>Variable dt clamp: 0.1 s (native kTimestepMaxDt).</summary>
|
||||
public const double MaxDeltaSeconds = 0.1;
|
||||
|
||||
/// <summary>Cap on fixed steps drained per frame (matches native kTimestepMaxFixedSteps).</summary>
|
||||
/// <summary>Cap on fixed steps per frame (native kTimestepMaxFixedSteps).</summary>
|
||||
public const int MaxFixedStepsPerFrame = 5;
|
||||
|
||||
private static double _timeScale = 1.0;
|
||||
private static double _deltaTime;
|
||||
private static double _realtime;
|
||||
private static double _accumulator;
|
||||
private static ulong _frameCount;
|
||||
private static ulong _clampCount;
|
||||
private static int _fixedStepsThisFrame;
|
||||
// Native selectors (ENGINE_TIME_* in engine_abi.h).
|
||||
private const uint SelDelta = 0;
|
||||
private const uint SelFixedDelta = 1;
|
||||
private const uint SelScale = 2;
|
||||
private const uint SelRealtime = 3;
|
||||
private const uint SelFrameCount = 0;
|
||||
private const uint SelFixedSteps = 1;
|
||||
private const uint SelClampCount = 2;
|
||||
|
||||
private static NativeApi.ApiTable* s_table;
|
||||
private static NativeApi.EngineWorld* s_world;
|
||||
|
||||
/// <summary>
|
||||
/// Variable dt for this frame: clamped wall delta times
|
||||
/// <see cref="TimeScale"/> (0 when the scale is not positive finite).
|
||||
/// Binds the clock to a world. <see cref="Frame"/> binds automatically;
|
||||
/// call this to read a world's clock before its first frame.
|
||||
/// </summary>
|
||||
public static double DeltaTime => _deltaTime;
|
||||
public static void Bind(NativeApi.ApiTable* table, NativeApi.EngineWorld* world)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(table);
|
||||
ArgumentNullException.ThrowIfNull(world);
|
||||
s_table = table;
|
||||
s_world = world;
|
||||
}
|
||||
|
||||
/// <summary>Drops the binding (call before destroying the bound world).</summary>
|
||||
public static void Unbind()
|
||||
{
|
||||
s_table = null;
|
||||
s_world = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs one frame: advances the world clock by
|
||||
/// <paramref name="wallDeltaSeconds"/> and runs the world's systems in
|
||||
/// phase order (Startup on the first frame only, then PreUpdate,
|
||||
/// Update, FixedUpdate once per fixed step, PostUpdate, Render).
|
||||
/// Returns the fixed steps taken. Throws on a native error.
|
||||
/// </summary>
|
||||
public static int Frame(NativeApi.ApiTable* table, NativeApi.EngineWorld* world, double wallDeltaSeconds)
|
||||
{
|
||||
Bind(table, world);
|
||||
var rc = table->EngineWorldFrame(world, wallDeltaSeconds);
|
||||
if (rc < 0)
|
||||
{
|
||||
throw new InvalidOperationException($"world_frame failed with code {rc}.");
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/// <summary>Variable dt for this frame: clamped wall delta times <see cref="TimeScale"/>.</summary>
|
||||
public static double DeltaTime => ReadF64(SelDelta);
|
||||
|
||||
/// <summary>Fixed step size (always exactly 1/50 s).</summary>
|
||||
public static double FixedDeltaTime => FixedStepSeconds;
|
||||
public static double FixedDeltaTime => ReadF64(SelFixedDelta);
|
||||
|
||||
/// <summary>
|
||||
/// Time multiplier applied to the clamped dt before accumulation.
|
||||
/// Values <= 0 or non-finite freeze simulation (dt becomes 0);
|
||||
/// persisted verbatim so round-trips hold.
|
||||
/// Multiplier applied to the clamped dt. Values <= 0 or non-finite
|
||||
/// freeze simulation (dt becomes 0); the value is stored as given.
|
||||
/// </summary>
|
||||
public static double TimeScale
|
||||
{
|
||||
get => _timeScale;
|
||||
set => _timeScale = value;
|
||||
}
|
||||
|
||||
/// <summary>Total frames advanced (first Advance makes it 1).</summary>
|
||||
public static ulong FrameCount => _frameCount;
|
||||
|
||||
/// <summary>Accumulated scaled time (sum of <see cref="DeltaTime"/>).</summary>
|
||||
public static double RealtimeSinceStartup => _realtime;
|
||||
|
||||
/// <summary>Fixed steps taken by the last <see cref="Advance"/>.</summary>
|
||||
public static int FixedStepsThisFrame => _fixedStepsThisFrame;
|
||||
|
||||
/// <summary>Times the wall delta exceeded the 0.1 s clamp.</summary>
|
||||
public static ulong ClampCount => _clampCount;
|
||||
|
||||
/// <summary>
|
||||
/// Advance one frame with a wall-clock delta in seconds. Negative
|
||||
/// deltas (clock jumps) clamp to 0; spikes above 0.1 s clamp and
|
||||
/// count once. Surplus beyond the 5-step cap is dropped (no spiral
|
||||
/// of death); the accumulator keeps only the unspent remainder.
|
||||
/// Returns the fixed steps to run this frame.
|
||||
/// </summary>
|
||||
public static int Advance(double wallDeltaSeconds)
|
||||
{
|
||||
var raw = wallDeltaSeconds;
|
||||
if (!(raw >= 0.0))
|
||||
get => ReadF64(SelScale);
|
||||
set
|
||||
{
|
||||
raw = 0.0;
|
||||
}
|
||||
|
||||
if (raw > MaxDeltaSeconds)
|
||||
{
|
||||
raw = MaxDeltaSeconds;
|
||||
_clampCount++;
|
||||
}
|
||||
|
||||
var scale = _timeScale;
|
||||
if (!(scale > 0.0))
|
||||
{
|
||||
scale = 0.0;
|
||||
}
|
||||
|
||||
_deltaTime = raw * scale;
|
||||
_realtime += _deltaTime;
|
||||
_frameCount++;
|
||||
_accumulator += _deltaTime;
|
||||
|
||||
var steps = 0;
|
||||
while (steps < MaxFixedStepsPerFrame && _accumulator >= FixedStepSeconds)
|
||||
{
|
||||
_accumulator -= FixedStepSeconds;
|
||||
steps++;
|
||||
}
|
||||
|
||||
if (steps == MaxFixedStepsPerFrame && _accumulator >= FixedStepSeconds)
|
||||
{
|
||||
_accumulator = 0.0;
|
||||
}
|
||||
|
||||
_fixedStepsThisFrame = steps;
|
||||
return steps;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run <paramref name="fixedStep"/> once per fixed step taken by the
|
||||
/// last <see cref="Advance"/> (the fixed-step dispatch: systems marked
|
||||
/// <c>[UpdateInPhase(Phase.FixedUpdate)]</c> tick here with
|
||||
/// <see cref="FixedDeltaTime"/>). Zero steps means zero calls.
|
||||
/// </summary>
|
||||
public static void RunFixedSteps(Action<double> fixedStep)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fixedStep);
|
||||
for (var i = 0; i < _fixedStepsThisFrame; i++)
|
||||
{
|
||||
fixedStep(FixedStepSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the system runs in the fixed step: it carries at least
|
||||
/// one <see cref="UpdateInPhaseAttribute"/> with
|
||||
/// <see cref="Phase.FixedUpdate"/>.
|
||||
/// </summary>
|
||||
public static bool RunsInFixedUpdate(IEnumerable<UpdateInPhaseAttribute> phases)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(phases);
|
||||
foreach (var phase in phases)
|
||||
{
|
||||
if (phase.Phase == Phase.FixedUpdate)
|
||||
var (table, world) = Bound();
|
||||
var rc = table->EngineTimeSetScale(world, value);
|
||||
if (rc != 0)
|
||||
{
|
||||
return true;
|
||||
throw new InvalidOperationException($"time_set_scale failed with code {rc}.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Reset all frame state (tests drive fake frames through this).</summary>
|
||||
public static void ResetForTests(double timeScale = 1.0)
|
||||
/// <summary>Total frames advanced (the first <see cref="Frame"/> makes it 1).</summary>
|
||||
public static ulong FrameCount => ReadU64(SelFrameCount);
|
||||
|
||||
/// <summary>Accumulated scaled time (sum of <see cref="DeltaTime"/>).</summary>
|
||||
public static double RealtimeSinceStartup => ReadF64(SelRealtime);
|
||||
|
||||
/// <summary>Fixed steps taken by the last frame (0..5).</summary>
|
||||
public static int FixedStepsThisFrame => (int)ReadU64(SelFixedSteps);
|
||||
|
||||
/// <summary>Frames whose wall delta exceeded the 0.1 s clamp.</summary>
|
||||
public static ulong ClampCount => ReadU64(SelClampCount);
|
||||
|
||||
private static (NativeApi.ApiTable* Table, NativeApi.EngineWorld* World) Bound()
|
||||
{
|
||||
_timeScale = timeScale;
|
||||
_deltaTime = 0.0;
|
||||
_realtime = 0.0;
|
||||
_accumulator = 0.0;
|
||||
_frameCount = 0;
|
||||
_clampCount = 0;
|
||||
_fixedStepsThisFrame = 0;
|
||||
if (s_table == null || s_world == null)
|
||||
{
|
||||
throw new InvalidOperationException("Time is not bound to a world: call Time.Bind or Time.Frame first.");
|
||||
}
|
||||
|
||||
return (s_table, s_world);
|
||||
}
|
||||
|
||||
private static double ReadF64(uint which)
|
||||
{
|
||||
var (table, world) = Bound();
|
||||
double value;
|
||||
var rc = table->EngineTimeGetF64(world, which, &value);
|
||||
if (rc != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"time_get_f64({which}) failed with code {rc}.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static ulong ReadU64(uint which)
|
||||
{
|
||||
var (table, world) = Bound();
|
||||
ulong value;
|
||||
var rc = table->EngineTimeGetU64(world, which, &value);
|
||||
if (rc != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"time_get_u64({which}) failed with code {rc}.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public static class AbiLayoutTests
|
||||
{
|
||||
var failures = 0;
|
||||
failures += Check(NativeApi.AbiVersionMajor == 4, "abi major == 4");
|
||||
failures += Check(NativeApi.AbiVersionMinor == 11, "abi minor == 11 (4.11: P5-06 audio mixer tail after 4.10 hierarchy)");
|
||||
failures += Check(NativeApi.AbiVersionMinor == 12, "abi minor == 12 (4.12: P2-03 frame-clock tail after 4.11 audio)");
|
||||
failures += Check(NativeApi.AbiVersionPatch == 0, "abi patch == 0");
|
||||
failures += Check(NativeApi.ColumnStride == 64, "column stride == 64");
|
||||
|
||||
@@ -35,12 +35,14 @@ public static class AbiLayoutTests
|
||||
failures += Check(sizeof(NativeApi.WorldTransform) == 64, "WorldTransform 64B");
|
||||
failures += Check(sizeof(NativeApi.EcsChunkView) == 48, "EcsChunkView 48B");
|
||||
failures += Check(sizeof(NativeApi.FrameDispatch) == 4, "FrameDispatch 4B");
|
||||
failures += Check(sizeof(NativeApi.ApiTable) == 784, "ApiTable 784B (ABI 4.11: +9 audio mixer entries)");
|
||||
failures += Check(sizeof(NativeApi.ApiTable) == 824, "ApiTable 824B (ABI 4.12: +5 frame-clock entries)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineEditorDestroyEntity").ToInt64() == 672, "editor_destroy_entity @ 672 (ABI 4.10 tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineEditorSnapshotRestoreSubtree").ToInt64() == 704, "snapshot_restore_subtree @ 704 (ABI 4.10 tail end)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineAudioCreate").ToInt64() == 712, "audio_create @ 712 (ABI 4.11 tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineAudioRenderSineDb").ToInt64() == 776, "audio_render_sine_db @ 776 (ABI 4.11 tail end)");
|
||||
failures += Check(sizeof(NativeApi.EngineAudioWorld) == 8, "EngineAudioWorld opaque 8B");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineSystemSetPhase").ToInt64() == 784, "system_set_phase @ 784 (ABI 4.12 tail)");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineTimeGetU64").ToInt64() == 816, "time_get_u64 @ 816 (ABI 4.12 tail end)");
|
||||
failures += Check(sizeof(NativeApi.InputState) == 1668, "InputState 1668B (P2-02 snapshot)");
|
||||
failures += Check(sizeof(NativeApi.EngineRenderer) == 8, "EngineRenderer opaque 8B");
|
||||
failures += Check(Marshal.OffsetOf<NativeApi.ApiTable>("EngineComponentRegister").ToInt64() == 312, "component_register @ 312 (ABI 4.2 tail)");
|
||||
|
||||
@@ -40,8 +40,8 @@ public static class RuntimeApiTests
|
||||
"UpdateInPhase(Phase.Update) round-trips");
|
||||
failures += Check(
|
||||
(int)Phase.Startup == 0 && (int)Phase.PreUpdate == 1 && (int)Phase.Update == 2 &&
|
||||
(int)Phase.PostUpdate == 3 && (int)Phase.Render == 4,
|
||||
"Phase ordering Startup..Render");
|
||||
(int)Phase.PostUpdate == 3 && (int)Phase.Render == 4 && (int)Phase.FixedUpdate == 5,
|
||||
"Phase values Startup..FixedUpdate match ENGINE_PHASE_*");
|
||||
|
||||
// --- AC2: blittable layouts ----------------------------------------
|
||||
failures += Check(RuntimeHelpers.IsReferenceOrContainsReferences<Entity>() == false, "Entity blittable");
|
||||
|
||||
@@ -135,9 +135,14 @@ extern "C" {
|
||||
// set_delay/set_delay_enabled/render_sine_db) AFTER the 4.10 hierarchy
|
||||
// tail (editor_snapshot_restore_subtree). All earlier entries keep order
|
||||
// + signature; new fields extend the tail only.
|
||||
//
|
||||
// ABI 4.12.0: appends the P2-03 frame-clock tail (system_set_phase/
|
||||
// world_frame/time_set_scale/time_get_f64/time_get_u64) AFTER the 4.11
|
||||
// audio tail (audio_render_sine_db). The world owns the only frame clock;
|
||||
// world_frame advances it and runs the generic systems phase by phase.
|
||||
// ---------------------------------------------------------------------------
|
||||
#define ENGINE_ABI_VERSION_MAJOR 4
|
||||
#define ENGINE_ABI_VERSION_MINOR 11
|
||||
#define ENGINE_ABI_VERSION_MINOR 12
|
||||
#define ENGINE_ABI_VERSION_PATCH 0
|
||||
|
||||
// 64-byte cache-line pitch for the component columns handed to managed
|
||||
@@ -595,6 +600,39 @@ typedef uint32_t (*EngineAudioRenderSineDbFn)(EngineAudioWorld* audio,
|
||||
float volume_db, float* out,
|
||||
uint32_t frames);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// P2-03 frame-clock tail (ABI 4.12): one clock per world + phase routing.
|
||||
//
|
||||
// Every EngineWorld owns a timestep: variable dt clamped to 0.1 s, then
|
||||
// scaled by time_scale; a 1/50 s fixed step drained from an accumulator,
|
||||
// at most 5 fixed steps per frame (surplus dropped). world_frame is the
|
||||
// frame-loop helper: it advances the clock with the caller's wall-clock
|
||||
// delta and runs the generic systems by phase, in this order:
|
||||
// Startup (first world_frame only, dt 0), PreUpdate, Update,
|
||||
// FixedUpdate once per fixed step (dt = 1/50 s), PostUpdate, Render.
|
||||
// Non-fixed phases get the scaled frame dt. systems_run (4.2) keeps its
|
||||
// old meaning: every system once, phase ignored.
|
||||
// All entries are plain blocking Cdecl (never SuppressGCTransition).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Moves generic system `system` (the index system_register returned) to
|
||||
// `phase` (ENGINE_PHASE_*). Returns 0 or ENGINE_ABI_ERR_*.
|
||||
typedef int32_t (*EngineSystemSetPhaseFn)(EngineWorld* world, int32_t system,
|
||||
uint32_t phase);
|
||||
// Advances the world clock by `wall_dt_seconds` and runs one frame of
|
||||
// phased systems. Returns the fixed steps run (0..5) or ENGINE_ABI_ERR_*.
|
||||
typedef int32_t (*EngineWorldFrameFn)(EngineWorld* world,
|
||||
double wall_dt_seconds);
|
||||
// Sets time_scale (<= 0 or non-finite freezes simulation: dt becomes 0;
|
||||
// the value itself is stored as given). Returns 0 or ENGINE_ABI_ERR_*.
|
||||
typedef int32_t (*EngineTimeSetScaleFn)(EngineWorld* world, double scale);
|
||||
// Reads one ENGINE_TIME_* f64 value into *out. Returns 0 or ENGINE_ABI_ERR_*.
|
||||
typedef int32_t (*EngineTimeGetF64Fn)(const EngineWorld* world,
|
||||
uint32_t which, double* out);
|
||||
// Reads one ENGINE_TIME_* u64 value into *out. Returns 0 or ENGINE_ABI_ERR_*.
|
||||
typedef int32_t (*EngineTimeGetU64Fn)(const EngineWorld* world,
|
||||
uint32_t which, uint64_t* out);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generic component/system tail (ABI 4.2: Illusion Layer).
|
||||
//
|
||||
@@ -627,6 +665,25 @@ typedef uint32_t (*EngineAudioRenderSineDbFn)(EngineAudioWorld* audio,
|
||||
#define ENGINE_ABI_ERR_IN_RUN -5
|
||||
#define ENGINE_ABI_ERR_INTERNAL -6
|
||||
|
||||
// P2-03 pipeline phases for system_set_phase (same values as the C#
|
||||
// Engine.Runtime.Phase enum). New systems start in ENGINE_PHASE_UPDATE.
|
||||
#define ENGINE_PHASE_STARTUP 0
|
||||
#define ENGINE_PHASE_PRE_UPDATE 1
|
||||
#define ENGINE_PHASE_UPDATE 2
|
||||
#define ENGINE_PHASE_POST_UPDATE 3
|
||||
#define ENGINE_PHASE_RENDER 4
|
||||
#define ENGINE_PHASE_FIXED_UPDATE 5
|
||||
#define ENGINE_PHASE_COUNT 6
|
||||
|
||||
// P2-03 selectors for time_get_f64 / time_get_u64.
|
||||
#define ENGINE_TIME_DELTA 0 // f64: clamped, scaled frame dt (s)
|
||||
#define ENGINE_TIME_FIXED_DELTA 1 // f64: fixed step, exactly 1/50 s
|
||||
#define ENGINE_TIME_SCALE 2 // f64: time_scale as last set
|
||||
#define ENGINE_TIME_REALTIME 3 // f64: sum of frame dt since create
|
||||
#define ENGINE_TIME_FRAME_COUNT 0 // u64: frames advanced
|
||||
#define ENGINE_TIME_FIXED_STEPS 1 // u64: fixed steps in the last frame
|
||||
#define ENGINE_TIME_CLAMP_COUNT 2 // u64: frames whose dt hit the 0.1 s clamp
|
||||
|
||||
// One matched table slice handed to a generic system callback. 120 bytes
|
||||
// on 64-bit targets (the engine is 64-bit only).
|
||||
typedef struct EngineChunkColumns {
|
||||
@@ -1225,6 +1282,12 @@ typedef struct EngineApiTable {
|
||||
EngineAudioSetDelayFn audio_set_delay;
|
||||
EngineAudioSetDelayEnabledFn audio_set_delay_enabled;
|
||||
EngineAudioRenderSineDbFn audio_render_sine_db;
|
||||
// P2-03 frame-clock tail (ABI 4.12). All earlier order frozen.
|
||||
EngineSystemSetPhaseFn system_set_phase;
|
||||
EngineWorldFrameFn world_frame;
|
||||
EngineTimeSetScaleFn time_set_scale;
|
||||
EngineTimeGetF64Fn time_get_f64;
|
||||
EngineTimeGetU64Fn time_get_u64;
|
||||
|
||||
} EngineApiTable;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "engine/physics/spatial_queries.hpp"
|
||||
#include "engine/physics/world2d.hpp"
|
||||
#include "engine/renderer/compositor_bridge.hpp"
|
||||
#include "engine/renderer/timestep.hpp"
|
||||
|
||||
// enkiTS C handle for the batch lane fan-out (opaque to this TU).
|
||||
struct enkiTaskScheduler;
|
||||
@@ -134,6 +135,10 @@ struct EngineWorldImpl {
|
||||
// The console ring and its sink are process-global (ConsoleLog
|
||||
// singleton below); log_set_sink only validates the world.
|
||||
engine::bridge::WorldProfiler profiler;
|
||||
// P2-03: the world's only frame clock (advanced by world_frame) and
|
||||
// whether Startup-phase systems have run.
|
||||
engine::renderer::Timestep timestep;
|
||||
bool started{false};
|
||||
};
|
||||
|
||||
// Lane geometry for one dispatch (defined at namespace scope: local
|
||||
@@ -1321,6 +1326,110 @@ auto ApiSystemsRun(EngineWorld* world, const FrameDispatch* dispatch)
|
||||
return impl->generic.run(impl->ecs.raw(), dt, &sink);
|
||||
}
|
||||
|
||||
// P2-03 frame-clock tail (ABI 4.12).
|
||||
auto ApiSystemSetPhase(EngineWorld* world, int32_t system, uint32_t phase)
|
||||
-> int32_t {
|
||||
if (world == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
if (system < 0) {
|
||||
return ENGINE_ABI_ERR_BAD_ARG;
|
||||
}
|
||||
return ImplOf(world)->generic.set_phase(static_cast<uint32_t>(system), phase);
|
||||
}
|
||||
|
||||
auto ApiWorldFrame(EngineWorld* world, double wall_dt_seconds) -> int32_t {
|
||||
if (world == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
auto* impl = ImplOf(world);
|
||||
ecs_world_t* raw = impl->ecs.raw();
|
||||
engine::bridge::SystemTimingSink sink{};
|
||||
sink.ctx = &impl->profiler;
|
||||
sink.record = [](void* ctx, uint32_t system, float ms) {
|
||||
static_cast<engine::bridge::WorldProfiler*>(ctx)->record_system(system, ms);
|
||||
};
|
||||
impl->profiler.resize_systems(impl->generic.system_count());
|
||||
const int steps = impl->timestep.advance(wall_dt_seconds);
|
||||
const auto dt = static_cast<float>(impl->timestep.dt());
|
||||
const auto fixed_dt = static_cast<float>(impl->timestep.fixed_dt());
|
||||
int32_t rc = 0;
|
||||
if (!impl->started) {
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_STARTUP, 0.0F, &sink);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
impl->started = true;
|
||||
}
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_PRE_UPDATE, dt, &sink);
|
||||
if (rc == 0) {
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_UPDATE, dt, &sink);
|
||||
}
|
||||
for (int i = 0; rc == 0 && i < steps; ++i) {
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_FIXED_UPDATE, fixed_dt, &sink);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_POST_UPDATE, dt, &sink);
|
||||
}
|
||||
if (rc == 0) {
|
||||
rc = impl->generic.run_phase(raw, ENGINE_PHASE_RENDER, dt, &sink);
|
||||
}
|
||||
return rc == 0 ? steps : rc;
|
||||
}
|
||||
|
||||
auto ApiTimeSetScale(EngineWorld* world, double scale) -> int32_t {
|
||||
if (world == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
ImplOf(world)->timestep.set_time_scale(scale);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto ApiTimeGetF64(const EngineWorld* world, uint32_t which, double* out)
|
||||
-> int32_t {
|
||||
if (world == nullptr || out == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
const auto& ts = ImplOf(const_cast<EngineWorld*>(world))->timestep; // NOLINT
|
||||
switch (which) {
|
||||
case ENGINE_TIME_DELTA:
|
||||
*out = ts.dt();
|
||||
return 0;
|
||||
case ENGINE_TIME_FIXED_DELTA:
|
||||
*out = ts.fixed_dt();
|
||||
return 0;
|
||||
case ENGINE_TIME_SCALE:
|
||||
*out = ts.time_scale();
|
||||
return 0;
|
||||
case ENGINE_TIME_REALTIME:
|
||||
*out = ts.realtime();
|
||||
return 0;
|
||||
default:
|
||||
return ENGINE_ABI_ERR_BAD_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
auto ApiTimeGetU64(const EngineWorld* world, uint32_t which, uint64_t* out)
|
||||
-> int32_t {
|
||||
if (world == nullptr || out == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
const auto& ts = ImplOf(const_cast<EngineWorld*>(world))->timestep; // NOLINT
|
||||
switch (which) {
|
||||
case ENGINE_TIME_FRAME_COUNT:
|
||||
*out = ts.frame_count();
|
||||
return 0;
|
||||
case ENGINE_TIME_FIXED_STEPS:
|
||||
*out = static_cast<uint64_t>(ts.fixed_steps());
|
||||
return 0;
|
||||
case ENGINE_TIME_CLAMP_COUNT:
|
||||
*out = ts.clamp_count();
|
||||
return 0;
|
||||
default:
|
||||
return ENGINE_ABI_ERR_BAD_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
// P3-09 console tail: process-global ring + per-world sink. The ring is
|
||||
// function-local static (constructed on first use, never destroyed —
|
||||
// avoids static-destruction ordering vs worlds destroyed at exit).
|
||||
@@ -2034,6 +2143,11 @@ const EngineApiTable kApiTable = {
|
||||
&ApiAudioSetDelay,
|
||||
&ApiAudioSetDelayEnabled,
|
||||
&ApiAudioRenderSineDb,
|
||||
&ApiSystemSetPhase,
|
||||
&ApiWorldFrame,
|
||||
&ApiTimeSetScale,
|
||||
&ApiTimeGetF64,
|
||||
&ApiTimeGetU64,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -155,6 +155,7 @@ auto GenericSystems::register_system(ecs_world_t* world, const char* name,
|
||||
sys.callback = callback;
|
||||
sys.user_data = user_data;
|
||||
sys.term_count = term_count;
|
||||
sys.phase = ENGINE_PHASE_UPDATE;
|
||||
try {
|
||||
systems_.push_back(sys);
|
||||
} catch (...) {
|
||||
@@ -170,6 +171,32 @@ auto GenericSystems::run(ecs_world_t* world, float dt) noexcept -> int32_t {
|
||||
|
||||
auto GenericSystems::run(ecs_world_t* world, float dt, SystemTimingSink* sink) noexcept
|
||||
-> int32_t {
|
||||
return dispatch(world, -1, dt, sink, true);
|
||||
}
|
||||
|
||||
auto GenericSystems::set_phase(uint32_t system, uint32_t phase) noexcept
|
||||
-> int32_t {
|
||||
if (in_run_) {
|
||||
return ENGINE_ABI_ERR_IN_RUN;
|
||||
}
|
||||
if (system >= systems_.size() || phase >= ENGINE_PHASE_COUNT) {
|
||||
return ENGINE_ABI_ERR_BAD_ARG;
|
||||
}
|
||||
systems_[system].phase = phase;
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto GenericSystems::run_phase(ecs_world_t* world, uint32_t phase, float dt,
|
||||
SystemTimingSink* sink) noexcept -> int32_t {
|
||||
if (phase >= ENGINE_PHASE_COUNT) {
|
||||
return ENGINE_ABI_ERR_BAD_ARG;
|
||||
}
|
||||
return dispatch(world, static_cast<int64_t>(phase), dt, sink, false);
|
||||
}
|
||||
|
||||
auto GenericSystems::dispatch(ecs_world_t* world, int64_t phase, float dt,
|
||||
SystemTimingSink* sink, bool resize) noexcept
|
||||
-> int32_t {
|
||||
if (world == nullptr) {
|
||||
return ENGINE_ABI_ERR_NULL_ARG;
|
||||
}
|
||||
@@ -177,11 +204,15 @@ auto GenericSystems::run(ecs_world_t* world, float dt, SystemTimingSink* sink) n
|
||||
return ENGINE_ABI_ERR_IN_RUN;
|
||||
}
|
||||
in_run_ = true;
|
||||
if (sink != nullptr && sink->resize != nullptr) {
|
||||
if (resize && sink != nullptr && sink->resize != nullptr) {
|
||||
sink->resize(sink->ctx, systems_.size());
|
||||
}
|
||||
uint32_t sys_index = 0;
|
||||
for (const System& sys : systems_) {
|
||||
if (phase >= 0 && static_cast<int64_t>(sys.phase) != phase) {
|
||||
++sys_index;
|
||||
continue;
|
||||
}
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
ecs_iter_t it = ecs_query_iter(world, sys.query);
|
||||
while (ecs_query_next(&it)) {
|
||||
|
||||
@@ -56,6 +56,19 @@ class GenericSystems {
|
||||
// per system in milliseconds, measured around that system's table loop.
|
||||
auto run(ecs_world_t* world, float dt, SystemTimingSink* sink) noexcept -> int32_t;
|
||||
|
||||
// P2-03 phase routing. Every system starts in ENGINE_PHASE_UPDATE.
|
||||
// set_phase moves one system (index from register_system) to another
|
||||
// pipeline phase (ENGINE_PHASE_*). Returns 0 or an ENGINE_ABI_ERR_* code.
|
||||
auto set_phase(uint32_t system, uint32_t phase) noexcept -> int32_t;
|
||||
// Runs only the systems whose phase == `phase`, in registration order.
|
||||
// Timings go to `sink` via record() only (the caller resizes once per
|
||||
// frame), so a frame's phases share one profiler row per system.
|
||||
auto run_phase(ecs_world_t* world, uint32_t phase, float dt,
|
||||
SystemTimingSink* sink) noexcept -> int32_t;
|
||||
[[nodiscard]] auto system_count() const noexcept -> std::size_t {
|
||||
return systems_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
struct System {
|
||||
char name[64];
|
||||
@@ -63,9 +76,15 @@ class GenericSystems {
|
||||
EngineSystemCallback callback;
|
||||
void* user_data;
|
||||
uint32_t term_count;
|
||||
uint32_t phase;
|
||||
int32_t sizes[ENGINE_ABI_MAX_TERMS];
|
||||
};
|
||||
|
||||
// Shared dispatch: runs every system when `phase` < 0, else only the
|
||||
// systems in that phase. resize: call sink->resize first (legacy run).
|
||||
auto dispatch(ecs_world_t* world, int64_t phase, float dt,
|
||||
SystemTimingSink* sink, bool resize) noexcept -> int32_t;
|
||||
|
||||
std::vector<System> systems_;
|
||||
bool in_run_{false};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "engine/bridge/engine_abi.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -38,19 +37,9 @@
|
||||
#include "engine/renderer/mesh_registry.hpp"
|
||||
#include "engine/renderer/renderer_window.hpp"
|
||||
#include "engine/renderer/swapchain.hpp"
|
||||
#include "engine/renderer/timestep.hpp"
|
||||
#include "engine/renderer/vulkan_device.hpp"
|
||||
#include "flecs.h"
|
||||
|
||||
// P2-03: production clock for the timestep. Steady (monotonic); seconds as
|
||||
// double. Kept out of the Timestep class so tests inject a fake clock.
|
||||
namespace {
|
||||
auto NowSeconds() noexcept -> double {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
return std::chrono::duration<double>(now).count();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Process-global VMA allocator (defined in vma_impl.cpp as
|
||||
// ::engine::renderer::vma_bridge::g_alloc; declared here as an opaque
|
||||
// pointer so this TU stays vk_mem_alloc.h-free — same object, same linkage).
|
||||
@@ -126,13 +115,6 @@ struct RendererImpl {
|
||||
::engine::renderer::FrameGpuTimer gpu_timer;
|
||||
bool gpu_timer_live{false};
|
||||
double gpu_ms{-1.0};
|
||||
// P2-03: frame-loop timestep (variable dt clamped to 0.1 s, fixed 1/50 s
|
||||
// with accumulator, max 5 fixed steps, time_scale). Advanced once per
|
||||
// renderer_frame from the steady clock; FixedUpdate dispatch + managed
|
||||
// Time read through the accessors below. last_frame_wall_s < 0 means
|
||||
// "no frame yet" (first frame advances with dt 0).
|
||||
::engine::renderer::Timestep timestep;
|
||||
double last_frame_wall_s{-1.0};
|
||||
};
|
||||
|
||||
auto ImplOf(EngineRenderer* r) noexcept -> RendererImpl* {
|
||||
@@ -177,21 +159,6 @@ auto RenderOne(RendererImpl* impl,
|
||||
if (quit != 0) {
|
||||
return 1;
|
||||
}
|
||||
// P2-03: advance the timestep once per frame from the steady clock.
|
||||
// First frame has no previous stamp, so it advances with dt 0 (no
|
||||
// fixed steps, no time jump). Managed Time reads dt/fixed_steps/
|
||||
// time_scale/frame_count/realtime from impl->timestep (ABI getters in
|
||||
// the follow-up Time slice expose them); FixedUpdate dispatch consumes
|
||||
// fixed_steps() the same way.
|
||||
{
|
||||
const double now_s = NowSeconds();
|
||||
double wall_dt = 0.0;
|
||||
if (impl->last_frame_wall_s >= 0.0) {
|
||||
wall_dt = now_s - impl->last_frame_wall_s;
|
||||
}
|
||||
impl->last_frame_wall_s = now_s;
|
||||
impl->timestep.advance(wall_dt);
|
||||
}
|
||||
auto begin = impl->frames.begin_frame();
|
||||
if (!begin.ok || begin.cmd == nullptr) {
|
||||
return ENGINE_ABI_ERR_INTERNAL;
|
||||
|
||||
@@ -616,6 +616,14 @@ engine_apply_base_flags(engine_generic_systems)
|
||||
add_test(NAME engine_generic_systems COMMAND engine_generic_systems)
|
||||
set_tests_properties(engine_generic_systems PROPERTIES TIMEOUT 120)
|
||||
|
||||
# P2-03: world_frame phase routing + world clock through the ABI
|
||||
# (FixedUpdate once per fixed step, fake clock [0.016, 0.016, 0.2, 0.001]).
|
||||
add_executable(engine_world_frame test_world_frame.cpp)
|
||||
target_link_libraries(engine_world_frame PRIVATE engine_core)
|
||||
engine_apply_base_flags(engine_world_frame)
|
||||
add_test(NAME engine_world_frame COMMAND engine_world_frame)
|
||||
set_tests_properties(engine_world_frame PROPERTIES TIMEOUT 120)
|
||||
|
||||
# P6-03: Recast bake + DetourCrowd agent acceptance (20x20 floor, center
|
||||
# box, agent routes around it within 600 frames at 60 Hz).
|
||||
add_executable(engine_navigation_p6_03 test_navigation_p6_03.cpp)
|
||||
@@ -1112,7 +1120,7 @@ add_test(NAME engine_timestep COMMAND engine_timestep)
|
||||
set_tests_properties(engine_timestep PROPERTIES TIMEOUT 120)
|
||||
|
||||
if(WIN32)
|
||||
foreach(tgt engine_audio_spatial engine_input engine_timestep)
|
||||
foreach(tgt engine_audio_spatial engine_input engine_timestep engine_world_frame)
|
||||
add_custom_command(TARGET ${tgt} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_RUNTIME_DLLS:${tgt}>
|
||||
|
||||
@@ -218,6 +218,11 @@ auto main() -> int {
|
||||
failures += check(api->audio_set_delay != nullptr, "abi: audio_set_delay set");
|
||||
failures += check(api->audio_set_delay_enabled != nullptr, "abi: audio_set_delay_enabled set");
|
||||
failures += check(api->audio_render_sine_db != nullptr, "abi: audio_render_sine_db set");
|
||||
failures += check(api->system_set_phase != nullptr, "abi: system_set_phase set");
|
||||
failures += check(api->world_frame != nullptr, "abi: world_frame set");
|
||||
failures += check(api->time_set_scale != nullptr, "abi: time_set_scale set");
|
||||
failures += check(api->time_get_f64 != nullptr, "abi: time_get_f64 set");
|
||||
failures += check(api->time_get_u64 != nullptr, "abi: time_get_u64 set");
|
||||
|
||||
// --- 2. Layout pins ---------------------------------------------------------
|
||||
failures += check(sizeof(EngineLocalTransform) == 64, "abi: EngineLocalTransform 64B");
|
||||
@@ -233,7 +238,7 @@ auto main() -> int {
|
||||
failures += check(offsetof(EcsChunkView, world) == 8, "abi: EcsChunkView.world @ 8");
|
||||
failures += check(offsetof(EcsChunkView, entities) == 16, "abi: EcsChunkView.entities @ 16");
|
||||
failures += check(offsetof(EcsChunkView, user_data) == 40, "abi: EcsChunkView.user_data @ 40");
|
||||
failures += check(sizeof(EngineApiTable) == 784, "abi: EngineApiTable 784B (ABI 4.11 audio mixer tail)");
|
||||
failures += check(sizeof(EngineApiTable) == 824, "abi: EngineApiTable 824B (ABI 4.12 frame-clock tail)");
|
||||
failures += check(sizeof(EngineInputState) == 1668, "abi: EngineInputState 1668B");
|
||||
failures += check(offsetof(EngineInputState, mouse_x) == 1536, "abi: EngineInputState.mouse_x @ 1536");
|
||||
failures += check(offsetof(EngineInputState, mouse_buttons) == 1552, "abi: EngineInputState.mouse_buttons @ 1552");
|
||||
@@ -304,6 +309,11 @@ auto main() -> int {
|
||||
"abi: audio_create @ 712 (4.11 tail)");
|
||||
failures += check(offsetof(EngineApiTable, audio_render_sine_db) == 776,
|
||||
"abi: audio_render_sine_db @ 776 (4.11 tail end)");
|
||||
// P2-03 frame-clock tail (ABI 4.12): 5 appended entries.
|
||||
failures += check(offsetof(EngineApiTable, system_set_phase) == 784,
|
||||
"abi: system_set_phase @ 784 (4.12 tail)");
|
||||
failures += check(offsetof(EngineApiTable, time_get_u64) == 816,
|
||||
"abi: time_get_u64 @ 816 (4.12 tail end)");
|
||||
failures += check(sizeof(EngineCollisionEvent2D) == 32, "abi: EngineCollisionEvent2D 32B");
|
||||
failures += check(offsetof(EngineCollisionEvent2D, point_x) == 16,
|
||||
"abi: EngineCollisionEvent2D.point_x @ 16");
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
// test_world_frame.cpp — P2-03 acceptance through the ABI (CTest
|
||||
// `engine_world_frame`).
|
||||
//
|
||||
// Registers generic systems the way generated C# does (system_register,
|
||||
// then system_set_phase), drives world_frame with the spec's fake clock
|
||||
// steps [0.016, 0.016, 0.2, 0.001], and checks that FixedUpdate systems
|
||||
// run once per fixed step with dt 1/50 s while Update/PostUpdate run once
|
||||
// per frame with the clamped dt. The accumulator math is written out in
|
||||
// test_timestep.cpp; expected fixed steps are [0, 1, 5, 0], one clamp.
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "engine/bridge/engine_abi.h"
|
||||
|
||||
namespace {
|
||||
|
||||
auto check(bool ok, const std::string& name) -> int {
|
||||
std::printf("[%s] %s\n", ok ? "PASS" : "FAIL", name.c_str());
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
|
||||
auto near(double a, double b) -> bool { return std::fabs(a - b) < 1e-6; }
|
||||
|
||||
struct Counter {
|
||||
float value;
|
||||
};
|
||||
|
||||
// One record per callback: which phase ran and the dt it received.
|
||||
struct Call {
|
||||
int phase;
|
||||
float dt;
|
||||
};
|
||||
std::vector<Call> g_calls;
|
||||
|
||||
void OnStartup(const EngineChunkColumns* /*view*/, float dt) {
|
||||
g_calls.push_back({ENGINE_PHASE_STARTUP, dt});
|
||||
}
|
||||
void OnUpdate(const EngineChunkColumns* /*view*/, float dt) {
|
||||
g_calls.push_back({ENGINE_PHASE_UPDATE, dt});
|
||||
}
|
||||
void OnFixed(const EngineChunkColumns* view, float dt) {
|
||||
g_calls.push_back({ENGINE_PHASE_FIXED_UPDATE, dt});
|
||||
// Integrate with the fixed dt: after N steps value == N * 0.02.
|
||||
auto* counters = static_cast<Counter*>(view->columns[0]);
|
||||
for (int32_t i = 0; i < view->count; ++i) {
|
||||
counters[i].value += dt;
|
||||
}
|
||||
}
|
||||
void OnPostUpdate(const EngineChunkColumns* /*view*/, float dt) {
|
||||
g_calls.push_back({ENGINE_PHASE_POST_UPDATE, dt});
|
||||
}
|
||||
|
||||
auto CountPhase(int phase) -> int {
|
||||
int n = 0;
|
||||
for (const Call& c : g_calls) {
|
||||
n += (c.phase == phase) ? 1 : 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
auto main() -> int {
|
||||
int failures = 0;
|
||||
const EngineApiTable* api = engine_get_api_table();
|
||||
failures += check(api != nullptr && api->world_frame != nullptr, "api table has world_frame");
|
||||
if (api == nullptr || api->world_frame == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
EngineWorld* world = api->world_create();
|
||||
failures += check(world != nullptr, "world_create");
|
||||
if (world == nullptr) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint64_t counter = api->component_register(world, "Test.Counter", 4, 4);
|
||||
const uint64_t entity = api->entity_create(world);
|
||||
const Counter zero{0.0F};
|
||||
failures += check(counter != 0 && api->entity_set(world, entity, counter, &zero, 4) == 0,
|
||||
"one Counter entity");
|
||||
|
||||
const std::array<uint64_t, 1> ids{counter};
|
||||
const std::array<uint32_t, 1> access{ENGINE_ABI_ACCESS_INOUT};
|
||||
// Registration order is deliberately not pipeline order: world_frame
|
||||
// must order by phase, not by index.
|
||||
const int32_t s_post = api->system_register(world, "Post", ids.data(), access.data(), 1,
|
||||
&OnPostUpdate, nullptr);
|
||||
const int32_t s_fixed = api->system_register(world, "Fixed", ids.data(), access.data(), 1,
|
||||
&OnFixed, nullptr);
|
||||
const int32_t s_update = api->system_register(world, "Update", ids.data(), access.data(), 1,
|
||||
&OnUpdate, nullptr);
|
||||
const int32_t s_start = api->system_register(world, "Start", ids.data(), access.data(), 1,
|
||||
&OnStartup, nullptr);
|
||||
failures += check(s_post == 0 && s_fixed == 1 && s_update == 2 && s_start == 3,
|
||||
"four systems registered");
|
||||
failures += check(api->system_set_phase(world, s_post, ENGINE_PHASE_POST_UPDATE) == 0,
|
||||
"set_phase PostUpdate");
|
||||
failures += check(api->system_set_phase(world, s_fixed, ENGINE_PHASE_FIXED_UPDATE) == 0,
|
||||
"set_phase FixedUpdate");
|
||||
failures += check(api->system_set_phase(world, s_start, ENGINE_PHASE_STARTUP) == 0,
|
||||
"set_phase Startup");
|
||||
failures += check(api->system_set_phase(world, s_update, ENGINE_PHASE_COUNT) ==
|
||||
ENGINE_ABI_ERR_BAD_ARG,
|
||||
"set_phase out-of-range phase -> BAD_ARG");
|
||||
failures += check(api->system_set_phase(world, 99, ENGINE_PHASE_UPDATE) == ENGINE_ABI_ERR_BAD_ARG,
|
||||
"set_phase unknown system -> BAD_ARG");
|
||||
failures += check(api->system_set_phase(world, -1, ENGINE_PHASE_UPDATE) == ENGINE_ABI_ERR_BAD_ARG,
|
||||
"set_phase negative system -> BAD_ARG");
|
||||
|
||||
// --- The spec's clock: [0.016, 0.016, 0.2, 0.001] -> [0, 1, 5, 0] ---------
|
||||
const std::array<double, 4> wall{0.016, 0.016, 0.2, 0.001};
|
||||
const std::array<int, 4> want_steps{0, 1, 5, 0};
|
||||
const std::array<float, 4> want_dt{0.016F, 0.016F, 0.1F, 0.001F};
|
||||
for (std::size_t f = 0; f < wall.size(); ++f) {
|
||||
g_calls.clear();
|
||||
const int32_t steps = api->world_frame(world, wall[f]);
|
||||
const std::string tag = "frame " + std::to_string(f + 1) + ": ";
|
||||
failures += check(steps == want_steps[f],
|
||||
tag + "world_frame returns " + std::to_string(steps) + " fixed steps (want " +
|
||||
std::to_string(want_steps[f]) + ")");
|
||||
failures += check(CountPhase(ENGINE_PHASE_FIXED_UPDATE) == want_steps[f],
|
||||
tag + "FixedUpdate system ran once per fixed step");
|
||||
failures += check(CountPhase(ENGINE_PHASE_UPDATE) == 1 &&
|
||||
CountPhase(ENGINE_PHASE_POST_UPDATE) == 1,
|
||||
tag + "Update and PostUpdate ran once");
|
||||
failures += check(CountPhase(ENGINE_PHASE_STARTUP) == (f == 0 ? 1 : 0),
|
||||
tag + "Startup runs on the first frame only");
|
||||
bool dt_ok = true;
|
||||
for (const Call& c : g_calls) {
|
||||
if (c.phase == ENGINE_PHASE_FIXED_UPDATE) {
|
||||
dt_ok = dt_ok && c.dt == static_cast<float>(1.0 / 50.0);
|
||||
} else if (c.phase == ENGINE_PHASE_STARTUP) {
|
||||
dt_ok = dt_ok && c.dt == 0.0F;
|
||||
} else {
|
||||
dt_ok = dt_ok && std::fabs(c.dt - want_dt[f]) < 1e-6F;
|
||||
}
|
||||
}
|
||||
failures += check(dt_ok, tag + "fixed dt == 1/50 s, frame dt == clamped wall dt");
|
||||
// Pipeline order: [Startup], Update, FixedUpdate x N, PostUpdate.
|
||||
bool order_ok = !g_calls.empty() && g_calls.back().phase == ENGINE_PHASE_POST_UPDATE;
|
||||
for (std::size_t i = 1; i < g_calls.size(); ++i) {
|
||||
order_ok = order_ok && g_calls[i - 1].phase != ENGINE_PHASE_POST_UPDATE;
|
||||
if (g_calls[i].phase == ENGINE_PHASE_FIXED_UPDATE) {
|
||||
order_ok = order_ok && (g_calls[i - 1].phase == ENGINE_PHASE_UPDATE ||
|
||||
g_calls[i - 1].phase == ENGINE_PHASE_FIXED_UPDATE);
|
||||
}
|
||||
}
|
||||
failures += check(order_ok, tag + "phases ran in pipeline order");
|
||||
}
|
||||
|
||||
Counter got{};
|
||||
failures += check(api->entity_get(world, entity, counter, &got, 4) == 0 &&
|
||||
std::fabs(got.value - 6.0F * 0.02F) < 1e-5F,
|
||||
"FixedUpdate integrated 6 steps of 0.02 s on the entity");
|
||||
|
||||
uint64_t frames = 0;
|
||||
uint64_t clamps = 0;
|
||||
uint64_t last_steps = 99;
|
||||
double realtime = 0.0;
|
||||
double fixed_dt = 0.0;
|
||||
failures += check(api->time_get_u64(world, ENGINE_TIME_FRAME_COUNT, &frames) == 0 && frames == 4,
|
||||
"time: frame_count == 4");
|
||||
failures += check(api->time_get_u64(world, ENGINE_TIME_CLAMP_COUNT, &clamps) == 0 && clamps == 1,
|
||||
"time: clamp happened once");
|
||||
failures += check(api->time_get_u64(world, ENGINE_TIME_FIXED_STEPS, &last_steps) == 0 &&
|
||||
last_steps == 0,
|
||||
"time: fixed_steps of the last frame == 0");
|
||||
failures += check(api->time_get_f64(world, ENGINE_TIME_REALTIME, &realtime) == 0 &&
|
||||
near(realtime, 0.016 + 0.016 + 0.1 + 0.001),
|
||||
"time: realtime == sum of clamped dt (0.133)");
|
||||
failures += check(api->time_get_f64(world, ENGINE_TIME_FIXED_DELTA, &fixed_dt) == 0 &&
|
||||
fixed_dt == 1.0 / 50.0,
|
||||
"time: fixed_dt exactly 1/50 s");
|
||||
failures += check(api->time_get_f64(world, 42, &fixed_dt) == ENGINE_ABI_ERR_BAD_ARG &&
|
||||
api->time_get_u64(world, 42, &frames) == ENGINE_ABI_ERR_BAD_ARG,
|
||||
"time: unknown selector -> BAD_ARG");
|
||||
|
||||
// --- time_scale 0 freezes simulation ---------------------------------------
|
||||
double scale = -1.0;
|
||||
failures += check(api->time_set_scale(world, 0.0) == 0 &&
|
||||
api->time_get_f64(world, ENGINE_TIME_SCALE, &scale) == 0 && scale == 0.0,
|
||||
"time_set_scale(0) round-trips");
|
||||
g_calls.clear();
|
||||
double dt = -1.0;
|
||||
failures += check(api->world_frame(world, 0.05) == 0 &&
|
||||
api->time_get_f64(world, ENGINE_TIME_DELTA, &dt) == 0 && dt == 0.0 &&
|
||||
CountPhase(ENGINE_PHASE_FIXED_UPDATE) == 0,
|
||||
"scale 0: dt 0 and no fixed steps");
|
||||
failures += check(api->time_set_scale(world, 2.0) == 0 && api->world_frame(world, 0.02) == 2,
|
||||
"scale 2: 0.02 s wall -> 0.04 s scaled -> 2 fixed steps");
|
||||
|
||||
// --- Legacy systems_run still runs every system once, phase ignored -------
|
||||
g_calls.clear();
|
||||
const FrameDispatch dispatch{0.5F};
|
||||
failures += check(api->systems_run(world, &dispatch) == 0 && g_calls.size() == 4,
|
||||
"systems_run (4.2) runs all four systems once");
|
||||
|
||||
failures += check(api->world_frame(nullptr, 0.016) == ENGINE_ABI_ERR_NULL_ARG,
|
||||
"world_frame(NULL) -> NULL_ARG");
|
||||
|
||||
api->world_destroy(world);
|
||||
std::printf(failures == 0 ? "world_frame: all checks passed\n" : "world_frame: %d FAILED\n",
|
||||
failures);
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -174,7 +174,10 @@ internal static unsafe class Program
|
||||
var closed = false;
|
||||
for (var f = 0; f < frames && !closed; f++)
|
||||
{
|
||||
Ecs.RunSystems(table, world, 1.0f / 60.0f);
|
||||
// P2-03 frame loop: advance the world clock and run the
|
||||
// systems by phase. A fixed 1/60 s wall delta keeps the
|
||||
// golden screenshot deterministic.
|
||||
Time.Frame(table, world, 1.0 / 60.0);
|
||||
Check(table->EngineWorldPropagate(world) == 0, "world_propagate");
|
||||
closed = renderer.Frame();
|
||||
}
|
||||
@@ -189,6 +192,7 @@ internal static unsafe class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
Time.Unbind();
|
||||
table->EngineWorldDestroy(world);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,3 +60,24 @@ public partial struct Bomb : IComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// P2-03 fixed-step system: ticks once per 1/50 s fixed step, never once
|
||||
/// per frame. Proves the generated FixedUpdate path end to end.
|
||||
/// </summary>
|
||||
[UpdateInPhase(Phase.FixedUpdate)]
|
||||
public partial struct FixedStepper : IComponent
|
||||
{
|
||||
/// <summary>Fixed steps this row has run.</summary>
|
||||
public int Steps;
|
||||
|
||||
/// <summary>Sum of the dt values this row received.</summary>
|
||||
public float Elapsed;
|
||||
|
||||
/// <summary>Per-row fixed-step update.</summary>
|
||||
public readonly void Tick(ref FixedStepper self, float dt)
|
||||
{
|
||||
self.Steps++;
|
||||
self.Elapsed += dt;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@
|
||||
// 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.
|
||||
// 5. P2-03 FixedUpdate: a generated [UpdateInPhase(Phase.FixedUpdate)]
|
||||
// system runs once per fixed step through Time.Frame (fake clock
|
||||
// [0.016, 0.016, 0.2, 0.001] -> fixed steps [0, 1, 5, 0]).
|
||||
// Prints ILLUSION-SAMPLE-OK and exits 0 on success; exits 1 on any failure.
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -34,6 +37,7 @@ internal static unsafe class Program
|
||||
CheckLayout();
|
||||
CheckBehaviour(table);
|
||||
CheckZeroGc(table);
|
||||
CheckFixedUpdate(table);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -161,6 +165,50 @@ internal static unsafe class Program
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckFixedUpdate(NativeApi.ApiTable* table)
|
||||
{
|
||||
var world = table->EngineWorldCreate();
|
||||
Check(world != null, "world_create (fixed-step world)");
|
||||
try
|
||||
{
|
||||
Check(FixedStepper.Register(table, world, null) == 0, "FixedStepper.Register -> system 0");
|
||||
Check(Spinner.Register(table, world, null) == 1, "Spinner.Register -> system 1 (Update)");
|
||||
var stepper = Ecs.CreateEntity(table, world);
|
||||
Ecs.Set(table, world, stepper, new FixedStepper());
|
||||
var spinner = Ecs.CreateEntity(table, world);
|
||||
Ecs.Set(table, world, spinner, new Spinner());
|
||||
|
||||
double[] wall = [0.016, 0.016, 0.2, 0.001];
|
||||
int[] wantSteps = [0, 1, 5, 0];
|
||||
var stepsSeen = 0;
|
||||
for (var f = 0; f < wall.Length; f++)
|
||||
{
|
||||
var steps = Time.Frame(table, world, wall[f]);
|
||||
stepsSeen += steps;
|
||||
var rowSteps = Ecs.Get<FixedStepper>(table, world, stepper).Steps;
|
||||
Check(steps == wantSteps[f] && Time.FixedStepsThisFrame == wantSteps[f],
|
||||
$"frame {f + 1}: {steps} fixed steps (want {wantSteps[f]})");
|
||||
Check(rowSteps == stepsSeen, $"frame {f + 1}: FixedStepper ran once per fixed step ({rowSteps} == {stepsSeen})");
|
||||
}
|
||||
|
||||
var s = Ecs.Get<FixedStepper>(table, world, stepper);
|
||||
Check(s.Steps == 6 && MathF.Abs(s.Elapsed - 0.12f) < 1e-5f, $"FixedStepper: 6 steps of 1/50 s (Elapsed {s.Elapsed})");
|
||||
var angle = Ecs.Get<Spinner>(table, world, spinner).Angle;
|
||||
Check(MathF.Abs(angle - 0.133f) < 1e-5f, $"Spinner (Update) got the clamped frame dt once per frame (Angle {angle})");
|
||||
Check(Time.FrameCount == 4 && Time.ClampCount == 1, "Time: FrameCount 4, one clamp");
|
||||
Check(Time.FixedDeltaTime == 1.0 / 50.0, "Time.FixedDeltaTime == 1/50 s");
|
||||
Check(Math.Abs(Time.RealtimeSinceStartup - 0.133) < 1e-9, "Time.RealtimeSinceStartup == 0.133");
|
||||
Check(Math.Abs(Time.DeltaTime - 0.001) < 1e-9, "Time.DeltaTime == last clamped dt");
|
||||
Time.TimeScale = 0.0;
|
||||
Check(Time.Frame(table, world, 0.05) == 0 && Time.DeltaTime == 0.0, "TimeScale 0 freezes the clock");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Time.Unbind();
|
||||
table->EngineWorldDestroy(world);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeApi.ApiTable* LoadApiTable(string libPath)
|
||||
{
|
||||
var handle = NativeLibrary.Load(Path.GetFullPath(libPath));
|
||||
|
||||
@@ -464,7 +464,8 @@ internal static class Program
|
||||
or "EngineNavStats*" or "uint8_t*"
|
||||
or "int32_t*" or "char*"
|
||||
or "EngineLogSinkCallback"
|
||||
or "EngineAudioWorld*" or "const EngineAudioWorld*";
|
||||
or "EngineAudioWorld*" or "const EngineAudioWorld*"
|
||||
or "double" or "double*";
|
||||
if (!ok)
|
||||
{
|
||||
throw new InvalidOperationException($"{header}:{line}: unsupported ABI type '{ctype}' (extend the generator's type map deliberately).");
|
||||
@@ -562,6 +563,8 @@ internal static class Program
|
||||
"uint8_t*" => "byte*",
|
||||
"EngineAudioWorld*" => "EngineAudioWorld*",
|
||||
"const EngineAudioWorld*" => "EngineAudioWorld*",
|
||||
"double" => "double",
|
||||
"double*" => "double*",
|
||||
_ => throw new InvalidOperationException($"no C# mapping for '{ctype}'"),
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user