Extract VM timing opcode handler

This commit is contained in:
gamer147
2026-08-03 00:20:44 -04:00
parent 03ddbf52d6
commit cc0ebbee4b
4 changed files with 91 additions and 59 deletions

View File

@@ -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 `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 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. 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-<SCENE>.jsonl` files are produced by editor/development Godot runs and map The disposable `build/page-map-<SCENE>.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 runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports

View File

@@ -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. host-thread input entry points, shared synchronization, and callback service helpers remain in the coordinator.
Runtime validation remains green. 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 **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 deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
each domain move. each domain move.
@@ -1059,10 +1065,9 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step ## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed 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` 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 domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text,
text-history `VirtualMachine.Step` families routed through domain handlers, with ADV skip/auto-message services text-history, ADV-service, input, and timing `VirtualMachine.Step` families routed through domain handlers,
and interactive input/hotspot/cursor callbacks now isolated as well, extract the sleep/timed-callback sequencing extract numbered-save/shared-profile persistence dispatch next without replacing the proven dispatcher or
opcode family next without replacing the proven dispatcher or changing public types, commands, and generated changing public types, commands, and generated output.
output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does 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. not replace Phase B gameplay validation or the open cross-platform gates.

View File

@@ -0,0 +1,76 @@
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepTiming(string label, IReadOnlyList<Operand> 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}");
}
}
}

View File

@@ -1247,7 +1247,7 @@ public sealed partial class VirtualMachine
return pc + 1; return pc + 1;
} }
case "get-monotonic-time-ms": 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 "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 "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; 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 "u0041E5E0":
case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table case "map-keyboard-scancode": // 0x10c: logical action <- DIK translated through native VK table
return StepInput(label, a, pc); return StepInput(label, a, pc);
case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. case "sleep":
_host.Sleep(Read(a[0])); return pc + 1;
case "u00425960": case "u00425960":
case "begin-timed-callback-sequence": // 0xd3: clear the frame-local relative schedule 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 "u004266F0":
case "append-relative-timed-callbacks": // 0xd4: (interval ms, count, on-time PC, catch-up PC) 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 "u004262C0":
case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret case "run-timed-callback-sequence": // 0xd5: dispatch each scheduled local callback, resuming here after ret
{ return StepTiming(label, a, pc);
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;
}
case "u0041B290": case "u0041B290":
case "set-message-skip": // 0x88: persistent all-message fast-forward service state case "set-message-skip": // 0x88: persistent all-message fast-forward service state
case "u00414E50": // 0x19a: persistent state used by the SO001 active overlay case "u00414E50": // 0x19a: persistent state used by the SO001 active overlay