From f26477082b24dce526697cf4a370e47dbd3680bb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 00:20:44 -0400 Subject: [PATCH] Extract VM timing opcode handler --- docs/PROJECT-STRUCTURE.md | 2 + docs/remake-architecture-and-roadmap.md | 15 ++-- engine/Age.Engine/Vm/VirtualMachine.Timing.cs | 76 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 57 +------------- 4 files changed, 91 insertions(+), 59 deletions(-) create mode 100644 engine/Age.Engine/Vm/VirtualMachine.Timing.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 17219d1..b827038 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -198,6 +198,8 @@ and refresh helpers remain in the VM coordinator because live text and input pat `engine/Age.Engine/Vm/VirtualMachine.Input.cs` owns blocking ADV waits, hotspot registration/arming, cursor resources and virtual position, raw mouse/joystick callback registration and dispatch, action polling, and physical-input mapping; public host-thread input entry points and shared synchronization remain in the coordinator. +`engine/Age.Engine/Vm/VirtualMachine.Timing.cs` owns the monotonic-time query, host sleep, relative timed-callback +schedule construction, deadline/catch-up selection, and callback resumption opcode handler. The disposable `build/page-map-.jsonl` files are produced by editor/development Godot runs and map runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports diff --git a/docs/remake-architecture-and-roadmap.md b/docs/remake-architecture-and-roadmap.md index 0b985c4..2a036af 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -688,6 +688,12 @@ do not mix mechanical moves with semantic changes. host-thread input entry points, shared synchronization, and callback service helpers remain in the coordinator. Runtime validation remains green. + The eleventh bounded `VirtualMachine.Step` extraction moved the monotonic-time query, host sleep, relative + timed-callback schedule construction, deadline/catch-up selection, and callback resumption into + `engine/Age.Engine/Vm/VirtualMachine.Timing.cs`. The top-level dispatcher retains all labels and aliases at + their existing positions and routes the separated groups through guarded `StepTiming`; animation-frame + sampling remains with `StepAnimation`. Runtime validation remains green. + **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -1059,10 +1065,9 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed by the tracked launcher and layered validation driver. With the planned `Main`, `GodotAdvHost`, and `GfxState` -domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, and -text-history `VirtualMachine.Step` families routed through domain handlers, with ADV skip/auto-message services -and interactive input/hotspot/cursor callbacks now isolated as well, extract the sleep/timed-callback sequencing -opcode family next without replacing the proven dispatcher or changing public types, commands, and generated -output. +domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text, +text-history, ADV-service, input, and timing `VirtualMachine.Step` families routed through domain handlers, +extract numbered-save/shared-profile persistence dispatch next without replacing the proven dispatcher or +changing public types, commands, and generated output. Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/engine/Age.Engine/Vm/VirtualMachine.Timing.cs b/engine/Age.Engine/Vm/VirtualMachine.Timing.cs new file mode 100644 index 0000000..f6d1038 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.Timing.cs @@ -0,0 +1,76 @@ +using Age.Engine.Model; + +namespace Age.Engine.Vm; + +public sealed partial class VirtualMachine +{ + private int StepTiming(string label, IReadOnlyList a, int pc) + { + switch (label) + { + case "get-monotonic-time-ms": + Write(a[0], unchecked((int)_host.InputClockMilliseconds)); return pc + 1; + case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. + _host.Sleep(Read(a[0])); return pc + 1; + case "u00425960": + case "begin-timed-callback-sequence": // 0xd3: clear the frame-local relative schedule + _cur.TimedCallbacks.Clear(); + _cur.TimedCallbackCursor = 0; + _cur.TimedCallbackStartedAtMs = null; + _cur.TimedCallbackAbortOffset = -1; + return pc + 1; + case "u004266F0": + case "append-relative-timed-callbacks": // 0xd4: (interval ms, count, on-time PC, catch-up PC) + { + long intervalMs = Read(a[0]); + int count = System.Math.Max(0, unchecked((int)Read(a[1]))); + int primaryOffset = unchecked((int)Read(a[2])); + int catchUpOffset = unchecked((int)Read(a[3])); + long deadlineMs = _cur.TimedCallbacks.Count == 0 ? 0 : _cur.TimedCallbacks[^1].DeadlineMs; + for (int i = 0; i < count; i++) + { + deadlineMs += intervalMs; + _cur.TimedCallbacks.Add(new ExecFrame.TimedCallback( + deadlineMs, primaryOffset, catchUpOffset)); + } + return pc + 1; + } + case "u004262C0": + case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret + { + if (_cur.TimedCallbackStartedAtMs == null) + { + _cur.TimedCallbackStartedAtMs = _host.InputClockMilliseconds; + _cur.TimedCallbackAbortOffset = unchecked((int)Read(a[0])); + } + // Native op 0xd5 stops at last_index rather than count. The final entry is a + // look-ahead sentinel: it supplies the next deadline for the preceding event but + // is not itself dispatched. + if (_cur.TimedCallbackCursor >= _cur.TimedCallbacks.Count - 1) + { + _cur.TimedCallbackStartedAtMs = null; + return pc + 1; + } + + var callback = _cur.TimedCallbacks[_cur.TimedCallbackCursor]; + long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; + if (elapsedMs < callback.DeadlineMs) + { + _host.WaitForTimedCallbackDeadline(callback.DeadlineMs - elapsedMs); + elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; + } + + bool fellBehind = _cur.TimedCallbackCursor + 1 < _cur.TimedCallbacks.Count + && _cur.TimedCallbacks[_cur.TimedCallbackCursor + 1].DeadlineMs < elapsedMs; + int targetOffset = fellBehind ? callback.CatchUpOffset : callback.PrimaryOffset; + _cur.TimedCallbackCursor++; + if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target)) + return pc; + _cur.CallStack.Add(pc); + return target; + } + default: + throw new InvalidOperationException($"Non-timing opcode routed to timing handler: {label}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index dca387b..dec90ab 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1247,7 +1247,7 @@ public sealed partial class VirtualMachine return pc + 1; } case "get-monotonic-time-ms": - Write(a[0], unchecked((int)_host.InputClockMilliseconds)); return pc + 1; + return StepTiming(label, a, pc); case "lt": Write(a[0], Read(a[1]) < Read(a[2]) ? 1 : 0); return pc + 1; case "lte": Write(a[0], Read(a[1]) <= Read(a[2]) ? 1 : 0); return pc + 1; case "gr": Write(a[0], Read(a[1]) > Read(a[2]) ? 1 : 0); return pc + 1; @@ -2019,65 +2019,14 @@ public sealed partial class VirtualMachine case "u0041E5E0": case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table return StepInput(label, a, pc); - case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. - _host.Sleep(Read(a[0])); return pc + 1; + case "sleep": case "u00425960": case "begin-timed-callback-sequence": // 0xd3: clear the frame-local relative schedule - _cur.TimedCallbacks.Clear(); - _cur.TimedCallbackCursor = 0; - _cur.TimedCallbackStartedAtMs = null; - _cur.TimedCallbackAbortOffset = -1; - return pc + 1; case "u004266F0": case "append-relative-timed-callbacks": // 0xd4: (interval ms, count, on-time PC, catch-up PC) - { - long intervalMs = Read(a[0]); - int count = System.Math.Max(0, unchecked((int)Read(a[1]))); - int primaryOffset = unchecked((int)Read(a[2])); - int catchUpOffset = unchecked((int)Read(a[3])); - long deadlineMs = _cur.TimedCallbacks.Count == 0 ? 0 : _cur.TimedCallbacks[^1].DeadlineMs; - for (int i = 0; i < count; i++) - { - deadlineMs += intervalMs; - _cur.TimedCallbacks.Add(new ExecFrame.TimedCallback( - deadlineMs, primaryOffset, catchUpOffset)); - } - return pc + 1; - } case "u004262C0": case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret - { - if (_cur.TimedCallbackStartedAtMs == null) - { - _cur.TimedCallbackStartedAtMs = _host.InputClockMilliseconds; - _cur.TimedCallbackAbortOffset = unchecked((int)Read(a[0])); - } - // Native op 0xd5 stops at last_index rather than count. The final entry is a - // look-ahead sentinel: it supplies the next deadline for the preceding event but - // is not itself dispatched. - if (_cur.TimedCallbackCursor >= _cur.TimedCallbacks.Count - 1) - { - _cur.TimedCallbackStartedAtMs = null; - return pc + 1; - } - - var callback = _cur.TimedCallbacks[_cur.TimedCallbackCursor]; - long elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; - if (elapsedMs < callback.DeadlineMs) - { - _host.WaitForTimedCallbackDeadline(callback.DeadlineMs - elapsedMs); - elapsedMs = _host.InputClockMilliseconds - _cur.TimedCallbackStartedAtMs.Value; - } - - bool fellBehind = _cur.TimedCallbackCursor + 1 < _cur.TimedCallbacks.Count - && _cur.TimedCallbacks[_cur.TimedCallbackCursor + 1].DeadlineMs < elapsedMs; - int targetOffset = fellBehind ? callback.CatchUpOffset : callback.PrimaryOffset; - _cur.TimedCallbackCursor++; - if (targetOffset < 0 || !_cur.Script.IndexByOffset.TryGetValue(targetOffset, out int target)) - return pc; - _cur.CallStack.Add(pc); - return target; - } + return StepTiming(label, a, pc); case "u0041B290": case "set-message-skip": // 0x88: persistent all-message fast-forward service state case "u00414E50": // 0x19a: persistent state used by the SO001 active overlay