Extract VM movie opcode handler

This commit is contained in:
gamer147
2026-08-02 23:34:22 -04:00
parent 738129faa1
commit ee4a89ca1c
4 changed files with 124 additions and 87 deletions

View File

@@ -175,7 +175,9 @@ accounting, and visible retained-scene snapshots.
`engine/Age.Engine/Vm/VirtualMachine.cs` retains VM lifecycle, cross-domain state, and the proven top-level `engine/Age.Engine/Vm/VirtualMachine.cs` retains VM lifecycle, cross-domain state, and the proven top-level
opcode dispatcher. Its partial-class companion `engine/Age.Engine/Vm/VirtualMachine.Audio.cs` owns VM audio opcode dispatcher. Its partial-class companion `engine/Age.Engine/Vm/VirtualMachine.Audio.cs` owns VM audio
state, BGM restart semantics, and the BGM/voice/SFX/mixer opcode handler; `Step` retains the audio labels and state, BGM restart semantics, and the BGM/voice/SFX/mixer opcode handler; `Step` retains the audio labels and
routes that family into the handler. routes that family into the handler. `engine/Age.Engine/Vm/VirtualMachine.Movie.cs` owns modal/asynchronous/
positioned movie playback, movie surface metadata/activity queries, and movie-mask transition dispatch; `Step`
likewise retains and routes the movie labels.
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

@@ -630,6 +630,12 @@ do not mix mechanical moves with semantic changes.
routes only that family through `StepAudio`; public VM behavior and case-body logic remain unchanged. Runtime routes only that family through `StepAudio`; public VM behavior and case-body logic remain unchanged. Runtime
validation remains green. validation remains green.
The second bounded `VirtualMachine.Step` extraction moved modal, asynchronous, and positioned movie playback,
movie surface stop-time/activity queries, and movie-mask transition case bodies into
`engine/Age.Engine/Vm/VirtualMachine.Movie.cs`. The top-level dispatcher retains all movie labels at their
existing positions and routes them through `StepMovie`; the original instruction remains available for the
two bytecode-offset compatibility paths. 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.
@@ -1001,8 +1007,8 @@ 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 first `VirtualMachine.Step` family routed through a domain handler, extract the movie domains isolated and the first two `VirtualMachine.Step` families routed through domain handlers, extract the
opcode family next without replacing the proven dispatcher or changing public types, commands, and generated surface/texture opcode family next without replacing the proven dispatcher or changing public types, commands,
output. 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,100 @@
using Age.Engine.Hosting;
using Age.Engine.Model;
namespace Age.Engine.Vm;
public sealed partial class VirtualMachine
{
private int StepMovie(string label, Instruction ins, int pc)
{
var a = ins.Args;
switch (label)
{
case "play-modal-movie-to-surface": // 0x20f (packed resource)(surface)(movie flags)
{
long resourceId = Read(a[0]);
int surfaceSlot = (int)Read(a[1]);
// Modal and non-modal paths share packed resolution and retained-surface composition.
// The distinct host entry point owns only 0x20f's blocking lifecycle. Native attaches its
// renderer to the existing mutable target; it does not reload that surface with resource
// id/movie bytes or apply color key 0. Keep the created surface's no-key state so MPEG
// black remains opaque.
_host.PlayModalMovieToSurface(resourceId, surfaceSlot, Read(a[2]));
return pc + 1;
}
case "u004221A0": // pre-reference compatibility
case "play-movie-to-surface": // 0x236 (resource)(surface)(movie flags)(start delay ms)
{
long resourceId = Read(a[0]);
int surfaceSlot = (int)Read(a[1]);
// Native SC0000's warm-engine trace evaluates this existing site as surface 0. The bounded
// single-scene bootstrap assigns its logical layer slot 5, which the immediately following
// 0x34/0x35 static loads reuse and would therefore evict the movie before presentation.
// Reproduce the native site assignment without changing the general surface allocator.
if (ins.Offset == 0x13c8 && _cur.Script.Name.StartsWith("SC0000", StringComparison.OrdinalIgnoreCase)
&& surfaceSlot != 0)
{
int logicalLayer = (int)Globals.GetValueOrDefault(0x62450);
long movieHandle = Globals.GetValueOrDefault(0x62455 + logicalLayer);
Gfx.RemapObjectSurface(movieHandle, surfaceSlot, 0);
surfaceSlot = 0;
}
// Graph construction is synchronous. Keep the existing created surface blank during that
// boundary; publishing the movie resource first would let a concurrent compositor mistake
// the MPEG payload's .AGF name for a still image before the host registers/decodes it.
long? stopTimeMs = _host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3]));
// Native replaces the pixels of the already-created surface without changing its resource
// identity or color key. The host's playback-to-surface binding resolves the live frame.
// Native never encounters a missing system decoder for shipped assets. If a host backend
// cannot initialize one, model the valid movie as completing immediately: BTL feeds this
// value into its effect timeline, where zero is a safe duration and -1 is not meaningful.
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs ?? 0);
return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous
}
case "u00422B80": // pre-reference compatibility
case "play-movie-to-surface-at-position": // 0x241 (+ initial position ms)
{
long resourceId = Read(a[0]);
int surfaceSlot = unchecked((int)Read(a[1]));
long? stopTimeMs = _host.PlayMovieToSurfaceAtPosition(
resourceId, surfaceSlot, Read(a[2]), Read(a[3]), Read(a[4]));
// As with 0x236, seeking changes decoder position but not the graph's stop metadata.
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs ?? 0);
return pc + 1;
}
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----
case "u00422930": // pre-reference compatibility
case "query-surface-stop-time-ms": // 0x23f (out_stop_time_ms)(surface_slot)
{
int surfaceSlot = (int)Read(a[1]);
if (!Gfx.TryGetMovieStopTime(surfaceSlot, out long? stopTimeMs))
{
Write(a[0], -1); // native null CMovieToTexture slot
return pc + 1;
}
if (!stopTimeMs.HasValue)
{
_host.ReportWarning(
$"movie stop-time unavailable {_cur.Script.Name}@0x{ins.Offset:x} " +
$"surface={surfaceSlot}; returning -1");
Write(a[0], -1);
return pc + 1;
}
Write(a[0], stopTimeMs.Value);
return pc + 1;
}
case "query-movie-surface-active": // 0x23a (out)(surface slot)
Write(a[0], _host.IsMovieSurfaceActive((int)Read(a[1])) ? 1 : 0); return pc + 1;
case "play-movie-mask-transition": // 0x24d: captured retained range + movie green-channel mask
_host.PlayMovieMaskTransition(Gfx, new MovieMaskTransitionRequest(
Read(a[0]), unchecked((int)Read(a[1])),
Read(a[2]), unchecked((int)Read(a[3])),
unchecked((int)Read(a[4])), unchecked((int)Read(a[5])),
unchecked((int)Read(a[6])), unchecked((int)Read(a[7])),
Read(a[8]), Read(a[9]), Read(a[10]), Read(a[11])));
return pc + 1;
default:
throw new InvalidOperationException($"Non-movie opcode routed to movie handler: {label}");
}
}
}

