Merge feat/frame-stepped-vm: throttle Godot VM to per-frame op budget
Fixes the SC0000 opening speeding through: FrameClock (virtual clock + op budget) + IHost.FrameYield per-opcode hook (no-op headless, parity held) + Godot host throttle/Sleep on the clock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -317,6 +317,7 @@ sealed class AudioTraceHost : IHost
|
|||||||
public void ShowText(int offset, string text) { }
|
public void ShowText(int offset, string text) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int slot, int width, int height) { }
|
public void CreateTexture(int slot, int width, int height) { }
|
||||||
public void SetTexture(long resourceId, int slot) { }
|
public void SetTexture(long resourceId, int slot) { }
|
||||||
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
||||||
@@ -366,6 +367,7 @@ sealed class GfxTraceHost : IHost
|
|||||||
public void ShowText(int offset, string text) { }
|
public void ShowText(int offset, string text) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void PlayBgm(long id) { }
|
public void PlayBgm(long id) { }
|
||||||
public void PlayVoice(long id) { }
|
public void PlayVoice(long id) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class CallScriptIntegrationTests
|
|||||||
public void ShowText(int o, string t) { }
|
public void ShowText(int o, string t) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int s, int w, int h) { }
|
public void CreateTexture(int s, int w, int h) { }
|
||||||
public void SetTexture(long r, int s) { }
|
public void SetTexture(long r, int s) { }
|
||||||
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class CallScriptTests
|
|||||||
public void ShowText(int o, string t) { }
|
public void ShowText(int o, string t) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int s, int w, int h) { }
|
public void CreateTexture(int s, int w, int h) { }
|
||||||
public void SetTexture(long r, int s) { }
|
public void SetTexture(long r, int s) { }
|
||||||
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
|||||||
29
engine/Age.Engine.Tests/FrameClockTests.cs
Normal file
29
engine/Age.Engine.Tests/FrameClockTests.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
engine/Age.Engine.Tests/FrameYieldTests.cs
Normal file
35
engine/Age.Engine.Tests/FrameYieldTests.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using Age.Engine.Hosting;
|
||||||
|
using Age.Engine.Model;
|
||||||
|
using Age.Engine.Sys4;
|
||||||
|
using Age.Engine.Vm;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
public class FrameYieldTests
|
||||||
|
{
|
||||||
|
private sealed class CountingHost : IHost
|
||||||
|
{
|
||||||
|
public long Yields;
|
||||||
|
public void FrameYield() => Yields++;
|
||||||
|
public void ShowText(int offset, string text) { }
|
||||||
|
public void WaitForInput() { }
|
||||||
|
public void Sleep(long duration) { }
|
||||||
|
public void CreateTexture(int slot, int width, int height) { }
|
||||||
|
public void SetTexture(long resourceId, int slot) { }
|
||||||
|
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
public (int Width, int Height) GetTextureSize(int slot) => (0, 0);
|
||||||
|
public void PlayBgm(long id) { }
|
||||||
|
public void PlayVoice(long id) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FrameYield_CalledOncePerStep()
|
||||||
|
{
|
||||||
|
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||||
|
var script = Sys4Loader.Load(Paths.Scripts()["INITCONFIG.BIN"], table);
|
||||||
|
var host = new CountingHost();
|
||||||
|
var vm = new VirtualMachine(script, table, host);
|
||||||
|
vm.Run();
|
||||||
|
Assert.True(vm.Steps > 0);
|
||||||
|
Assert.Equal(vm.Steps, host.Yields); // exactly one FrameYield per executed opcode
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ public class GameSessionTests
|
|||||||
public void ShowText(int o, string t) => Emitted.Add(o);
|
public void ShowText(int o, string t) => Emitted.Add(o);
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int s, int w, int h) { }
|
public void CreateTexture(int s, int w, int h) { }
|
||||||
public void SetTexture(long r, int s) { }
|
public void SetTexture(long r, int s) { }
|
||||||
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ internal sealed class RecordingHost : IHost
|
|||||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||||
public void WaitForInput() => Waits++;
|
public void WaitForInput() => Waits++;
|
||||||
public void Sleep(long duration) => SleptDurations.Add(duration);
|
public void Sleep(long duration) => SleptDurations.Add(duration);
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int slot, int w, int h) { }
|
public void CreateTexture(int slot, int w, int h) { }
|
||||||
public void SetTexture(long resId, int slot) { }
|
public void SetTexture(long resId, int slot) { }
|
||||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class TextureGeometryTests
|
|||||||
public void ShowText(int o, string t) { }
|
public void ShowText(int o, string t) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int slot, int w, int h) { }
|
public void CreateTexture(int slot, int w, int h) { }
|
||||||
public void SetTexture(long resId, int slot) { }
|
public void SetTexture(long resId, int slot) { }
|
||||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class TextureOpsTests
|
|||||||
public void ShowText(int o, string t) { }
|
public void ShowText(int o, string t) { }
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int slot, int w, int h) => Creates++;
|
public void CreateTexture(int slot, int w, int h) => Creates++;
|
||||||
public void SetTexture(long resId, int slot) => Sets.Add((resId, slot));
|
public void SetTexture(long resId, int slot) => Sets.Add((resId, slot));
|
||||||
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h));
|
public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public sealed class CaptureHost : IHost
|
|||||||
public void ShowText(int offset, string text) => Emitted.Add((offset, text));
|
public void ShowText(int offset, string text) => Emitted.Add((offset, text));
|
||||||
public void WaitForInput() { }
|
public void WaitForInput() { }
|
||||||
public void Sleep(long duration) { }
|
public void Sleep(long duration) { }
|
||||||
|
public void FrameYield() { }
|
||||||
public void CreateTexture(int slot, int width, int height) { }
|
public void CreateTexture(int slot, int width, int height) { }
|
||||||
public void SetTexture(long resourceId, int slot) { }
|
public void SetTexture(long resourceId, int slot) { }
|
||||||
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { }
|
||||||
|
|||||||
23
engine/Age.Engine/Hosting/FrameClock.cs
Normal file
23
engine/Age.Engine/Hosting/FrameClock.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
namespace Age.Engine.Hosting;
|
||||||
|
|
||||||
|
/// <summary>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 <see cref="Speed"/>
|
||||||
|
/// 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.</summary>
|
||||||
|
public sealed class FrameClock
|
||||||
|
{
|
||||||
|
/// <summary>Monotonic virtual time in milliseconds (scaled by Speed).</summary>
|
||||||
|
public long NowMs { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Speed multiplier. 1.0 = normal. The future Ctrl hook (ADV-scoped); leave at 1.0 for now.</summary>
|
||||||
|
public double Speed = 1.0;
|
||||||
|
|
||||||
|
/// <summary>Base per-frame interpreter op budget (tunable by eye; ~30 ≈ 1,800 ops/sec at 60fps).</summary>
|
||||||
|
public int OpsPerFrame = 30;
|
||||||
|
|
||||||
|
/// <summary>Advance the clock by one rendered frame's real delta (seconds), scaled by Speed.</summary>
|
||||||
|
public void Advance(double realDeltaSeconds) => NowMs += (long)(realDeltaSeconds * 1000.0 * Speed);
|
||||||
|
|
||||||
|
/// <summary>Ops the VM may run before yielding a frame, scaled by Speed (min 1).</summary>
|
||||||
|
public int EffectiveBudget => System.Math.Max(1, (int)System.Math.Round(OpsPerFrame * Speed));
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ public interface IHost
|
|||||||
void ShowText(int offset, string text);
|
void ShowText(int offset, string text);
|
||||||
void WaitForInput();
|
void WaitForInput();
|
||||||
void Sleep(long duration);
|
void Sleep(long duration);
|
||||||
|
void FrameYield();
|
||||||
void CreateTexture(int slot, int width, int height);
|
void CreateTexture(int slot, int width, int height);
|
||||||
void SetTexture(long resourceId, int slot);
|
void SetTexture(long resourceId, int slot);
|
||||||
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY);
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ public sealed class VirtualMachine
|
|||||||
Steps++;
|
Steps++;
|
||||||
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, frame.Script.Instructions[pc], _depth));
|
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Step(pc, frame.Script.Instructions[pc], _depth));
|
||||||
int next = Step(frame.Script.Instructions[pc], pc);
|
int next = Step(frame.Script.Instructions[pc], pc);
|
||||||
|
_host.FrameYield();
|
||||||
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
if (next == FRAME_RETURN) { outcome = FrameOutcome.Returned; break; }
|
||||||
if (next == HALT) { outcome = FrameOutcome.Halted; break; }
|
if (next == HALT) { outcome = FrameOutcome.Halted; break; }
|
||||||
pc = next;
|
pc = next;
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
// the single-scene harness skips; seed it so the first CG's anchor math stays correct (not 0x0).
|
||||||
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
private readonly Dictionary<int, (int W, int H)> _slotDims = new() { { 0, (800, 600) } };
|
||||||
private readonly SemaphoreSlim _gate = new(0, 1);
|
private readonly SemaphoreSlim _gate = new(0, 1);
|
||||||
|
private readonly Age.Engine.Hosting.FrameClock _clock;
|
||||||
|
private readonly System.Threading.AutoResetEvent _frameSignal = new(false);
|
||||||
|
private int _opsSinceYield;
|
||||||
public volatile bool IsWaiting;
|
public volatile bool IsWaiting;
|
||||||
public readonly List<(int Offset, string Text)> Captured = new();
|
public readonly List<(int Offset, string Text)> Captured = new();
|
||||||
|
|
||||||
public GodotAdvHost(Main main, ResourceMap res, string scene)
|
public GodotAdvHost(Main main, ResourceMap res, string scene, Age.Engine.Hosting.FrameClock clock)
|
||||||
{
|
{
|
||||||
_main = main; _res = res; _scene = scene;
|
_main = main; _res = res; _scene = scene; _clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowText(int offset, string text)
|
public void ShowText(int offset, string text)
|
||||||
@@ -42,16 +45,35 @@ public sealed class GodotAdvHost : IHost
|
|||||||
// called from the main thread (click) or the selftest auto-clicker
|
// called from the main thread (click) or the selftest auto-clicker
|
||||||
public void SignalInput() { if (_gate.CurrentCount == 0) _gate.Release(); }
|
public void SignalInput() { if (_gate.CurrentCount == 0) _gate.Release(); }
|
||||||
|
|
||||||
|
// Main thread, once per rendered frame: releases a VM thread parked in FrameYield/Sleep.
|
||||||
|
public void PulseFrame() => _frameSignal.Set();
|
||||||
|
|
||||||
|
// Called once per executed opcode (IHost.FrameYield). After a frame's worth of ops (the clock's
|
||||||
|
// budget), block the VM background thread until Main._Process advances the clock — throttling the
|
||||||
|
// interpreter to ~budget ops per rendered frame (the native engine's rate-limited cadence).
|
||||||
|
public void FrameYield()
|
||||||
|
{
|
||||||
|
if (++_opsSinceYield < _clock.EffectiveBudget) return;
|
||||||
|
_opsSinceYield = 0;
|
||||||
|
long start = _clock.NowMs;
|
||||||
|
while (_clock.NowMs == start) // wait until a real _Process advanced the clock
|
||||||
|
if (!_frameSignal.WaitOne(50)) break; // 50ms safety cap: never hang if _Process stalls
|
||||||
|
}
|
||||||
|
|
||||||
// op 0xc8: block the VM background thread so the main-thread compositor (Main.Recomposite in _Process)
|
// op 0xc8: block the VM background thread so the main-thread compositor (Main.Recomposite in _Process)
|
||||||
// presents the current retained GfxState — this is what makes the sleep-paced opening burst animate.
|
// presents the current retained GfxState — this is what makes the sleep-paced opening burst animate.
|
||||||
// Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer;
|
// Time-based sibling of WaitForInput's suspend. The native op arms a non-blocking main-loop-polled timer;
|
||||||
// blocking this throwaway task thread is behaviorally equivalent given our threading model. Operand is
|
// blocking this throwaway task thread is behaviorally equivalent given our threading model. Operand is
|
||||||
// MILLISECONDS (docs/engine-re.md sleep section + opcodes.toml 0xc8). Headless CLI hosts no-op it (parity).
|
// MILLISECONDS (docs/engine-re.md sleep section + opcodes.toml 0xc8). Headless CLI hosts no-op it (parity).
|
||||||
public double SleepScale = 1.0; // --sleep-scale <f>: debug multiplier to slow/speed the paced opening for inspection
|
public double SleepScale = 1.0; // --sleep-scale <f>: debug multiplier to slow/speed the paced opening for inspection
|
||||||
|
// Wait on the unified FrameClock timebase (not Thread.Sleep) so a future Speed multiplier scales
|
||||||
|
// sleeps together with the throttle and the tween. Main._Process advances the clock + pulses each frame.
|
||||||
public void Sleep(long duration)
|
public void Sleep(long duration)
|
||||||
{
|
{
|
||||||
int ms = (int)System.Math.Clamp(duration * SleepScale, 0, 60_000); // cap so a pathological script can't hang the window
|
long ms = (long)System.Math.Clamp(duration * SleepScale, 0, 60_000); // cap so a pathological script can't hang the window
|
||||||
if (ms > 0) Thread.Sleep(ms);
|
long deadline = _clock.NowMs + ms;
|
||||||
|
while (_clock.NowMs < deadline)
|
||||||
|
if (!_frameSignal.WaitOne(2000)) break; // safety cap
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- texture ops (run on the VM thread; marshal Godot node work to the main thread) ----
|
// ---- texture ops (run on the VM thread; marshal Godot node work to the main thread) ----
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public partial class Main : Godot.Control
|
|||||||
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
|
||||||
private VirtualMachine _vm = null!;
|
private VirtualMachine _vm = null!;
|
||||||
private GodotAdvHost _host = null!;
|
private GodotAdvHost _host = null!;
|
||||||
|
private readonly Age.Engine.Hosting.FrameClock _clock = new();
|
||||||
private GodotTraceSink _trace = null!;
|
private GodotTraceSink _trace = null!;
|
||||||
private Age.Engine.Diagnostics.HistogramTraceSink? _hist; // --trace-histogram: profile the real run
|
private Age.Engine.Diagnostics.HistogramTraceSink? _hist; // --trace-histogram: profile the real run
|
||||||
private string? _histFile;
|
private string? _histFile;
|
||||||
@@ -119,7 +120,7 @@ public partial class Main : Godot.Control
|
|||||||
IScriptProvider provider;
|
IScriptProvider provider;
|
||||||
if (_selftest) (script, provider) = BuildSelfTestScene(table);
|
if (_selftest) (script, provider) = BuildSelfTestScene(table);
|
||||||
else { script = Sys4Loader.Load(Paths.Scripts()[scene.ToUpperInvariant() + ".BIN"], table); provider = Sys4ScriptProvider.Load(table); }
|
else { script = Sys4Loader.Load(Paths.Scripts()[scene.ToUpperInvariant() + ".BIN"], table); provider = Sys4ScriptProvider.Load(table); }
|
||||||
_host = new GodotAdvHost(this, ResourceMap.Load(), scene) { SleepScale = sleepScale };
|
_host = new GodotAdvHost(this, ResourceMap.Load(), scene, _clock) { SleepScale = sleepScale };
|
||||||
_trace = new GodotTraceSink();
|
_trace = new GodotTraceSink();
|
||||||
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
|
// --trace-histogram: aggregate op/call-site execution counts of the REAL Godot run (headless flow
|
||||||
// diverges — wait-for-input is a no-op there — so this is the only way to profile the live path).
|
// diverges — wait-for-input is a no-op there — so this is the only way to profile the live path).
|
||||||
@@ -156,6 +157,8 @@ public partial class Main : Godot.Control
|
|||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
_lastDelta = delta;
|
_lastDelta = delta;
|
||||||
|
_clock.Advance(delta);
|
||||||
|
_host?.PulseFrame();
|
||||||
if (!_selftest && _vm != null) Recomposite(); // retained per-frame compositor (surface+object model)
|
if (!_selftest && _vm != null) Recomposite(); // retained per-frame compositor (surface+object model)
|
||||||
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
// --shot-sequence: dump one PNG per frame across the opening so a time-based (paced) effect can be
|
||||||
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
// verified as distinct frames, not just the final state. Captures after Recomposite; quits when full.
|
||||||
@@ -278,7 +281,7 @@ public partial class Main : Godot.Control
|
|||||||
tw.TargetA = targetA;
|
tw.TargetA = targetA;
|
||||||
tw.Initialized = true;
|
tw.Initialized = true;
|
||||||
}
|
}
|
||||||
tw.Elapsed += _lastDelta;
|
tw.Elapsed += _lastDelta * _clock.Speed; // Speed==1 now => identical; future Ctrl scales the tween
|
||||||
double p = tw.Duration > 0 ? System.Math.Clamp(tw.Elapsed / tw.Duration, 0, 1) : 1;
|
double p = tw.Duration > 0 ? System.Math.Clamp(tw.Elapsed / tw.Duration, 0, 1) : 1;
|
||||||
tw.CurrentA = tw.StartA + (tw.TargetA - tw.StartA) * p;
|
tw.CurrentA = tw.StartA + (tw.TargetA - tw.StartA) * p;
|
||||||
return (float)tw.CurrentA;
|
return (float)tw.CurrentA;
|
||||||
|
|||||||
Reference in New Issue
Block a user