From 6ab4df39dc67ab3e2d04c499e2ab9e10bd538348 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 3 Aug 2026 09:16:46 -0400 Subject: [PATCH] Extract VM control flow handler --- docs/PROJECT-STRUCTURE.md | 3 + docs/remake-architecture-and-roadmap.md | 12 ++- .../Vm/VirtualMachine.ControlFlow.cs | 91 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 63 +------------ 4 files changed, 107 insertions(+), 62 deletions(-) create mode 100644 engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index 77d74ef..f61a1e7 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -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, 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. +`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-.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 69c5eb8..7b5467c 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -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 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 deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after 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` 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 -through domain handlers, extract control-flow/coroutine dispatch next without replacing the proven dispatcher or -changing public types, commands, and generated output. +through domain handlers, with control-flow/coroutine dispatch now isolated as well, extract process/root-exit and +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 not replace Phase B gameplay validation or the open cross-platform gates. diff --git a/engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs b/engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs new file mode 100644 index 0000000..622c486 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.ControlFlow.cs @@ -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}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 7817906..5f9585b 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1344,79 +1344,22 @@ public sealed partial class VirtualMachine case "random-modulo": // 0x60: native CRT rand() % bound case "u0041A270": return StepMemoryCollection(label, a, pc); - 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 "jmp": + case "call": 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; - } + return StepControlFlow(label, ins, pc); case "throw-exit-request": if (_o.IgnoreExitRequests) return pc + 1; // Native op 0x1 throws Command_Exit_Exception through callbacks and nested script