Extract VM script lifecycle handler

This commit is contained in:
gamer147
2026-08-03 09:29:47 -04:00
parent 82749fc1ba
commit 4e7ea79082
4 changed files with 157 additions and 121 deletions

View File

@@ -208,7 +208,9 @@ inline arrays, rectangle search and stable index sorting, bounded integer queues
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, `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 ADV coroutine handler save/yield/resume, and bounded labeled-yield dispatch; process/root exit and cross-script
lifecycle remain in `VirtualMachine.cs`. lifecycle are routed separately. `engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs` owns process/frame/root
exit, ordinary cross-script calls, mounted append autoruns, and preloaded script-slot load/call dispatch; frame
execution, script-provider access, and shared lifecycle state 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

@@ -714,6 +714,13 @@ do not mix mechanical moves with semantic changes.
available for labeled-yield opcode/offset diagnostics. Process/root exit and cross-script lifecycle remain in available for labeled-yield opcode/offset diagnostics. Process/root exit and cross-script lifecycle remain in
the coordinator. Runtime validation remains green. the coordinator. Runtime validation remains green.
The fifteenth bounded `VirtualMachine.Step` extraction moved process/frame/root exit, ordinary cross-script
calls, mounted append autoruns, and preloaded script-slot load/call dispatch into
`engine/Age.Engine/Vm/VirtualMachine.ScriptLifecycle.cs`. The top-level dispatcher retains all labels and
aliases at their existing positions and routes them through guarded `StepScriptLifecycle`; frame execution,
provider access, call-depth enforcement, and shared lifecycle state 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.
@@ -1087,8 +1094,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, with control-flow/coroutine dispatch now isolated as well, extract process/root-exit and through domain handlers, with control-flow/coroutine and process/root-exit/cross-script lifecycle dispatch now
cross-script lifecycle dispatch next without replacing the proven dispatcher or changing public types, commands, isolated as well, extract scalar arithmetic and string-value dispatch next without replacing the proven
and generated output. 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,142 @@
using Age.Engine.Diagnostics;
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepScriptLifecycle(string label, IReadOnlyList<Operand> a, int pc)
{
switch (label)
{
case "throw-exit-request":
if (_o.IgnoreExitRequests) return pc + 1;
// Native op 0x1 throws Command_Exit_Exception through callbacks and nested script
// frames. The outer engine loop catches it and exits without advancing frame_pc.
throw new ProcessExitRequestedException();
case "exit": return FRAME_RETURN;
case "exit-script":
// Native op 0x9 clears the process-initial flag, disposes every active script frame,
// resets scene-owned services, and loads raw script resource 0 as the new root.
_initialRootRun = false;
return ROOT_RELOAD;
case "call-script":
{
long id = a.Count > 0 ? Read(a[0]) : 0;
CallScriptDispatches++;
if (_provider == null)
{
_sink.Emit(TraceEvent.CallScript(id, null)); // stub mode: notify only, no child pushed
return pc + 1;
}
if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; }
var child = _provider.GetById(id);
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
if (child == null) { HaltReason ??= $"callscript-unresolved:0x{id:x}"; return HALT; }
var entry = child.IndexByOffset.TryGetValue(0, out var ci) ? ci : 0;
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
if (outcome == FrameOutcome.Halted) return HALT; // propagate whole-VM halt up
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD; // discard every caller frame
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
return pc + 1; // Returned / RanOff: resume caller
}
case "u00415FB0":
case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero
{
if (_provider == null) return pc + 1;
// Native first scans every mounted selector into its launch queue, then dispatches
// those packed scripts serially. Snapshot before running any child so script-side
// effects cannot change the current batch.
int[] selectors = _provider.MountedAppendSelectors
.Where(selector => selector is > 0 and <= 0xff)
.Distinct()
.Order()
.ToArray();
foreach (int selector in selectors)
{
if (_depth >= _o.CallDepthCap)
{
HaltReason ??= "call-depth-exceeded";
return HALT;
}
long id = (long)selector << 24;
CallScriptDispatches++;
var child = _provider.GetById(id);
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
if (child == null)
{
HaltReason ??= $"append-autorun-unresolved:0x{id:x}";
return HALT;
}
int entry = child.IndexByOffset.TryGetValue(0, out int childEntry) ? childEntry : 0;
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
if (outcome == FrameOutcome.Halted) return HALT;
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
}
return pc + 1;
}
case "u00417E80":
case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39
{
long id = Read(a[0]);
int slot = unchecked((int)Read(a[1]));
if ((uint)slot >= 40)
{
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
return HALT;
}
if (_provider == null)
{
HaltReason ??= $"preloaded-script-provider-unavailable:0x{id:x}";
return HALT;
}
var script = _provider.GetById(id);
if (script == null)
{
HaltReason ??= $"preloaded-script-unresolved:0x{id:x}";
return HALT;
}
int entry = script.IndexByOffset.TryGetValue(0, out int loadedEntry) ? loadedEntry : 0;
_preloadedScriptSlots[slot] = new PreloadedScriptSlot(id, new ExecFrame(script, entry));
return pc + 1;
}
case "u00417FC0":
case "call-preloaded-script-slot": // 0x08 (frame_slot)
{
int slot = unchecked((int)Read(a[0]));
if ((uint)slot >= 40)
{
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
return HALT;
}
if (!_preloadedScriptSlots.TryGetValue(slot, out var loaded))
{
HaltReason ??= $"preloaded-script-slot-empty:{slot}";
return HALT;
}
if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; }
CallScriptDispatches++;
_sink.Emit(TraceEvent.CallScript(loaded.ScriptId, loaded.Frame.Script.Name));
// PC restarts at codebase while the native slot's local banks remain allocated.
// Balanced local calls leave this empty; clearing the port-only emission guard makes
// each invocation an independent diagnostic activation.
loaded.Frame.CallStack.Clear();
loaded.Frame.EmitSeen.Clear();
loaded.Frame.Pc = loaded.Frame.Script.IndexByOffset.TryGetValue(0, out int loadedEntry)
? loadedEntry : 0;
var outcome = RunFrame(loaded.Frame, FrameCause.CallScript, loaded.ScriptId);
if (outcome == FrameOutcome.Halted) return HALT;
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
return pc + 1;
}
default:
throw new InvalidOperationException($"Non-script-lifecycle opcode routed to script-lifecycle handler: {label}");
}
}
}

View File

@@ -1361,131 +1361,16 @@ public sealed partial class VirtualMachine
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); return StepControlFlow(label, ins, pc);
case "throw-exit-request": case "throw-exit-request":
if (_o.IgnoreExitRequests) return pc + 1; case "exit":
// Native op 0x1 throws Command_Exit_Exception through callbacks and nested script
// frames. The outer engine loop catches it and exits without advancing frame_pc.
throw new ProcessExitRequestedException();
case "exit": return FRAME_RETURN;
case "exit-script": case "exit-script":
// Native op 0x9 clears the process-initial flag, disposes every active script frame,
// resets scene-owned services, and loads raw script resource 0 as the new root.
_initialRootRun = false;
return ROOT_RELOAD;
case "call-script": case "call-script":
{
long id = a.Count > 0 ? Read(a[0]) : 0;
CallScriptDispatches++;
if (_provider == null)
{
_sink.Emit(TraceEvent.CallScript(id, null)); // stub mode: notify only, no child pushed
return pc + 1;
}
if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; }
var child = _provider.GetById(id);
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
if (child == null) { HaltReason ??= $"callscript-unresolved:0x{id:x}"; return HALT; }
var entry = child.IndexByOffset.TryGetValue(0, out var ci) ? ci : 0;
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
if (outcome == FrameOutcome.Halted) return HALT; // propagate whole-VM halt up
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD; // discard every caller frame
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
return pc + 1; // Returned / RanOff: resume caller
}
case "u00415FB0": case "u00415FB0":
case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero
{
if (_provider == null) return pc + 1;
// Native first scans every mounted selector into its launch queue, then dispatches
// those packed scripts serially. Snapshot before running any child so script-side
// effects cannot change the current batch.
int[] selectors = _provider.MountedAppendSelectors
.Where(selector => selector is > 0 and <= 0xff)
.Distinct()
.Order()
.ToArray();
foreach (int selector in selectors)
{
if (_depth >= _o.CallDepthCap)
{
HaltReason ??= "call-depth-exceeded";
return HALT;
}
long id = (long)selector << 24;
CallScriptDispatches++;
var child = _provider.GetById(id);
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
if (child == null)
{
HaltReason ??= $"append-autorun-unresolved:0x{id:x}";
return HALT;
}
int entry = child.IndexByOffset.TryGetValue(0, out int childEntry) ? childEntry : 0;
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
if (outcome == FrameOutcome.Halted) return HALT;
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
}
return pc + 1;
}
case "u00417E80": case "u00417E80":
case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39 case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39
{
long id = Read(a[0]);
int slot = unchecked((int)Read(a[1]));
if ((uint)slot >= 40)
{
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
return HALT;
}
if (_provider == null)
{
HaltReason ??= $"preloaded-script-provider-unavailable:0x{id:x}";
return HALT;
}
var script = _provider.GetById(id);
if (script == null)
{
HaltReason ??= $"preloaded-script-unresolved:0x{id:x}";
return HALT;
}
int entry = script.IndexByOffset.TryGetValue(0, out int loadedEntry) ? loadedEntry : 0;
_preloadedScriptSlots[slot] = new PreloadedScriptSlot(id, new ExecFrame(script, entry));
return pc + 1;
}
case "u00417FC0": case "u00417FC0":
case "call-preloaded-script-slot": // 0x08 (frame_slot) case "call-preloaded-script-slot": // 0x08 (frame_slot)
{ return StepScriptLifecycle(label, a, pc);
int slot = unchecked((int)Read(a[0]));
if ((uint)slot >= 40)
{
HaltReason ??= $"preloaded-script-slot-out-of-range:{slot}";
return HALT;
}
if (!_preloadedScriptSlots.TryGetValue(slot, out var loaded))
{
HaltReason ??= $"preloaded-script-slot-empty:{slot}";
return HALT;
}
if (_depth >= _o.CallDepthCap) { HaltReason ??= "call-depth-exceeded"; return HALT; }
CallScriptDispatches++;
_sink.Emit(TraceEvent.CallScript(loaded.ScriptId, loaded.Frame.Script.Name));
// PC restarts at codebase while the native slot's local banks remain allocated.
// Balanced local calls leave this empty; clearing the port-only emission guard makes
// each invocation an independent diagnostic activation.
loaded.Frame.CallStack.Clear();
loaded.Frame.EmitSeen.Clear();
loaded.Frame.Pc = loaded.Frame.Script.IndexByOffset.TryGetValue(0, out int loadedEntry)
? loadedEntry : 0;
var outcome = RunFrame(loaded.Frame, FrameCause.CallScript, loaded.ScriptId);
if (outcome == FrameOutcome.Halted) return HALT;
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
return pc + 1;
}
case "show-text": case "show-text":
case "define-adv-text-layout": case "define-adv-text-layout":
case "reset-adv-text-layout": case "reset-adv-text-layout":