Extract VM control flow handler

This commit is contained in:
gamer147
2026-08-03 09:16:46 -04:00
parent 16ba4ec683
commit 6ab4df39dc
4 changed files with 107 additions and 62 deletions

View File

@@ -206,6 +206,9 @@ dispatch; capture/apply helpers and persistent coordinator state remain in `Virt
`engine/Age.Engine/Vm/VirtualMachine.MemoryCollections.cs` owns string byte length, addressed lookup/copy, `engine/Age.Engine/Vm/VirtualMachine.MemoryCollections.cs` owns string byte length, addressed lookup/copy,
inline arrays, rectangle search and stable index sorting, bounded integer queues/stacks, bit/range operations, inline arrays, rectangle search and stable index sorting, bounded integer queues/stacks, bit/range operations,
and native-style random-modulo dispatch; shared storage/address helpers remain in the coordinator. and native-style random-modulo dispatch; shared storage/address helpers remain in the coordinator.
`engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs` owns local jumps/calls/returns, value-switch construction,
ADV coroutine handler save/yield/resume, and bounded labeled-yield dispatch; process/root exit and cross-script
lifecycle remain in `VirtualMachine.cs`.
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

@@ -707,6 +707,13 @@ do not mix mechanical moves with semantic changes.
aliases at their existing positions and routes them through guarded `StepMemoryCollection`; shared storage and aliases at their existing positions and routes them through guarded `StepMemoryCollection`; shared storage and
address-resolution helpers remain in the coordinator. Runtime validation remains green. address-resolution helpers remain in the coordinator. Runtime validation remains green.
The fourteenth bounded `VirtualMachine.Step` extraction moved local jumps/calls/returns, value-switch
construction, ADV coroutine handler save/yield/resume, and bounded labeled-yield dispatch into
`engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs`. The top-level dispatcher retains all labels and aliases
at their existing positions and routes them through guarded `StepControlFlow`; the complete instruction remains
available for labeled-yield opcode/offset diagnostics. Process/root exit and cross-script lifecycle remain in
the coordinator. 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.
@@ -1080,7 +1087,8 @@ Continue step 2 of the **codebase consolidation** maintenance slice: behavior-ne
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, domains isolated and the audio, movie, surface/texture, retained-object, animation, presentation, ADV-text,
text-history, ADV-service, input, timing, persistence, and memory/collection `VirtualMachine.Step` families routed text-history, ADV-service, input, timing, persistence, and memory/collection `VirtualMachine.Step` families routed
through domain handlers, extract control-flow/coroutine dispatch next without replacing the proven dispatcher or through domain handlers, with control-flow/coroutine dispatch now isolated as well, extract process/root-exit and
changing public types, commands, and generated output. cross-script lifecycle 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 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,91 @@
using Age.Engine.Diagnostics;
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepControlFlow(string label, Instruction ins, int pc)
{
int op = ins.Opcode;
var a = ins.Args;
switch (label)
{
case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
case "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1);
case "ret":
if (_cur.CallStack.Count > 0) { int r = _cur.CallStack[^1]; _cur.CallStack.RemoveAt(_cur.CallStack.Count - 1); return r; }
return FRAME_RETURN; // empty intra-call stack => return from the script frame
case "jcc":
{
long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value;
return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1);
}
case "begin-value-switch":
_valueSwitchTargets.Clear(); return pc + 1;
case "add-value-switch-case":
_valueSwitchTargets[FormatSwitchValue(a[0])] = checked((int)Read(a[1])); return pc + 1;
case "value-switch-jump":
{
int target = _valueSwitchTargets.TryGetValue(FormatSwitchValue(a[0]), out int matched)
? matched : checked((int)Read(a[1]));
return _cur.Script.IndexByOffset.GetValueOrDefault(target, pc + 1);
}
case "u0041ADB0":
case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata
_cur.CoroutineYieldHandlerA = (int)Read(a[0]);
_cur.CoroutineYieldHandlerB = (int)Read(a[1]);
return pc + 1;
case "u00414D50":
case "yield-adv-coroutine": // 0x199: A -> nested service -> B -> 0x7c resume
{
int? targetOffset;
if (!_cur.CoroutineYieldActive)
{
_cur.CoroutineResumePc = pc + 1;
_cur.CoroutineYieldActive = true;
_host.SetAdvPagePresentationSuspended(Gfx, true);
targetOffset = _cur.CoroutineYieldHandlerA;
}
else targetOffset = _cur.CoroutineYieldHandlerB;
return targetOffset is int offset
? _cur.Script.IndexByOffset.GetValueOrDefault(offset, pc + 1)
: pc + 1;
}
case "u00416A90":
case "coroutine-resume": // 0x7c: restore the PC saved by op 0x199
if (_cur.CoroutineResumePc is int resumePc)
{
_cur.CoroutineResumePc = null;
_cur.CoroutineYieldActive = false;
_host.SetAdvPagePresentationSuspended(Gfx, false);
return resumePc;
}
return pc + 1; // cold bounded scene-entry path
case "u0041F9C0":
case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only
{
if (!IsAdvLabeledYield(_cur.Script, ins))
{
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc));
return pc + 1;
}
if (!TryGetAdvYieldTerminal(pc, a[0], out long terminal))
{
HaltReason ??= $"coroutine-yield-pattern@0x{ins.Offset:x}";
return HALT;
}
int visits = _cur.CoroutineYieldVisits.GetValueOrDefault(pc);
_cur.CoroutineYieldVisits[pc] = visits + 1;
// First visit must enter setup even if out retained this same terminal from a prior scene.
// Every later visit returns the script-encoded terminal and exits the bounded loop.
Write(a[0], visits == 0 ? (terminal == 0 ? 1 : 0) : terminal);
return pc + 1;
}
default:
throw new InvalidOperationException($"Non-control-flow opcode routed to control-flow handler: {label}");
}
}
}

View File

@@ -1344,79 +1344,22 @@ public sealed partial class VirtualMachine
case "random-modulo": // 0x60: native CRT rand() % bound case "random-modulo": // 0x60: native CRT rand() % bound
case "u0041A270": case "u0041A270":
return StepMemoryCollection(label, a, pc); return StepMemoryCollection(label, a, pc);
case "jmp": return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); case "jmp":
case "call": _cur.CallStack.Add(pc + 1); return _cur.Script.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); case "call":
case "ret": case "ret":
if (_cur.CallStack.Count > 0) { int r = _cur.CallStack[^1]; _cur.CallStack.RemoveAt(_cur.CallStack.Count - 1); return r; }
return FRAME_RETURN; // empty intra-call stack => return from the script frame
case "jcc": case "jcc":
{
long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value;
return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1);
}
case "begin-value-switch": case "begin-value-switch":
_valueSwitchTargets.Clear(); return pc + 1;
case "add-value-switch-case": case "add-value-switch-case":
_valueSwitchTargets[FormatSwitchValue(a[0])] = checked((int)Read(a[1])); return pc + 1;
case "value-switch-jump": case "value-switch-jump":
{
int target = _valueSwitchTargets.TryGetValue(FormatSwitchValue(a[0]), out int matched)
? matched : checked((int)Read(a[1]));
return _cur.Script.IndexByOffset.GetValueOrDefault(target, pc + 1);
}
case "u0041ADB0": case "u0041ADB0":
case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata
_cur.CoroutineYieldHandlerA = (int)Read(a[0]);
_cur.CoroutineYieldHandlerB = (int)Read(a[1]);
return pc + 1;
case "u00414D50": case "u00414D50":
case "yield-adv-coroutine": // 0x199: A -> nested service -> B -> 0x7c resume case "yield-adv-coroutine": // 0x199: A -> nested service -> B -> 0x7c resume
{
int? targetOffset;
if (!_cur.CoroutineYieldActive)
{
_cur.CoroutineResumePc = pc + 1;
_cur.CoroutineYieldActive = true;
_host.SetAdvPagePresentationSuspended(Gfx, true);
targetOffset = _cur.CoroutineYieldHandlerA;
}
else targetOffset = _cur.CoroutineYieldHandlerB;
return targetOffset is int offset
? _cur.Script.IndexByOffset.GetValueOrDefault(offset, pc + 1)
: pc + 1;
}
case "u00416A90": case "u00416A90":
case "coroutine-resume": // 0x7c: restore the PC saved by op 0x199 case "coroutine-resume": // 0x7c: restore the PC saved by op 0x199
if (_cur.CoroutineResumePc is int resumePc)
{
_cur.CoroutineResumePc = null;
_cur.CoroutineYieldActive = false;
_host.SetAdvPagePresentationSuspended(Gfx, false);
return resumePc;
}
return pc + 1; // cold bounded scene-entry path
case "u0041F9C0": case "u0041F9C0":
case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only case "coroutine-label-yield": // 0x140: bounded host model for LABEL/J only
{ return StepControlFlow(label, ins, pc);
if (!IsAdvLabeledYield(_cur.Script, ins))
{
if (_sink.TracingSteps) _sink.Emit(TraceEvent.Stub(op, pc));
return pc + 1;
}
if (!TryGetAdvYieldTerminal(pc, a[0], out long terminal))
{
HaltReason ??= $"coroutine-yield-pattern@0x{ins.Offset:x}";
return HALT;
}
int visits = _cur.CoroutineYieldVisits.GetValueOrDefault(pc);
_cur.CoroutineYieldVisits[pc] = visits + 1;
// First visit must enter setup even if out retained this same terminal from a prior scene.
// Every later visit returns the script-encoded terminal and exits the bounded loop.
Write(a[0], visits == 0 ? (terminal == 0 ? 1 : 0) : terminal);
return pc + 1;
}
case "throw-exit-request": case "throw-exit-request":
if (_o.IgnoreExitRequests) return pc + 1; if (_o.IgnoreExitRequests) return pc + 1;
// Native op 0x1 throws Command_Exit_Exception through callbacks and nested script // Native op 0x1 throws Command_Exit_Exception through callbacks and nested script