Fix MPEG movie startup cadence
This commit is contained in:
@@ -434,6 +434,10 @@ depend on the movie service itself to park until EOF/input cancellation before t
|
||||
signature gate. `IHost.PlayModalMovieToSurface` is distinct from the non-modal call for lifecycle only: Godot
|
||||
reuses the asynchronous FFmpeg decoder and retained compositor but parks the VM thread until EOF
|
||||
or mouse/Accept/Cancel input. The wrapper's following release then tears down the completed/cancelled movie.
|
||||
Native attaches the movie renderer to the already-created target without replacing that surface's resource
|
||||
identity or color key. The port now does the same: `MovieSurfaceRegistry` resolves the live instance pixels while
|
||||
the VM retains the mutable surface's `resourceId=0, colorKey=-1`. The former synthetic `colorKey=0` binding made
|
||||
exact-black MPEG pixels transparent and was not part of the native asset-resolution contract.
|
||||
The FFmpeg backend now renders embedded audio through an engine-owned synchronized path rather than an
|
||||
unmanaged default-device side path. Timestamped stereo float PCM feeds a per-playback Godot
|
||||
`AudioStreamGenerator`; the sound-hardware position drives video presentation, and modal cancellation or
|
||||
|
||||
@@ -366,6 +366,38 @@ catalog opener as `0x20f`. Both paths now decode the MPEG audio pin through FFmp
|
||||
float PCM. Godot owns one `AudioStreamGenerator` per playback instance, applies the native flag-selected
|
||||
mute/music/SE/voice or default movie route, and uses the sound-hardware position as the presentation clock.
|
||||
|
||||
**Native first-frame preroll and stream origin (2026-07-28):** AGE does not seek or manually decode an
|
||||
opening frame. `movie_to_texture_open_asset_graph@0x463e20` finishes with the DirectShow graph stopped.
|
||||
`movie_start_modal_playback@0x463280` then calls `IMediaControl::Run` at vtable slot `+0x1c`; there is no
|
||||
preceding `Pause` or `IMediaPosition` mutation in `0x20f`. DirectShow's stopped-to-running transition
|
||||
necessarily passes through paused/preroll. For a file source, the graph manager waits for each renderer to
|
||||
receive a sample; a video renderer displays that held first sample as a poster image before the graph enters
|
||||
running state. See Microsoft's [Filter States](https://learn.microsoft.com/en-us/windows/win32/directshow/filter-states)
|
||||
contract.
|
||||
|
||||
The custom `movie_texture_renderer_receive_sample@0x4628d0` copies that preroll sample into the D3D texture
|
||||
and sets renderer `+0x5b4`. `movie_consume_renderer_new_frame_flag@0x405120` clears the flag and makes the
|
||||
outer engine tick render the retained surface. Audio is cued during the same graph transition but begins only
|
||||
with the running reference clock. Native observation adds the decisive cadence constraint: OP's image advances
|
||||
immediately with its audio; it does not hold the preroll image for roughly 600 ms. AGE contains no later
|
||||
timestamp correction, so the DirectShow splitter/graph is necessarily presenting the video stream relative to
|
||||
its first sample rather than exposing FFmpeg's shared program-stream timestamp origin. This last mapping is an
|
||||
inference about DirectShow internals, but the visible native behavior is the parity oracle.
|
||||
|
||||
The installed FFmpeg probe rules out dropped or transparent source frames. LOGO and OP first report video PTS
|
||||
600/601 ms on the shim's shared mux timeline, then deliver every tested opening frame at consecutive 33/34 ms
|
||||
steps; pixels change within the first 20 frames. Every decoded alpha byte in those frames is 255. MPEG-1 has no
|
||||
alpha plane. Native `movie_texture_renderer_set_media_type@0x463750` accepts RGB24/RGB32, and the RGB24 copy
|
||||
path promotes pixels to alpha `0xff` before comparing the complete packed ARGB value with its renderer key.
|
||||
Opcode `0x20f` attaches that renderer to the existing D3D target and never reloads the retained surface or
|
||||
assigns it an RGB color key. The former port did both `Gfx.SetSurface(..., colorKey: 0)` and RGB-only keying,
|
||||
which incorrectly made exact-black movie texels transparent and explained grey-background MPEG speckles.
|
||||
|
||||
The corrected port publishes frame zero before starting audio, then schedules all video frames from
|
||||
`source_video_pts - first_video_pts`; audio retains its own complete stream from timestamp zero. No video or
|
||||
audio samples are discarded. Signal-bearing PCM in OP's first 600 ms proves only that the audio data is real,
|
||||
not that the MPEG mux's cross-stream start offset is an intended 600 ms visible lead.
|
||||
|
||||
An existing native operand trace identifies every observed heap codebase by a 100% match against its static
|
||||
instruction-offset set. The captured New Game route is:
|
||||
|
||||
@@ -1293,18 +1325,22 @@ their later lifecycle/presentation slices.
|
||||
|
||||
### 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
|
||||
The exact ABI is `play-movie-to-surface(resource_id, surface_slot, movie_flags, start_delay_ms)`. Handler
|
||||
`op_0x236_play_movie_to_surface@0x423ee0` records a 9-dword instruction length 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
|
||||
operand 1 through `asset_open_indexed_entry`, constructs a stopped DirectShow FilterGraph, and configures its
|
||||
pending start. 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
|
||||
mute/music/SE/voice routing; without an override, native setting `set:DependMovieSound` supplies the normal
|
||||
movie route. The port applies those categories through Godot audio buses. 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.
|
||||
movie route. The port applies those categories through Godot audio buses. Operand 4 is stored at movie object
|
||||
`+0x42c` as a millisecond start delay: `movie_start_pending_after_sync_delay` records `timeGetTime()` on the
|
||||
first service tick and starts the graph once `current - start >= operand4`. It is not a device mask or movie
|
||||
duration. Replacing or releasing the owning surface stops the graph and detaches the renderer. The port still
|
||||
threads this value through a legacy `syncMask`-named host parameter but does not yet delay FFmpeg start for
|
||||
nonzero values. The corpus has 25 sites: 24 pass literal zero, while BTL supplies a dynamic local-pointer value.
|
||||
|
||||
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
|
||||
@@ -1314,6 +1350,11 @@ not itself block the VM. SC0000 prepares additional static layers, then reaches
|
||||
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.
|
||||
|
||||
Non-modal start is deferred through `movie_play_configure@0x4625e0`: the outer tick's
|
||||
`movie_start_pending_after_sync_delay@0x4633b0` records the first service time and calls the same shared
|
||||
`IMediaControl::Run` worker once operand 4's sync delay has elapsed. A zero mask starts on the first service
|
||||
tick. It therefore receives the same stopped-to-paused preroll and first-frame poster behavior as modal `0x20f`.
|
||||
|
||||
**Manual-test corrections (2026-07-11):** the initial port incorrectly treated pre-yield static loads as
|
||||
surface replacement, producing start/first-frame/stop all in render frame 0. The bounded host now retains
|
||||
the movie through `0x21c` until DirectShow completion. A follow-up compositor trace proved the movie object
|
||||
|
||||
@@ -744,9 +744,9 @@ This is a target-pixel operation, not retained-object teardown. It invokes IDire
|
||||
### 0x20f `play-modal-movie-to-surface` (play-modal-movie-to-surface, argc 3)
|
||||
- **summary:** (packed_resource_id)(surface_slot)(movie_flags) - open a universal packed SYS4INI/AAI MPEG asset into an existing retained surface, start its native movie graph, and arm modal run-state bit 0x2000 so script execution remains parked until the movie completes or is skipped. This is the LOGO/OP/ED whole-movie path; op 0x236 uses the same resolver with non-modal lifecycle.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x20f_play_modal_movie_to_surface@0x422e50 shares the movie-object allocation, DirectShow graph open, audio-route, and volume setup used by 0x236, then calls movie_start_modal_playback@0x463280, ORs EngineCtx+0xa0ce4 with 0x2000, and marks movie presentation dirty. The main loop and window procedure special-case run-state 0x2000. Corpus has exactly three sites: LOGO (0x335f,42,4), OP (0x3364,42,4), and ED (0x3324,42,dynamic flags). Those ids are universal raw SYS4INI indexes for MPEG-pack LOGO.AGF, OP.AGF, and ED.AGF; each script releases its surface only after 0x20f resumes.
|
||||
- **evidence:** Ghidra /v2: op_0x20f_play_modal_movie_to_surface@0x422e50 requires an existing retained D3D target, shares the movie-object allocation, DirectShow graph open, audio-route, and volume setup used by 0x236, then calls movie_start_modal_playback@0x463280, ORs EngineCtx+0xa0ce4 with 0x2000, and marks movie presentation dirty. It does not reload/rebind the gfx surface record or assign an RGB color key. The start worker calls IMediaControl::Run at vtable +0x1c on the freshly stopped graph; AGE performs no explicit Pause, seek, or opening-frame decode. DirectShow's stopped-to-paused-to-running transition cues the first sample. movie_texture_renderer_receive_sample@0x4628d0 copies it to the D3D texture and sets renderer +0x5b4; movie_consume_renderer_new_frame_flag@0x405120 makes the outer tick render it. Native OP then advances video immediately with audio, so FFmpeg's roughly 600 ms shared-mux video origin is not an AGE-visible hold. Corpus has exactly three sites: LOGO (0x335f,42,4), OP (0x3364,42,4), and ED (0x3324,42,dynamic flags).
|
||||
|
||||
Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same native universal packed-id catalog contract as 0x236; the separate host call exists for modal wait/cancel lifecycle, not a different resolver. ResourceMap.ResolveMovie selects through ResolvePacked and retains MPEG signature validation in ReadMovie. Godot reuses the asynchronous FFmpeg decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases both video and per-playback audio output. Audio-bearing movies deliver timestamped stereo float PCM through AudioStreamGenerator and use the sound-hardware position as the video/master clock; video-only movies retain monotonic-clock pacing.
|
||||
Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same native universal packed-id catalog contract as 0x236; the separate host call exists for modal wait/cancel lifecycle, not a different resolver. ResourceMap.ResolveMovie selects through ResolvePacked and retains MPEG signature validation in ReadMovie. Godot reuses the asynchronous FFmpeg decoder/retained-surface compositor, parks only the VM thread until EOF, and treats mouse click or Accept/Cancel input as completion before wrapper cleanup releases both video and per-playback audio output. Movie playback leaves the existing mutable target at resourceId 0 with no RGB color key; MovieSurfaceRegistry supplies its live pixels. Audio-bearing movies deliver timestamped stereo float PCM through AudioStreamGenerator and use the sound-hardware position as the master clock. The first decoded image is latched before audio starts, and all video deadlines are rebased to the first video PTS while the complete audio stream begins at its own timestamp zero. No opening samples are discarded.
|
||||
|
||||
### 0x212 `set-gfx-field64` (set-gfx-field64, argc 2)
|
||||
- **summary:** 0x212 (obj_idx)(val) — handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. The generic instruction length is 5 dwords. See docs/engine-re.md gfx op-contract table.
|
||||
@@ -872,11 +872,11 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat
|
||||
- **evidence:** Ghidra handler 0x423da0 converts axis ints to floats -> worker 0x47f060. gfx_object_anim_interpolate@0x473ed0 consumes obj+0x228/+0x214/+0x244 on retained-gfx owner+0xb550 (EngineCtx+0x51b64) 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:** (packed_resource_id)(surface_slot)(movie_flags)(sync_mask) - synchronously open a universal packed SYS4INI/AAI 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 0x33 and BTL's 0x2axx/0x2bxx MVB ids are already absolute base-catalog indexes.
|
||||
- **summary:** (packed_resource_id)(surface_slot)(movie_flags)(start_delay_ms) - synchronously open a universal packed SYS4INI/AAI movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface after the requested service-tick delay. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's 0x33 and BTL's 0x2axx/0x2bxx MVB ids are already absolute base-catalog indexes.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2 op_0x236_play_movie_to_surface@0x423ee0 fetches operand 1 and passes it unchanged to movie_to_texture_open_asset_graph@0x463e20, which passes it unchanged to asset_open_indexed_entry@0x44f390. The opener directly indexes the flat base table or selected AAI table and has no scene input. SC0000 native operand capture and exact 0x13c8->0x13d1 trace prove nonblocking behavior. BTL's live 0x2b21 site supplies 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde, the exact base entries MVB001/MVB004/MVB914/MVB958/MVB955.
|
||||
- **evidence:** Ghidra /v2 op_0x236_play_movie_to_surface@0x423ee0 fetches operand 1 and passes it unchanged to movie_to_texture_open_asset_graph@0x463e20, which passes it unchanged to asset_open_indexed_entry@0x44f390. The opener directly indexes the flat base table or selected AAI table and has no scene input. The handler leaves playback pending through movie_play_configure@0x4625e0; movie_start_pending_after_sync_delay@0x4633b0 records the first service tick and calls the shared IMediaControl::Run worker after operand 4's delay, giving non-modal playback the same DirectShow first-sample preroll as 0x20f. SC0000 native operand capture and exact 0x13c8->0x13d1 trace prove nonblocking behavior. BTL's live 0x2b21 site supplies 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde, the exact base entries MVB001/MVB004/MVB914/MVB958/MVB955.
|
||||
|
||||
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 universal packed-id 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 mute/music/SE/voice routes, otherwise set:DependMovieSound selects the normal movie route; SC0000's low value 2 is retained as native movie mode state. Godot maps those categories to audio buses and sends FFmpeg-decoded timestamped stereo float PCM through a per-playback AudioStreamGenerator. Audio-bearing movies use the sound-hardware position as the master presentation clock; video-only movies retain monotonic pacing. Operand 4 is the movie sync/device mask. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches both decoder and audio output. The port type-checks the selected ResolvePacked record as MPEG without adding a scene base or fallback.
|
||||
The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface without replacing its resource identity/color key, opens operand 1 through the native universal packed-id 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 mute/music/SE/voice routes, otherwise set:DependMovieSound selects the normal movie route; SC0000's low value 2 is retained as native movie mode state. Godot maps those categories to audio buses and sends FFmpeg-decoded timestamped stereo float PCM through a per-playback AudioStreamGenerator. The first image is latched before audio start; video cadence is rebased to its first decoded PTS for both audio-bearing and video-only streams, while audio retains every sample from its own timestamp zero. Operand 4 delays the pending graph start in native service ticks. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches both decoder and audio output. The port type-checks the selected ResolvePacked record as MPEG without adding a scene base or fallback.
|
||||
|
||||
### 0x238 `set-anim-clock` (set-anim-clock, argc 1)
|
||||
- **summary:** (duration) — configure the separate global finite-animation service window: native ctx+0x51b78=0 (start), +0x51b7c=duration. The generic instruction length is 3 dwords. NON-BLOCKING: gfx_animation_service_poll seeds the start from the shared frame timestamp and keeps redraw active through the duration. This is not the time source or cadence for cyclic object channels, which sample ctx+0x51b64/+0x51b68 with object-local starts and periods. SC0000 opening @0x123bd/@0x13858. Handler 0x4240e0; Kelebek VA 0x422390 is drift.
|
||||
|
||||
@@ -133,6 +133,28 @@ the first timestamp and material later gaps/overlaps. A 4,093-block OP regressio
|
||||
The clean audible recheck passed for LOGO/OP and CHAPTER. DirectShow and the managed Windows platform annotations
|
||||
were then deleted; missing, distorted, or unsynchronized audio on any of the 29 audio-bearing assets is now a bug.
|
||||
|
||||
Movie startup was corrected on 2026-07-28 after a first-frame latch exposed a second, visible 600 ms freeze.
|
||||
`LOGO.AGF` and `OP.AGF` begin audio at 0 ms on the shim's shared mux timeline, while their first decoded video
|
||||
frames carry PTS 600 ms and 601 ms. Signal-bearing PCM in OP's first 600 ms initially looked like an intentional
|
||||
audio lead, but native OP advances video and audio immediately. A real-asset regression proves the decoder is not
|
||||
losing opening data: the first 20 LOGO/OP video frames are consecutive 33/34 ms samples, change within that window,
|
||||
and contain alpha 255 at every pixel. The mux offset is therefore not a presentation deadline.
|
||||
|
||||
The paced decoder still latches frame zero and Godot still waits to start its `AudioStreamGenerator` until that
|
||||
image has been published. It now schedules every later video frame from
|
||||
`source_video_pts - first_video_pts`, including audio-bearing movies, while submitting the complete audio stream
|
||||
from its own timestamp zero. This preserves every decoded sample and immediately advances frame cadence instead
|
||||
of freezing frame zero until the audio clock reaches the program-stream offset. Startup logs and F6 snapshots
|
||||
retain the raw first video PTS for diagnosis.
|
||||
|
||||
A separate compositor error amplified the symptom. MPEG-1 has no transparency and FFmpeg's RGBA conversion
|
||||
produces opaque pixels, but the VM had replaced the existing mutable movie target with an invented RGB color key
|
||||
of zero. Exact black became transparent and exposed the Godot clear color, producing the previously reported grey
|
||||
MPEG speckles. Both movie opcodes now retain the existing surface's no-key state; the host's instance binding
|
||||
supplies changing movie pixels without changing surface resource identity. Native `0x20f` likewise attaches its
|
||||
DirectShow renderer to the existing D3D target without assigning an RGB key. The annotated native details and the
|
||||
DirectShow preroll/stream-origin inference are recorded in `docs/engine-re.md`.
|
||||
|
||||
Native deliverables are RID-specific and bundled with the Godot export; the runtime must not discover an
|
||||
arbitrary system FFmpeg. The first implementation gate is Windows x64 because that is the current runnable
|
||||
target, but the C ABI and loader paths must reserve Windows x64, Linux x64, macOS x64, and macOS arm64 from the
|
||||
|
||||
Reference in New Issue
Block a user