Implement movie surface stop-time queries

This commit is contained in:
gamer147
2026-07-21 14:43:24 -04:00
parent b0971f2047
commit fdcfbd9fa6
15 changed files with 330 additions and 60 deletions

View File

@@ -1102,13 +1102,13 @@ annotated in Ghidra, saved.
**In scope (built this slice):**
| op | handler / worker | semantics |
|---|---|---|
| `0x22f` | `gfx_op_0x22f_set_position_anim` → `gfx_worker_set_translation` | set object **position** (translation vec `obj+0x5d4`); base transform, not a ping-pong channel |
| `0x22f` | `gfx_op_0x22f_set_position_anim` → `gfx_worker_set_translation` | set object **position** (translation vec `obj+0x5d4`); base transform, not a ping-pong channel. Operand 2 is also retained as channel control state; the port currently ignores that field. |
| `0x229` | `op_0x229_set_gfx_range_transform` → `gfx_range_transform_reset` / `select_handles` / `set_anchor` | reset/select the shared **retained-object range transform**; not a per-object position setter (superseded finding above) |
| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` |
| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong |
| `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color**: bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Negative alpha/RGB preserve corresponding bytes from static color `obj+0x60`; alpha >255 clamps. Distinct from one-shot `0x202`/static `0x203` |
| `0x228` | `gfx_op_0x228_query_position` → `gfx_object_query_translation_target` (`0x47cdd0`) | **query** the decomposed target-translation matrix (x,y,z), `obj+0x1ac/+0x1b0/+0x1b4`, → operand slots 3/4/5; success is 0 and missing is 1 |
| `0x23f` | `gfx_op_0x23f_query_object` (`FUN_0042a520`) | **query** an object status/value → operand slot 1 |
| `0x23f` | `op_0x23f_query_surface_stop_time_ms` (`0x42a520`) | **query loaded-surface DirectShow stop position in integer milliseconds** → operand slot 1, or -1 for an empty movie surface. Implemented with an explicit warning/-1 safety path when host timing metadata is unavailable. |
**Follow-up resolution (2026-07-10):** `0x21f` is the one-shot axis-angle channel and is implemented with
affine rasterization. `0x223` is **not affine**: `gfx_queue_surface_alpha_transition` (`0x47f440`) inserts
@@ -1223,6 +1223,48 @@ The `/v2` image names/comments the handler; movie ctor/interface/open/play/volum
route helpers; renderer media-type/sample workers; and stop/detach/destructor lifecycle. The image was
saved after annotation.
### Movie-surface stop-time query `0x23f` (2026-07-21)
The exact ABI is `query-surface-stop-time-ms(out_stop_time_ms, surface_slot)`. Handler
`op_0x23f_query_surface_stop_time_ms@0x42a520` records a 5-dword instruction length, fetches operand 2,
and directly indexes `EngineCtx+0x52bd4[surface_slot]`. A null surface writes `-1` to operand 1. A non-null
surface dereferences the `CMovieToTexture+0x414` interface pointer and calls vtable slot `+0x28` with a
stack `double` output parameter.
That interface is conclusively `IMediaPosition`: graph initialization queries IID
`{56A868B2-0AD4-11CE-B03A-0020AF0BA770}`, and the inherited `IUnknown` + `IDispatch` layout places
`get_Duration` at `+0x1c`, `put_CurrentPosition` at `+0x20`, `get_CurrentPosition` at `+0x24`, and
`get_StopTime` at `+0x28`. Neighboring op `0x23e` calls `+0x24`, while movie op `0x245` calls `+0x20`,
which independently confirms the slot mapping. Thus `0x23f` does **not** call `get_Duration`; it asks for
the configured playback stop position. With an ordinary freshly opened graph that position normally
equals the media duration, explaining why every observed consumer uses it as a lifetime.
Native multiplies the returned `REFTIME` seconds by the double `1000.0` at `0x5713e8`, passes the x87
value to the compiler helper at `0x550850`, and writes the low 32-bit integer to operand 1. The helper's
SSE2 branch uses `CVTTSD2SI`; its x87 fallback corrects the current rounding result to the same behavior,
so conversion is truncation toward zero. The COM `HRESULT` is ignored and the local output is not
preinitialized: the handler assumes any non-null movie surface has a usable `IMediaPosition`. It is a
pure query—no run-state bit, service boundary, seek, or playback mutation occurs.
Himegari has 23 calls in 17 scripts: BTL, DEBUGADV2, FIELD (2), SC0000/10/20/40/50/60/70/80/90,
SC0100 (2), SC0110 (2), SC0120 (2), SC0130 (3), and USEMAGIC. Every site is associated with a preceding
`0x236` load of the queried surface; the repeated ADV form has five table writes between load and query.
FIELD supplies the strongest unit evidence: one path computes `stop_time_ms / 16 + 1` for a 16 ms callback
schedule, and another clamps it to 600 ms before calling DRAWVOL. This corroborates the native decode and
rules out the former port behavior, `retained gfx object exists ? 0 : -1`.
**Port implementation (2026-07-21):** `DirectShowMovieDecoder` now queries
`IMediaPosition::get_StopTime` after the graph reaches its running state and before synchronous `0x236`
initialization returns. It applies native seconds-to-milliseconds truncation and hands the value through
`IHost.PlayMovieToSurface` into a movie-surface record owned by `GfxState`. Decoder construction moved from
the deferred Godot callback to the VM-side synchronous open boundary; the ready decoder is staged in a
thread-safe pending registry and adopted by the main thread before frame sampling, preserving asynchronous
presentation. `0x23f` silently returns -1 for an empty movie slot. If the movie record exists but DirectShow
returned an error, non-finite value, or value outside native signed-32-bit range, the port emits
`movie stop-time unavailable ...; returning -1` and returns -1. This is the chosen safe substitute for
native's ignored-HRESULT/uninitialized-output edge case. A normal Game Start through SC0000 was manually
validated without the warning, confirming that the installed movie's ordinary metadata path succeeds.
### Grey-background root cause — slot collision + tint-strength (2026-07-08, gfx-log)
Diagnosed with the new `--gfx-log` compositor/op trace (docs/tools-reference.md). The grey background has

