Implement SC0000 movie playback lifecycle
This commit is contained in:
@@ -133,7 +133,8 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). Scene-local SFX WAV entries use
|
||||
the same byte path while retaining the existing channel lifecycle. See `docs/phase-a-slice-plan.md`.
|
||||
Diagnostic: `Age.Cli audio <SCENE>`.
|
||||
5. **Movies** (`OP`/`MVB`, MPEG) — a separate video-playback path; deferred.
|
||||
5. **Movies** (`OP`/`MVB`, MPEG) — SC0000 `0x236` now resolves its scene-local id through the same catalog
|
||||
and reads owned payload bytes through `IAssetStore`; see the movie section below.
|
||||
|
||||
## Validation reality (why this is the big haul)
|
||||
|
||||
@@ -177,7 +178,7 @@ behavior:
|
||||
Native evidence already proves this ordering for scripts: `resource_open_by_raw_id@0x44f390` indexes the
|
||||
80-byte SYS4 record and calls `CreateFileA(record.name)` before opening `record.archive`, seeking to
|
||||
`record.offset`, and reading `record.size`. The same service is the correct common seam for scripts,
|
||||
graphics, voice/SFX, and later movie bytes. Resolution and opening must remain separate: scene-local ids and
|
||||
graphics, voice/SFX, and movie bytes. Resolution and opening must remain separate: scene-local ids and
|
||||
universal `raw_index` ids select a record differently, but both records flow through the same loose-first
|
||||
store.
|
||||
|
||||
@@ -256,8 +257,34 @@ Audio byte migration passes its bounded gate in `Sys4AssetStoreTests`: archive-o
|
||||
`BGM005.OGG` and `MAN999.OGG` as Ogg streams and `E0808.WAV` as RIFF/WAVE without consulting `extracted/`.
|
||||
A windowed SC0000 run with `extracted/` moved aside crossed both voice sites and the first SFX sequence,
|
||||
recording BGM005 plus `E0808.WAV` load/start/preload on channels 0/0/4 with no Godot OGG/WAV decode errors.
|
||||
Channel, loop, interruption, timing, fade, load/start, and release behavior is unchanged. Movie `0x236`
|
||||
remains unimplemented.
|
||||
Channel, loop, interruption, timing, fade, load/start, and release behavior is unchanged.
|
||||
|
||||
### SC0000 movie payload and presentation (2026-07-11)
|
||||
|
||||
The only implemented site is SC0000 `0x236@0x13c8`. Scene-local resource `0x33` resolves through the
|
||||
authoritative catalog to `DATA1.ALF:CHAPTER.AGF` (archive offset 3,908,816; size 8,194,052). Despite the
|
||||
`.AGF` name, its payload begins with MPEG program-stream pack start code `00 00 01 BA`; the installed asset
|
||||
is MPEG-1 program stream video at 800x600, 29.97 fps, approximately 11.98 seconds, with video stream `E0`
|
||||
and audio stream `C0`.
|
||||
|
||||
`ResourceMap.ReadMovie` accepts the resolved `AssetEntry`, reads it through the injected `IAssetStore`, and
|
||||
rejects a non-MPEG-pack payload. Godot has no built-in MPEG decoder suitable for this asset, so the
|
||||
Windows backend adapts those already-owned VFS bytes to the system DirectShow MPEG source using a private
|
||||
temporary file. DirectShow decodes RGB32 on an MTA thread; the sample-grabber callback converts bottom-up
|
||||
BGRA to top-down RGBA and publishes only the newest frame to the retained compositor surface. The temporary
|
||||
file is an implementation adapter, never an alternate resolver or dependency on `extracted/`. The audio pin
|
||||
is intentionally left unrendered for this bounded slice.
|
||||
|
||||
Archive-only tests verify the exact catalog entry, payload size/header, and an actual decoded 800x600 RGBA
|
||||
frame; both still pass with the entire `extracted/` tree physically moved aside and restored afterward.
|
||||
SC0000's native capture verifies operands `(0x33, 0, 2, 0)` and immediate VM continuation at `0x13d1`.
|
||||
In Godot the real site opens the same 8,194,052 VFS bytes, publishes changing 800x600 frames, and retains
|
||||
them across pre-yield static surface preparation. The later `0x21c` presentation service remains parked
|
||||
until DirectShow EOF, then scene cleanup stops the movie. Manual interactive validation still does not show
|
||||
the movie despite those successful lifecycle logs; visible compositor/layer publication is the next bounded
|
||||
investigation. The real-scene trace remains a separate extracted-present test
|
||||
because the current `Paths.Scripts()` test bootstrap still locates its root `*.BIN` fixtures there; migrating
|
||||
that test/bootstrap path is unrelated to movie asset loading and was not folded into this slice.
|
||||
|
||||
VFS-B passes its bounded gates in `Sys4AssetStoreTests`: the installed AAI expands from the LZSS stream at
|
||||
`0x118` (expanded size at `0x110`, packed size at `0x114`) to one `APPEND01.ALF` archive and 81 80-byte
|
||||
@@ -270,7 +297,8 @@ through `0x01xxxxxx`; direct base-name lookup deliberately does not see append r
|
||||
### Deliberate non-goals
|
||||
|
||||
- Writing/repacking ALF or AAI; loose overrides already provide the native mod/translation workflow.
|
||||
- AGF encoding or movie/video decoding for MPEG-like `OP/MVB*.AGF`.
|
||||
- AGF encoding, movie audio, or generalized video APIs. The implemented movie path is deliberately limited
|
||||
to SC0000's existing `0x236` site and Windows' native DirectShow MPEG decoder.
|
||||
- A generalized multi-mod dependency manager. Start with native game-root loose overrides; configurable
|
||||
ordered mod roots can be layered onto the same store later.
|
||||
- Removing the extraction/conversion tools immediately. They remain independent parity oracles until the
|
||||
|
||||
@@ -845,9 +845,44 @@ while `0x21c` is parked completes only the active foreground surface transition
|
||||
not pre-arm the following `wait-for-input`, and it does not complete independent retained rotation,
|
||||
matrix, spritesheet, or color-animation channels.
|
||||
|
||||
**Still deferred:** `0x236` (`gfx_op_0x236` @`0x423ee0`) a
|
||||
**timed/animated-surface (movie-like) op**; plus the unclassified `0x242/0x23d/0x20a/0x20e`
|
||||
tail (2-arg flags / inline). These stay GAP until a follow-up slice or are safe-noop'd if the opening tolerates it.
|
||||
**Resolved 2026-07-11:** `0x236` is the movie-to-retained-surface path described below. The unclassified
|
||||
`0x242/0x23d/0x20a/0x20e` tail (2-arg flags / inline) remains GAP and outside this slice.
|
||||
|
||||
### Movie-to-surface opcode `0x236` (2026-07-11)
|
||||
|
||||
The exact ABI is `play-movie-to-surface(resource_id, surface_slot, movie_flags, sync_mask)`. Handler
|
||||
`op_0x236_play_movie_to_surface@0x423ee0` is command type 9 and requires the destination texture to exist.
|
||||
It allocates/reuses a 0x478-byte `CMovieToTexture` object, binds the D3D device/backing texture, opens
|
||||
operand 1 through `asset_open_indexed_entry`, constructs a DirectShow FilterGraph, and starts it. The graph
|
||||
queries `IGraphBuilder`, `IMediaControl`, `IMediaPosition`, `IMediaEvent`, and `IBasicAudio`; its custom
|
||||
`CMovieTextureRenderer` accepts RGB samples and copies the bottom-up frame into the retained texture.
|
||||
|
||||
Operand 3 is retained movie mode plus sound-route policy. Bits `0x10000/0x20000/0x40000/0x80000` force
|
||||
sound route 0/1/2/3; without an override, native setting `set:DependMovieSound` supplies the route. This
|
||||
slice deliberately does not implement the audio branch. Operand 4 is stored at movie object `+0x42c` as
|
||||
the sync/device mask; it is not a duration or loop count. Replacing or releasing the owning surface stops
|
||||
the graph and detaches the renderer.
|
||||
|
||||
Graph construction/open is synchronous, but playback and sample delivery are asynchronous. The handler
|
||||
returns normally and the interpreter advances one instruction: at SC0000 `0x13c8`, the native operand
|
||||
capture evaluates `(0x33, 0, 2, 0)` and the next executed bytecode is `0x13d1`. The movie therefore does
|
||||
not itself block the VM. SC0000 prepares additional static layers, then reaches `0x21c` through
|
||||
`label_1235a@0x1574`. That opcode sets run-state bit `0x400` and yields the interpreter; the presentation
|
||||
service continues sampling the retained movie until DirectShow EOF, after which the following script
|
||||
cleanup releases it. The static preparation before `0x21c` is not a movie teardown boundary.
|
||||
|
||||
**Manual-test correction (2026-07-11):** the initial port incorrectly treated those pre-yield static loads
|
||||
as surface replacement, producing start/first-frame/stop all in render frame 0. The bounded host now keeps
|
||||
the movie as an independently updating retained layer, parks `0x21c` until the DirectShow completion event,
|
||||
then permits cleanup. A real-render lifecycle run records first frame 98 and stop frame 181. The screenshot
|
||||
sequence's known stale-CG/transition behavior can obscure this layer, so screenshot appearance is not used
|
||||
as the visual oracle. **Manual recheck remains unresolved:** normal interactive SC0000 logs start, changing
|
||||
frame delivery, EOF, and delayed stop, but the movie is not visibly presented in the application. The next
|
||||
slice must diagnose compositor/layer publication rather than reopening decode or VM-lifecycle timing.
|
||||
|
||||
The `/v2` image names/comments the handler; movie ctor/interface/open/play/volume/release workers; sound
|
||||
route helpers; renderer media-type/sample workers; and stop/detach/destructor lifecycle. The image was
|
||||
saved after annotation.
|
||||
|
||||
### Grey-background root cause — slot collision + tint-strength (2026-07-08, gfx-log)
|
||||
|
||||
|
||||
@@ -278,6 +278,13 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on ctx+0xb550 and matrix4_make_axis_angle@0x48b215. gfx_object_composite@0x47f650 calls one-shot transform first, cyclic animation second.
|
||||
|
||||
### 0x236 `play-movie-to-surface` (play-movie-to-surface, argc 4)
|
||||
- **summary:** (resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously resolve/open an archive movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's native site evaluates (0x33, 0, 2, 0) at 0x13c8 and resumes at bytecode 0x13d1; its later 0x21c service boundary yields until movie EOF before cleanup.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Native handler 0x423ee0 and helpers 0x463c50/0x463aa0/0x463e20/0x4625e0; SC0000 native operand capture; exact 0x13c8->0x13d1 trace; archive-only changing-frame decoder and Godot lifecycle tests.
|
||||
|
||||
The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native indexed-asset reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is stored as the movie sync/device mask at object+0x42c; SC0000 passes 0. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it.
|
||||
|
||||
### 0x238 `set-anim-clock` (set-anim-clock, argc 1)
|
||||
- **summary:** (duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. cmd-type 3. NON-BLOCKING: only configures; the render loop advances it and interpolates all animating objects. SC0000 opening @0x123bd/@0x13858. Handler 0x4240e0; Kelebek VA 0x422390 is drift.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -1043,10 +1050,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x236 `u004221A0` (u004221A0, argc 4)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x23a `u00422420` (u00422420, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -1203,3 +1203,36 @@ crossed voice at `0xa0a`/`0xbf6`, BGM005 at `0x7fa`, and the established SFX loa
|
||||
`0xc29`/`0xc2e`/`0xc31` on channels 0/0/4. Godot reported no OGG/WAV read, decode, or deferred-call errors;
|
||||
`extracted/` was restored afterward. Movie `0x236`, AGF work, opcode semantics, and unrelated VM behavior
|
||||
were untouched.
|
||||
|
||||
### Phase A — SC0000 movie opcode `0x236` DONE (2026-07-11)
|
||||
|
||||
Reversed and implemented only SC0000's existing `0x236@0x13c8`. Native ABI is
|
||||
`(resource_id, surface_slot, movie_flags, sync_mask)`; the native site evaluates
|
||||
`(0x33, 0, 2, 0)`. Native graph construction and asset open are synchronous, playback is asynchronous,
|
||||
and the opcode itself does not park the interpreter: execution resumes at bytecode `0x13d1`. SC0000's
|
||||
later `0x21c` presentation boundary sets run-state bit `0x400` and services the retained movie until EOF;
|
||||
only the following cleanup stops and detaches playback.
|
||||
|
||||
Resource `0x33` resolves to the 8,194,052-byte `DATA1.ALF:CHAPTER.AGF` MPEG-1 program stream through the
|
||||
catalog and `IAssetStore`. `ResourceMap.ReadMovie` owns that VFS read. The Godot Windows backend uses the
|
||||
system DirectShow MPEG splitter/decoder, converts its bottom-up RGB32 samples to retained RGBA frames, and
|
||||
presents them on the native slot-0 movie layer. The payload contains an audio stream, but its pin remains unrendered;
|
||||
audio, AGF still-image work, generalized video APIs, other opcodes, and unrelated VM behavior remain out
|
||||
of scope.
|
||||
|
||||
Ghidra `/v2` names/comments the `0x423ee0` handler and the movie ctor, graph-interface/open/play,
|
||||
sound-route/volume, texture-renderer sample/media-type, stop/detach/release/destructor chain; the program is
|
||||
saved. Validation covers the exact real-SC0000 operands and `0x13c8 -> 0x13d1` boundary, archive-only VFS
|
||||
payload identity, actual 800x600 RGBA DirectShow decode, .NET/Godot builds, and the real Godot site opening
|
||||
the same VFS byte count. Manual testing exposed and corrected an initial same-frame teardown: pre-yield
|
||||
static loads no longer destroy the movie, `0x21c` waits for DirectShow completion, and a real-render run
|
||||
records first frame 98 / stop frame 181. The shot-sequence's known stale-CG behavior can cover the movie,
|
||||
so it is not treated as visual proof. **Manual recheck is not visually complete:** the normal application
|
||||
logs start, changing frame delivery, EOF, and delayed stop, but shows no movie. Carry compositor/layer
|
||||
publication forward as the next bounded diagnostic; do not reopen the proven VFS/decode/resume lifecycle.
|
||||
The decoder test
|
||||
asserts two delivered MPEG frames differ. The two archive/decode tests
|
||||
also pass with all of `extracted/` physically moved aside. The real-scene trace is intentionally separate:
|
||||
the existing test bootstrap still finds root script fixtures through `Paths.Scripts()` under `extracted/`,
|
||||
and changing that unrelated bootstrap was outside this movie slice. Final validation: engine **132/132**,
|
||||
Godot build with zero warnings, threaded `SELFTEST OK`, and opcode-map lint clean.
|
||||
|
||||
Reference in New Issue
Block a user