From 82492d8a6ca1f3f18b8e9eea8192c409aae2ae77 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 8 Jul 2026 17:27:41 -0400 Subject: [PATCH] feat: add FrameClock (virtual clock + per-frame op budget) Co-Authored-By: Claude Opus 4.8 --- engine/Age.Engine.Tests/FrameClockTests.cs | 29 ++++++++++++++++++++++ engine/Age.Engine/Hosting/FrameClock.cs | 23 +++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 engine/Age.Engine.Tests/FrameClockTests.cs create mode 100644 engine/Age.Engine/Hosting/FrameClock.cs diff --git a/engine/Age.Engine.Tests/FrameClockTests.cs b/engine/Age.Engine.Tests/FrameClockTests.cs new file mode 100644 index 0000000..6a30797 --- /dev/null +++ b/engine/Age.Engine.Tests/FrameClockTests.cs @@ -0,0 +1,29 @@ +using Age.Engine.Hosting; +using Xunit; + +public class FrameClockTests +{ + [Fact] + public void Advance_AtSpeed1_AddsRealMilliseconds() + { + var c = new FrameClock(); // Speed defaults to 1.0 + c.Advance(0.016); // one ~60fps frame + Assert.Equal(16, c.NowMs); + } + + [Fact] + public void Advance_ScalesBySpeed() + { + var c = new FrameClock { Speed = 4.0 }; + c.Advance(0.016); + Assert.Equal(64, c.NowMs); // 4x virtual time + } + + [Fact] + public void EffectiveBudget_ScalesBySpeed_AndFloorsAtOne() + { + Assert.Equal(30, new FrameClock { OpsPerFrame = 30, Speed = 1.0 }.EffectiveBudget); + Assert.Equal(120, new FrameClock { OpsPerFrame = 30, Speed = 4.0 }.EffectiveBudget); + Assert.Equal(1, new FrameClock { OpsPerFrame = 0, Speed = 1.0 }.EffectiveBudget); + } +} diff --git a/engine/Age.Engine/Hosting/FrameClock.cs b/engine/Age.Engine/Hosting/FrameClock.cs new file mode 100644 index 0000000..3ff0433 --- /dev/null +++ b/engine/Age.Engine/Hosting/FrameClock.cs @@ -0,0 +1,23 @@ +namespace Age.Engine.Hosting; + +/// Host-owned virtual clock + per-frame op budget. Pure (no threading): the Godot host +/// advances it once per rendered frame and consults it to pace the VM. The one +/// factor is the future (unwired) Ctrl fast-forward multiplier — scaling it scales the throttle +/// budget, sleeps, and the anim tween together. See docs/superpowers/specs/2026-07-08-frame-stepped-vm-design.md. +public sealed class FrameClock +{ + /// Monotonic virtual time in milliseconds (scaled by Speed). + public long NowMs { get; private set; } + + /// Speed multiplier. 1.0 = normal. The future Ctrl hook (ADV-scoped); leave at 1.0 for now. + public double Speed = 1.0; + + /// Base per-frame interpreter op budget (tunable by eye; ~30 ≈ 1,800 ops/sec at 60fps). + public int OpsPerFrame = 30; + + /// Advance the clock by one rendered frame's real delta (seconds), scaled by Speed. + public void Advance(double realDeltaSeconds) => NowMs += (long)(realDeltaSeconds * 1000.0 * Speed); + + /// Ops the VM may run before yielding a frame, scaled by Speed (min 1). + public int EffectiveBudget => System.Math.Max(1, (int)System.Math.Round(OpsPerFrame * Speed)); +}