View File

@@ -589,7 +589,7 @@ Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie d
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **evidence:** Ghidra handler 0x417550 -> gfx_command_queue_clear 0x47cb10, which destroys queued nodes and restores the sentinel links/count.
### 0x228 `u00421940` (u00421940, argc 5)
### 0x228 `query-gfx-translation-target` (u00421940, argc 5)
- **summary:** 0x228 query translation target (succ)(handle)(outX)(outY)(outZ): clone the retained object, decompose its target translation matrix at obj+0x17c, and return matrix translation obj+0x1ac/+0x1b0/+0x1b4. Returns succ=0 when found; when absent, writes succ=1 and leaves outputs untouched. The C# VM queries TranslationTarget independently of V24. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2 handler gfx_op_0x228_query_position@0x42a3a0 calls gfx_object_query_translation_target@0x47cdd0. The worker copies the complete 0xb5-dword object record, passes copied obj+0x17c to matrix4_decompose_affine@0x48d7c8, and returns its translation outputs; the decomposition reads matrix elements +0x30/+0x34/+0x38, corresponding to obj+0x1ac/+0x1b0/+0x1b4. SC0000 AE001H queries this before each 0x220 leg. C# regression covers targets (40,-20), (50,-80), (130,-100), plus the missing-object output-preservation path.
@@ -614,16 +614,17 @@ Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie d
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x22d_set_gfx_range_scale_target@0x423990 divides operands 3..5 by 100 and calls gfx_range_transform_set_scale_target@0x472d50. The worker arms the embedded transform object's ordinary scale channel (delay obj+0x3c, duration +0x50, target matrix +0xac), which gfx_range_transform_sample_frame@0x476df0 samples before range composition. FIELD has the sole corpus call, a 300 ms camera zoom.
### 0x22f `u00421DD0` (u00421DD0, argc 5)
- **summary:** 0x22f set-position (handle)(op2)(x)(y)(z): set the object base position (direct transform, not ping-pong). Worker gfx_worker_set_translation @0x472e90. C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=kelebek, confidence=low
### 0x22f `set-gfx-position-current` (u00421DD0, argc 5)
- **summary:** (handle)(control)(x)(y)(z) set the object's current/base translation matrix. The native worker also retains operand 2 as channel control state; the port currently applies x/y/z but ignores that control operand.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: gfx_op_0x22f_set_position_anim@0x423b00 fetches x/y/z as floats and passes all five operands to gfx_worker_set_translation@0x472e90. The worker builds the current translation matrix with x/y/z and stores operand 2 in the channel record. This is distinct from the shared range transform at 0x229. The C# handler sets V24 directly and is therefore position-correct but does not yet model operand 2.
### 0x231 `u00421EA0` (u00421EA0, argc 4)
### 0x231 `animate-gfx-srcrect-loop` (u00421EA0, argc 4)
- **summary:** (handle)(frame_period_ms)(frame_count)(column_count) — loop row-major through the spritesheet. Every frame preserves draw-texture's source-rectangle width/height; frame=floor(elapsed/frame_period)%frame_count, src offset=(frame%columns*width, frame/columns*height). Worker gfx_worker_anim_srcrect @0x47eec0; consumer gfx_object_anim_interpolate @0x473ed0.
- **grounding:** source=investigation, confidence=high
- **evidence:** Native /v2 decompile: worker stores period at obj+0x230, frame_count at +0x238, columns at +0x23c. Interpolator computes ((now-start)/period)%frame_count, then offsets both source-rect X bounds by rect_width*(frame%columns) and Y bounds by rect_height*(frame/columns). SC0000 uses (100,8,4) with AE001H's eight 200x200 cells in a 4x2 800x400 sheet.
### 0x232 `u00421EF0` (u00421EF0, argc 4)
### 0x232 `animate-gfx-color-loop` (u00421EF0, argc 4)
- **summary:** 0x232 anim-color (handle)(period)(alpha)(color): ping-pong the temporary packed ARGB passed to the normal object blit. Handler resolves negative alpha/RGB from static color obj+0x60 and clamps alpha above 255. Blend selector obj+0x30 is unchanged: mode 0 keeps default blending (animated alpha is inert; RGB is vertex modulation), while mode 1 uses sampled ARGB alpha as the SRCALPHA scale for additive composition. Fresh static color is 0xffffffff. The C# VM resolves sentinels and consumes sampled ARGB through the unchanged mode-specific path. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: gfx_op_0x232_anim_color@0x423c30 resolves sentinels then calls gfx_worker_anim_color@0x47ef50; gfx_object_anim_interpolate@0x473ed0 samples static obj+0x60 toward target obj+0x240 into a temporary packed color; gfx_object_composite@0x47f650 passes that color plus unchanged selector obj+0x30 to gfx_object_blit_d3d9@0x4774c0. Blit mode 0 leaves the default path and passes RGB as modulation; mode 1 sets SRCALPHA/ONE additive composition. gfx_object_init_default@0x472810 initializes obj+0x60=0xffffffff. SC0000 0x1a0e (handle,1200,224,-1) is therefore 0xffffffff<->0xe0ffffff with inert alpha and identity RGB in mode 0: no visible pulse. C# regressions cover exact AE001H visual invariance, negative-RGB preservation, mode-0 RGB modulation, and mode-1 additive scaling.
@@ -644,7 +645,7 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
- **summary:** (duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. The generic instruction length is 3 dwords. 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
### 0x239 `u004223C0` (u004223C0, argc 6)
### 0x239 `animate-gfx-srcrect-target` (u004223C0, argc 6)
- **summary:** (handle)(delay_ms)(duration_ms)(frame_count)(column_count)(target_frame) — one-shot row-major source-rectangle cell channel. Worker gfx_worker_set_srcrect_cell @0x47ed90 stores timing at obj+0x48/+0x5c, layout at +0x238/+0x23c, and target at +0x234. C# currently retains the endpoint cell immediately.
- **grounding:** source=investigation, confidence=high
- **evidence:** Native /v2 worker and gfx_object_apply_transform_channels decompile. The consumer advances target_frame cells over duration after delay, preserves the existing source-rect dimensions, and commits the endpoint.
@@ -656,9 +657,12 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b
For each fixed slot in [42,1000), the handler stops/releases the movie-to-texture object at ctx+0x52bd4[slot], then invokes the ordinary retained-gfx surface-release worker. Protected/externally owned slots may be retained by the worker's per-slot guard. This is the resource half of the common 0x1f6/0x23d full-reset sequence.
### 0x23f `u00422930` (u00422930, argc 2)
- **summary:** 0x23f query-object (out)(handle): return object status (FUN_0042a520; -1 if none). C# VM: 0 if the object exists else -1. See docs/engine-re.md §SC0000 anim cluster.
- **grounding:** source=kelebek, confidence=low
### 0x23f `query-surface-stop-time-ms` (u00422930, argc 2)
- **summary:** (out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. The port retains this metadata during 0x236 graph initialization; unavailable metadata emits a warning and also returns -1.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x23f_query_surface_stop_time_ms@0x42a520 indexes EngineCtx surface array operand 2, returns -1 for a null slot, otherwise dereferences movie+0x414 IMediaPosition and calls vtable+0x28 get_StopTime. The adjacent op 0x23e uses the same interface at vtable+0x24 get_CurrentPosition; movie op 0x245 uses +0x20 put_CurrentPosition, independently confirming the documented vtable layout. The returned seconds are multiplied by g_dMillisecondsPerSecond@0x5713e8 (double 1000.0) and truncated by crt_ftol2_sse_truncate@0x550850 before vm_operand_write(1). All 23 Himegari sites in 17 scripts are associated with a preceding op 0x236 movie load to the queried surface. FIELD divides one result by 16 and adds 1 to build a 16 ms callback schedule; another path clamps the result to 600 ms before DRAWVOL.
The surface object's +0x414 member is IMediaPosition. Its vtable +0x28 entry is get_StopTime (after IUnknown, IDispatch, get_Duration, put_CurrentPosition, and get_CurrentPosition), returning a REFTIME double in seconds. Native multiplies by the double constant 1000.0 and calls the compiler float-to-integer helper, whose SSE2 and x87 paths both truncate toward zero. It does not inspect the getter HRESULT. For a valid graph the default stop time normally equals media duration, which explains duration-style consumers, but the exact ABI is stop position rather than get_Duration. The handler only queries state; it does not yield or alter playback. Port safety extension: a modeled movie surface whose decoder cannot supply a finite signed-32-bit stop time reports a warning and returns -1 instead of reproducing native's uninitialized-output failure path.
### 0x242 `set-object-animation-detached` (set-object-animation-detached, argc 2)
- **summary:** Replace the retained object's animation-control word at obj+0x2d0. Bit 0 detaches finite one-shot channels from blocking presentation and protects them from 0x243 forced completion until they finish naturally.
@@ -871,6 +875,11 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
## marker
### 0x1a8 `instruction-marker-noop` (dev_ukn, argc 0)
- **summary:** Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns.
- **grounding:** source=investigation, confidence=high, noop_headless=True
- **evidence:** Ghidra /v2: op_0xaf_0x1a8_handler@0x416650 writes frame_instruction_word_count=1 for the current script context and immediately returns. Himegari has 1,989 op-0x1a8 sites. The port's explicit no-op matches native behavior.
### 0x1bf `call-end` (u004156C0, argc 0)
- **summary:** zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker
- **grounding:** source=inference, confidence=med, noop_headless=True
@@ -1142,10 +1151,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=med
### 0x1a8 `dev_ukn` (dev_ukn, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1a9 `u00428090` (u00428090, argc 1)
- **summary:** —
- **grounding:** source=kelebek, confidence=low

View File

@@ -2238,3 +2238,24 @@ and closed without losing the underlying page, then classify only the secondary
**Manual validation:** passed on 2026-07-21. The system menu opens from ADV and returns successfully through
the implemented script-owned action path; no underlying-page restoration discrepancy was reported.
### Slice B1 movie-surface stop-time query implemented (2026-07-21)
Native RE corrected op `0x23f` from the port's former retained-object existence test to
`query-surface-stop-time-ms(out, surface_slot)`. It calls `IMediaPosition::get_StopTime`, multiplies seconds
by 1000, truncates toward zero, and returns -1 only for an empty movie slot. All 23 Himegari sites are
associated with a preceding non-modal `0x236`; FIELD consumes the result as an effect lifetime.
The Windows decoder now obtains the stop time inside `0x236`'s synchronous graph-construction boundary and
hands it into engine-owned per-surface movie state before the VM advances. The already initialized decoder
is staged for main-thread adoption, so frame delivery remains asynchronous without reopening the graph.
`0x23f` returns the retained value. If a movie exists but its decoder supplies no finite signed-32-bit
timing value, the host reports a warning containing script/offset/surface context and returns -1; a genuinely
empty movie slot returns -1 silently. Focused tests cover all three outcomes and the real SC0000 DirectShow
payload exposes a positive stop time. Validation: 270 engine tests, zero-warning Godot build, and threaded
Godot selftest.
**Manual validation:** passed on 2026-07-21. A normal Game Start through SC0000's movie-backed opening
produced no `movie stop-time unavailable` warning, confirming the ordinary DirectShow timing handoff.
**Next:** continue with the next concrete discrepancy reached by manual testing.

View File

@@ -423,6 +423,17 @@ decoder-format gap, not missing or EXE-embedded artwork. The CUR decoder now han
and 4-bpp formats with independent XOR/AND strides; archive-backed pixel/hotspot tests pass. Manual
acceptance confirms the gripped cursor now displays correctly during drag-panning.
A follow-up full-corpus analytics audit corrected a metadata artifact: ops `0x228`, `0x22f`, `0x231`,
`0x232`, `0x239`, and `0x23f` had detailed native RE but still retained opaque semantic names, while
`0x1a8` was an unnamed explicit no-op. Their semantic names are now current, and native proves `0x1a8`
is a structural marker whose handler only records instruction length. The same audit exposed a real runtime
gap, now closed: `0x23f` queries a loaded surface's DirectShow stop position in truncated integer
milliseconds (all 23 sites are associated with a preceding `0x236`). On a freshly opened graph the stop
position normally equals its duration; FIELD uses it for 16 ms animation scheduling. The port now queries
that value during synchronous movie-graph initialization and retains it per movie surface. Empty movie
slots return -1; unavailable timing metadata warns and returns -1 rather than exposing native's undefined
failure output.
## Later Phase B breadth
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks:

View File

@@ -52,10 +52,12 @@ VM op 0x236 (non-modal) / op 0x20f (modal)
```
Everything before and after `DirectShowMovieDecoder` is portable. The backend currently exposes the right
conceptual operations (`TryTakeFrame`, `IsCompleted`, and `Dispose`) but they are not formalized as an
conceptual operations (`StopTimeMs`, `TryTakeFrame`, `IsCompleted`, and `Dispose`) but they are not formalized as an
interface. A future cleanup should:
1. Add an `IMovieDecoder` contract for frame delivery, completion, failure, and disposal.
1. Add an `IMovieDecoder` contract for synchronous stop-time metadata, frame delivery, completion, failure,
and disposal. Stop time must be available before `0x236` returns so an immediately following `0x23f`
remains deterministic.
2. Add an injected factory that accepts `MoviePayload` and selects an available backend.
3. Keep DirectShow in a Windows-specific source set or assembly, with its platform annotation local to it.
4. Implement a portable MPEG program-stream backend that produces the same top-down RGBA8 frames.