View File

@@ -2567,59 +2567,12 @@ public sealed partial class VirtualMachine
case "get-initial-root-run": // 0x130 (out) case "get-initial-root-run": // 0x130 (out)
Write(a[0], _initialRootRun ? 1 : 0); Write(a[0], _initialRootRun ? 1 : 0);
return pc + 1; return pc + 1;
case "play-modal-movie-to-surface": // 0x20f (packed resource)(surface)(movie flags) case "play-modal-movie-to-surface":
{ case "u004221A0":
long resourceId = Read(a[0]); case "play-movie-to-surface":
int surfaceSlot = (int)Read(a[1]); case "u00422B80":
// Modal and non-modal paths share packed resolution and retained-surface composition. case "play-movie-to-surface-at-position":
// The distinct host entry point owns only 0x20f's blocking lifecycle. Native attaches its return StepMovie(label, ins, pc);
// renderer to the existing mutable target; it does not reload that surface with resource
// id/movie bytes or apply color key 0. Keep the created surface's no-key state so MPEG
// black remains opaque.
_host.PlayModalMovieToSurface(resourceId, surfaceSlot, Read(a[2]));
return pc + 1;
}
case "u004221A0": // pre-reference compatibility
case "play-movie-to-surface": // 0x236 (resource)(surface)(movie flags)(start delay ms)
{
long resourceId = Read(a[0]);
int surfaceSlot = (int)Read(a[1]);
// Native SC0000's warm-engine trace evaluates this existing site as surface 0. The bounded
// single-scene bootstrap assigns its logical layer slot 5, which the immediately following
// 0x34/0x35 static loads reuse and would therefore evict the movie before presentation.
// Reproduce the native site assignment without changing the general surface allocator.
if (ins.Offset == 0x13c8 && _cur.Script.Name.StartsWith("SC0000", StringComparison.OrdinalIgnoreCase)
&& surfaceSlot != 0)
{
int logicalLayer = (int)Globals.GetValueOrDefault(0x62450);
long movieHandle = Globals.GetValueOrDefault(0x62455 + logicalLayer);
Gfx.RemapObjectSurface(movieHandle, surfaceSlot, 0);
surfaceSlot = 0;
}
// Graph construction is synchronous. Keep the existing created surface blank during that
// boundary; publishing the movie resource first would let a concurrent compositor mistake
// the MPEG payload's .AGF name for a still image before the host registers/decodes it.
long? stopTimeMs = _host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3]));
// Native replaces the pixels of the already-created surface without changing its resource
// identity or color key. The host's playback-to-surface binding resolves the live frame.
// Native never encounters a missing system decoder for shipped assets. If a host backend
// cannot initialize one, model the valid movie as completing immediately: BTL feeds this
// value into its effect timeline, where zero is a safe duration and -1 is not meaningful.
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs ?? 0);
return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous
}
case "u00422B80": // pre-reference compatibility
case "play-movie-to-surface-at-position": // 0x241 (+ initial position ms)
{
long resourceId = Read(a[0]);
int surfaceSlot = unchecked((int)Read(a[1]));
long? stopTimeMs = _host.PlayMovieToSurfaceAtPosition(
resourceId, surfaceSlot, Read(a[2]), Read(a[3]), Read(a[4]));
// As with 0x236, seeking changes decoder position but not the graph's stop metadata.
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs ?? 0);
return pc + 1;
}
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----
case "query-gfx-object?": // 0x215 (out)(handle) -> slot | -1 case "query-gfx-object?": // 0x215 (out)(handle) -> slot | -1
if (_diagSetTexture) // reuse the flag: show what the slot query returns (grey-BG slot dig) if (_diagSetTexture) // reuse the flag: show what the slot query returns (grey-BG slot dig)
{ {
@@ -2687,28 +2640,10 @@ public sealed partial class VirtualMachine
else Write(a[0], 1); // native missing-object path leaves output operands untouched else Write(a[0], 1); // native missing-object path leaves output operands untouched
return pc + 1; return pc + 1;
} }
case "u00422930": // pre-reference compatibility case "u00422930":
case "query-surface-stop-time-ms": // 0x23f (out_stop_time_ms)(surface_slot) case "query-surface-stop-time-ms":
{ case "query-movie-surface-active":
int surfaceSlot = (int)Read(a[1]); return StepMovie(label, ins, pc);
if (!Gfx.TryGetMovieStopTime(surfaceSlot, out long? stopTimeMs))
{
Write(a[0], -1); // native null CMovieToTexture slot
return pc + 1;
}
if (!stopTimeMs.HasValue)
{
_host.ReportWarning(
$"movie stop-time unavailable {_cur.Script.Name}@0x{ins.Offset:x} " +
$"surface={surfaceSlot}; returning -1");
Write(a[0], -1);
return pc + 1;
}
Write(a[0], stopTimeMs.Value);
return pc + 1;
}
case "query-movie-surface-active": // 0x23a (out)(surface slot)
Write(a[0], _host.IsMovieSurfaceActive((int)Read(a[1])) ? 1 : 0); return pc + 1;
case "sample-frame-time": // 0x23c: previous <- current; current <- monotonic time case "sample-frame-time": // 0x23c: previous <- current; current <- monotonic time
Gfx.SampleFrameTime(_host.InputClockMilliseconds); return pc + 1; Gfx.SampleFrameTime(_host.InputClockMilliseconds); return pc + 1;
case "set-gfx-geom3-c": // 0x1ff: set current translation matrix case "set-gfx-geom3-c": // 0x1ff: set current translation matrix
@@ -2772,14 +2707,8 @@ public sealed partial class VirtualMachine
Gfx.ResetAnimClock(); return pc + 1; Gfx.ResetAnimClock(); return pc + 1;
case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243 case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243
Gfx.SetAnimationServiceFlags(Read(a[0])); return pc + 1; Gfx.SetAnimationServiceFlags(Read(a[0])); return pc + 1;
case "play-movie-mask-transition": // 0x24d: captured retained range + movie green-channel mask case "play-movie-mask-transition":
_host.PlayMovieMaskTransition(Gfx, new MovieMaskTransitionRequest( return StepMovie(label, ins, pc);
Read(a[0]), unchecked((int)Read(a[1])),
Read(a[2]), unchecked((int)Read(a[3])),
unchecked((int)Read(a[4])), unchecked((int)Read(a[5])),
unchecked((int)Read(a[6])), unchecked((int)Read(a[7])),
Read(a[8]), Read(a[9]), Read(a[10]), Read(a[11])));
return pc + 1;
case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges case "queue-surface-alpha-transition": // 0x223: target surface crossfade over two object ranges
Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]), Gfx.QueueSurfaceAlphaTransition(Read(a[0]), (int)Read(a[1]), Read(a[2]), (int)Read(a[3]),
Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1; Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;