From 889b7e533696696cbb468450302f9b6ff4998090 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Sun, 2 Aug 2026 23:34:22 -0400 Subject: [PATCH] Extract VM movie opcode handler --- docs/PROJECT-STRUCTURE.md | 4 +- docs/remake-architecture-and-roadmap.md | 12 ++- engine/Age.Engine/Vm/VirtualMachine.Movie.cs | 100 +++++++++++++++++++ engine/Age.Engine/Vm/VirtualMachine.cs | 95 +++--------------- 4 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 engine/Age.Engine/Vm/VirtualMachine.Movie.cs diff --git a/docs/PROJECT-STRUCTURE.md b/docs/PROJECT-STRUCTURE.md index f9552a2..75fd983 100644 --- a/docs/PROJECT-STRUCTURE.md +++ b/docs/PROJECT-STRUCTURE.md @@ -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 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 -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-.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 bd6d2bf..273d278 100644 --- a/docs/remake-architecture-and-roadmap.md +++ b/docs/remake-architecture-and-roadmap.md @@ -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 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 deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after each domain move. @@ -1001,8 +1007,8 @@ layer's rendering diverges from ADV; save layout. ## 8. Immediate next step 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` -domains isolated and the first `VirtualMachine.Step` family routed through a domain handler, extract the movie -opcode family next without replacing the proven dispatcher or changing public types, commands, and generated -output. +domains isolated and the first two `VirtualMachine.Step` families routed through domain handlers, extract the +surface/texture opcode family 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.Movie.cs b/engine/Age.Engine/Vm/VirtualMachine.Movie.cs new file mode 100644 index 0000000..d18dbc4 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.Movie.cs @@ -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}"); + } + } +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 17b2ed5..5b1f961 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -2567,59 +2567,12 @@ public sealed partial class VirtualMachine case "get-initial-root-run": // 0x130 (out) Write(a[0], _initialRootRun ? 1 : 0); return pc + 1; - 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 "play-modal-movie-to-surface": + case "u004221A0": + case "play-movie-to-surface": + case "u00422B80": + case "play-movie-to-surface-at-position": + return StepMovie(label, ins, pc); case "query-gfx-object?": // 0x215 (out)(handle) -> slot | -1 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 return pc + 1; } - 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 "u00422930": + case "query-surface-stop-time-ms": + case "query-movie-surface-active": + return StepMovie(label, ins, pc); case "sample-frame-time": // 0x23c: previous <- current; current <- monotonic time Gfx.SampleFrameTime(_host.InputClockMilliseconds); return pc + 1; case "set-gfx-geom3-c": // 0x1ff: set current translation matrix @@ -2772,14 +2707,8 @@ public sealed partial class VirtualMachine Gfx.ResetAnimClock(); return pc + 1; case "set-gfx-animation-service-flags": // 0x24e: bit 1 suppresses op 0x243 Gfx.SetAnimationServiceFlags(Read(a[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; + case "play-movie-mask-transition": + return StepMovie(label, ins, pc); 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]), Read(a[4]), (int)Read(a[5]), Read(a[6]), Read(a[7])); return pc + 1;