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):** **In scope (built this slice):**
| op | handler / worker | semantics | | 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) | | `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` | | `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 | | `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` | | `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 | | `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 **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 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 route helpers; renderer media-type/sample workers; and stop/detach/destructor lifecycle. The image was
saved after annotation. 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) ### 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 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 - **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. - **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. - **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 - **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. - **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 - **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. - **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) ### 0x22f `set-gfx-position-current` (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. - **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=kelebek, confidence=low - **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. - **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 - **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. - **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. - **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 - **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. - **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. - **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 - **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. - **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 - **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. - **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. 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) ### 0x23f `query-surface-stop-time-ms` (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. - **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=kelebek, confidence=low - **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) ### 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. - **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 ## 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) ### 0x1bf `call-end` (u004156C0, argc 0)
- **summary:** zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker - **summary:** zero-arg; call->0x1bf->stmt-end — end-of-call-statement marker
- **grounding:** source=inference, confidence=med, noop_headless=True - **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:** — - **summary:** —
- **grounding:** source=kelebek, confidence=med - **grounding:** source=kelebek, confidence=med
### 0x1a8 `dev_ukn` (dev_ukn, argc 0)
- **summary:** —
- **grounding:** source=kelebek, confidence=low
### 0x1a9 `u00428090` (u00428090, argc 1) ### 0x1a9 `u00428090` (u00428090, argc 1)
- **summary:** — - **summary:** —
- **grounding:** source=kelebek, confidence=low - **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 **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. 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 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. 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 ## Later Phase B breadth
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks: 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 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: 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. 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. 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. 4. Implement a portable MPEG program-stream backend that produces the same top-down RGBA8 frames.

View File

@@ -100,6 +100,70 @@ public class MovieOpcodeTests
Assert.Equal(0x33, vm.Gfx.SnapshotVisibleObjects().Single().SurfaceResId); Assert.Equal(0x33, vm.Gfx.SnapshotVisibleObjects().Single().SurfaceResId);
} }
[Fact]
public void QueryMovieStopTimeReturnsTheValueRetainedByPlayMovie()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "MOVIE-TIME", new List<(int, Operand[])>
{
(0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }),
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
(0x2, System.Array.Empty<Operand>()),
}, System.Array.Empty<string>());
var host = new RecordingHost { MovieStopTimeMs = 1876 };
var vm = new VirtualMachine(script, table, host);
vm.Run();
Assert.Equal(1876, vm.Globals[0x1234]);
Assert.True(vm.Gfx.TryGetMovieStopTime(5, out long? retained));
Assert.Equal(1876, retained);
Assert.Empty(host.Warnings);
vm.Gfx.SetSurface(5, 0x34, 0); // replacing the native surface tears down its movie metadata
Assert.False(vm.Gfx.TryGetMovieStopTime(5, out _));
}
[Fact]
public void QueryMovieStopTimeReturnsMinusOneWithoutWarningForAnEmptyMovieSlot()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "EMPTY-MOVIE-TIME", new List<(int, Operand[])>
{
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
(0x2, System.Array.Empty<Operand>()),
}, System.Array.Empty<string>());
var host = new RecordingHost();
var vm = new VirtualMachine(script, table, host);
vm.Gfx.GetOrCreate(5); // retained-object existence is unrelated to the native movie-object slot
vm.Run();
Assert.Equal(-1, vm.Globals[0x1234]);
Assert.Empty(host.Warnings);
}
[Fact]
public void QueryMovieStopTimeWarnsAndReturnsMinusOneWhenMetadataIsUnavailable()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "MISSING-MOVIE-TIME", new List<(int, Operand[])>
{
(0x236, new[] { new Operand(0, 0x33), new Operand(0, 5), new Operand(0, 2), new Operand(0, 0) }),
(0x23f, new[] { new Operand(3, 0x1234), new Operand(0, 5) }),
(0x2, System.Array.Empty<Operand>()),
}, System.Array.Empty<string>());
var host = new RecordingHost();
var vm = new VirtualMachine(script, table, host);
vm.Run();
Assert.Equal(-1, vm.Globals[0x1234]);
string warning = Assert.Single(host.Warnings);
Assert.Contains("movie stop-time unavailable MISSING-MOVIE-TIME@", warning);
Assert.Contains("surface=5; returning -1", warning);
}
[Fact] [Fact]
public void Sc0000MoviePayloadReadsFromArchiveVfsAndIsMpegProgramStream() public void Sc0000MoviePayloadReadsFromArchiveVfsAndIsMpegProgramStream()
{ {
@@ -136,6 +200,8 @@ public class MovieOpcodeTests
var entry = resources.Resolve("SC0000", 0x33)!; var entry = resources.Resolve("SC0000", 0x33)!;
using var decoder = new DirectShowMovieDecoder(resources.ReadMovie(entry)); using var decoder = new DirectShowMovieDecoder(resources.ReadMovie(entry));
Assert.True(decoder.StopTimeMs > 0, "DirectShow should expose a positive IMediaPosition stop time");
var deadline = DateTime.UtcNow.AddSeconds(10); var deadline = DateTime.UtcNow.AddSeconds(10);
RgbaImage? frame = null; RgbaImage? frame = null;
while (DateTime.UtcNow < deadline && !decoder.TryTakeFrame(out frame)) while (DateTime.UtcNow < deadline && !decoder.TryTakeFrame(out frame))

View File

@@ -38,6 +38,7 @@ internal class RecordingHost : IHost
public readonly List<int> SfxReleases = new(); public readonly List<int> SfxReleases = new();
public readonly List<(int Target, long Duration)> BgmFades = new(); public readonly List<(int Target, long Duration)> BgmFades = new();
public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new(); public readonly List<(long Resource, int Surface, long Flags, long SyncMask)> Movies = new();
public long? MovieStopTimeMs;
public readonly List<(long Resource, int Surface, long Flags)> ModalMovies = new(); public readonly List<(long Resource, int Surface, long Flags)> ModalMovies = new();
public readonly List<int> ClearedRenderTargets = new(); public readonly List<int> ClearedRenderTargets = new();
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new(); public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
@@ -45,11 +46,13 @@ internal class RecordingHost : IHost
public readonly List<(long Resource, int Slot)> Textures = new(); public readonly List<(long Resource, int Slot)> Textures = new();
public readonly List<bool> MessageSkipChanges = new(); public readonly List<bool> MessageSkipChanges = new();
public readonly List<bool> PhysicalMessageSkipChanges = new(); public readonly List<bool> PhysicalMessageSkipChanges = new();
public readonly List<string> Warnings = new();
public readonly List<long> CursorResources = new(); public readonly List<long> CursorResources = new();
public readonly List<bool> AdvPagePresentationSuspended = new(); public readonly List<bool> AdvPagePresentationSuspended = new();
public int CursorClearCount; public int CursorClearCount;
public int SceneContextResets; public int SceneContextResets;
public long TextureResourceIdOffset; public long TextureResourceIdOffset;
public void ReportWarning(string message) => Warnings.Add(message);
public long ResolveTextureResourceId(long resourceId) => resourceId + TextureResourceIdOffset; public long ResolveTextureResourceId(long resourceId) => resourceId + TextureResourceIdOffset;
public void ShowText(int offset, string text) => Lines.Add((offset, text)); public void ShowText(int offset, string text) => Lines.Add((offset, text));
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y)); public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
@@ -145,8 +148,11 @@ internal class RecordingHost : IHost
=> ScheduledSfxStarts.Add((channel, startMode, delayMs)); => ScheduledSfxStarts.Add((channel, startMode, delayMs));
public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel); public void ReleaseSoundEffect(int channel) => SfxReleases.Add(channel);
public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs)); public void FadeBgm(int targetPercent, long durationMs) => BgmFades.Add((targetPercent, durationMs));
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
=> Movies.Add((resourceId, surfaceSlot, movieFlags, syncMask)); {
Movies.Add((resourceId, surfaceSlot, movieFlags, syncMask));
return MovieStopTimeMs;
}
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags) public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
=> ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags)); => ModalMovies.Add((rawResourceId, surfaceSlot, movieFlags));
} }

View File

@@ -15,6 +15,8 @@ public readonly record struct SurfaceRectFill(
public interface IHost public interface IHost
{ {
/// <summary>Report a recoverable runtime discrepancy while allowing script execution to continue.</summary>
void ReportWarning(string message) => System.Console.Error.WriteLine(message);
// Script-local resource ids resolve against the currently executing frame's SYS4INI section. // Script-local resource ids resolve against the currently executing frame's SYS4INI section.
// Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior. // Interactive hosts track this stack; headless hosts may keep the no-op/default identity behavior.
void EnterScriptContext(string scriptName) { } void EnterScriptContext(string scriptName) { }
@@ -111,7 +113,10 @@ public interface IHost
void FadeBgm(int targetPercent, long durationMs) { } void FadeBgm(int targetPercent, long durationMs) { }
// Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface. // Native op 0x236 binds a DirectShow movie decoder to an existing retained texture surface.
// Playback is non-modal: the VM advances to the following instruction while the host publishes frames. // Playback is non-modal: the VM advances to the following instruction while the host publishes frames.
void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) { } /// <returns>The initialized movie graph's stop position in truncated integer milliseconds, or null
/// when the host could not obtain usable timing metadata. Native op 0x23f queries this state
/// immediately after 0x236 returns.</returns>
long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) => null;
// Native op 0x20f uses a universal raw-catalog id and parks script execution until the movie // Native op 0x20f uses a universal raw-catalog id and parks script execution until the movie
// reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host // reaches EOF or the player cancels it. The decoder remains asynchronous; the interactive host
// owns the modal wait so its render loop can continue publishing frames. // owns the modal wait so its render loop can continue publishing frames.

View File

@@ -299,6 +299,7 @@ public sealed class GfxState
_objects.Clear(); _objects.Clear();
_fieldTable.Clear(); _fieldTable.Clear();
_surfaces.Clear(); _surfaces.Clear();
_movieStopTimesMs.Clear();
_surfaceTransitions.Clear(); _surfaceTransitions.Clear();
CurrentObject = 0; CurrentObject = 0;
CurrentRenderTargetSlot = -1; CurrentRenderTargetSlot = -1;
@@ -314,8 +315,32 @@ public sealed class GfxState
// ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ---- // ---- surfaces (image buffers per slot): ctx+0x52bd4[slot], from create/set-texture ----
private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new(); private readonly Dictionary<int, (long ResId, long ColorKey)> _surfaces = new();
// A separate entry models the native CMovieToTexture object attached to a surface. A null value means
// the movie object exists but its host decoder supplied no usable IMediaPosition stop time.
private readonly Dictionary<int, long?> _movieStopTimesMs = new();
private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new(); private readonly Dictionary<int, SurfaceTransition> _surfaceTransitions = new();
public void SetSurface(int slot, long resId, long colorKey) { lock (_lock) { _surfaces[slot] = (resId, colorKey); } } public void SetSurface(int slot, long resId, long colorKey)
{
lock (_lock)
{
_surfaces[slot] = (resId, colorKey);
_movieStopTimesMs.Remove(slot);
}
}
/// <summary>Op 0x236 handoff: retain the initialized movie graph's IMediaPosition stop time. Null
/// deliberately distinguishes a movie surface with unavailable metadata from an empty movie slot.</summary>
public void SetMovieStopTime(int slot, long? stopTimeMs)
{
lock (_lock) _movieStopTimesMs[slot] = stopTimeMs;
}
/// <summary>Op 0x23f query. False means no movie object occupies the slot; true with a null value
/// means the movie exists but its stop-time query failed or returned unusable metadata.</summary>
public bool TryGetMovieStopTime(int slot, out long? stopTimeMs)
{
lock (_lock) return _movieStopTimesMs.TryGetValue(slot, out stopTimeMs);
}
/// <summary>Op 0x20d: select a surface as the D3D render target; values at or above 1000 restore the /// <summary>Op 0x20d: select a surface as the D3D render target; values at or above 1000 restore the
/// device backbuffer in the native engine.</summary> /// device backbuffer in the native engine.</summary>
@@ -333,6 +358,7 @@ public sealed class GfxState
for (int slot = firstSlot; slot < end; slot++) for (int slot = firstSlot; slot < end; slot++)
{ {
_surfaces.Remove(slot); _surfaces.Remove(slot);
_movieStopTimesMs.Remove(slot);
_surfaceTransitions.Remove(slot); _surfaceTransitions.Remove(slot);
} }
if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end) if (CurrentRenderTargetSlot >= firstSlot && CurrentRenderTargetSlot < end)
@@ -429,7 +455,14 @@ public sealed class GfxState
o.ColorAnim = true; o.ColorAnim = true;
} }
} }
public void ClearSurface(int slot) { lock (_lock) { _surfaces[slot] = (0, 0); } } // create-texture (blank) public void ClearSurface(int slot)
{
lock (_lock)
{
_surfaces[slot] = (0, 0); // create-texture (blank)
_movieStopTimesMs.Remove(slot);
}
}
/// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary> /// <summary>Op 0x223: queue a type-0 timed alpha transition into a target surface slot.</summary>
public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot, public void QueueSurfaceAlphaTransition(long commandKey, int targetSlot,

View File

@@ -1425,7 +1425,8 @@ public sealed class VirtualMachine
// The native CMovieToTexture renderer replaces the pixels of the already-created surface. // The native CMovieToTexture renderer replaces the pixels of the already-created surface.
// Retain the same resource binding so the compositor resolves live movie frames for its objects. // Retain the same resource binding so the compositor resolves live movie frames for its objects.
Gfx.SetSurface(surfaceSlot, resourceId, 0); Gfx.SetSurface(surfaceSlot, resourceId, 0);
_host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3])); long? stopTimeMs = _host.PlayMovieToSurface(resourceId, surfaceSlot, Read(a[2]), Read(a[3]));
Gfx.SetMovieStopTime(surfaceSlot, stopTimeMs);
return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous return pc + 1; // native cmd size 9 resumes at the next instruction; playback is asynchronous
} }
// ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ---- // ---- gfx command-buffer ops (VM-internal GfxState; docs/engine-re.md op-contract table) ----
@@ -1490,8 +1491,26 @@ public sealed 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": // 0x23f query-object: (out)(handle) <- 0 if the object exists, else -1 case "u00422930": // pre-reference compatibility
Write(a[0], Gfx.TryGet(Read(a[1])) != null ? 0 : -1); return pc + 1; 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 "set-gfx-geom3-c": // 0x1ff: set current translation matrix case "set-gfx-geom3-c": // 0x1ff: set current translation matrix
Gfx.SetCurrentTranslation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1; Gfx.SetCurrentTranslation(Read(a[0]), (Read(a[1]), Read(a[2]), Read(a[3]))); return pc + 1;
case "u00420620": // upstream ABI label case "u00420620": // upstream ABI label

View File

@@ -35,6 +35,9 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
private IMediaControl? _control; private IMediaControl? _control;
public bool IsCompleted => _completed; public bool IsCompleted => _completed;
/// <summary>The graph's IMediaPosition stop time converted exactly as native op 0x23f does:
/// seconds * 1000, truncated toward zero. Null means DirectShow supplied no usable value.</summary>
public long? StopTimeMs { get; private set; }
public DirectShowMovieDecoder(MoviePayload movie) public DirectShowMovieDecoder(MoviePayload movie)
{ {
@@ -112,6 +115,13 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
int stateHr = _control.GetState(5000, out int graphState); int stateHr = _control.GetState(5000, out int graphState);
if (stateHr < 0) Check(stateHr, "wait for running movie graph"); if (stateHr < 0) Check(stateHr, "wait for running movie graph");
if (graphState != 2) throw new InvalidOperationException($"movie graph entered unexpected state {graphState}"); if (graphState != 2) throw new InvalidOperationException($"movie graph entered unexpected state {graphState}");
var mediaPosition = (IMediaPosition)_graphObject;
int stopHr = mediaPosition.get_StopTime(out double stopTimeSeconds);
double stopTimeMilliseconds = stopTimeSeconds * 1000.0;
if (stopHr >= 0 && double.IsFinite(stopTimeMilliseconds)
&& stopTimeMilliseconds >= int.MinValue
&& stopTimeMilliseconds <= int.MaxValue)
StopTimeMs = (long)System.Math.Truncate(stopTimeMilliseconds);
_ready.Set(); _ready.Set();
var mediaEvent = (IMediaEvent)_graphObject; var mediaEvent = (IMediaEvent)_graphObject;
while (!_stopping) while (!_stopping)
@@ -265,6 +275,21 @@ internal sealed class DirectShowMovieDecoder : IDisposable, ISampleGrabberCB
[ComImport, Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] [ComImport, Guid("56A868B1-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
private interface IMediaControl private interface IMediaControl
{ [PreserveSig] int Run(); [PreserveSig] int Pause(); [PreserveSig] int Stop(); [PreserveSig] int GetState(int timeout, out int state); } { [PreserveSig] int Run(); [PreserveSig] int Pause(); [PreserveSig] int Stop(); [PreserveSig] int GetState(int timeout, out int state); }
[ComImport, Guid("56A868B2-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
private interface IMediaPosition
{
[PreserveSig] int get_Duration(out double seconds);
[PreserveSig] int put_CurrentPosition(double seconds);
[PreserveSig] int get_CurrentPosition(out double seconds);
[PreserveSig] int get_StopTime(out double seconds);
[PreserveSig] int put_StopTime(double seconds);
[PreserveSig] int get_PrerollTime(out double seconds);
[PreserveSig] int put_PrerollTime(double seconds);
[PreserveSig] int put_Rate(double rate);
[PreserveSig] int get_Rate(out double rate);
[PreserveSig] int CanSeekForward(out int canSeekForward);
[PreserveSig] int CanSeekBackward(out int canSeekBackward);
}
[ComImport, Guid("56A868B6-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)] [ComImport, Guid("56A868B6-0AD4-11CE-B03A-0020AF0BA770"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
private interface IMediaEvent private interface IMediaEvent
{ {

View File

@@ -74,6 +74,8 @@ public sealed class GodotAdvHost : IHost
_locator = locator; _timeline = timeline; _locator = locator; _timeline = timeline;
} }
public void ReportWarning(string message) => System.Console.Error.WriteLine(message);
private string CurrentScene private string CurrentScene
{ {
get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; } get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; }
@@ -767,12 +769,13 @@ public sealed class GodotAdvHost : IHost
return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null; return asset != null && image != null ? (image, asset.Name, asset.RawIndex, false) : null;
} }
public void PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask) public long? PlayMovieToSurface(long resourceId, int surfaceSlot, long movieFlags, long syncMask)
{ {
string scene = CurrentScene; string scene = CurrentScene;
var asset = _res.Resolve(scene, resourceId); var asset = _res.Resolve(scene, resourceId);
if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return; } if (asset == null) { Godot.GD.Print($"movie unresolved {scene}:0x{resourceId:x}"); return null; }
StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false); return StartMovie(asset, resourceId, surfaceSlot, movieFlags, syncMask, modal: false,
out long? stopTimeMs) ? stopTimeMs : null;
} }
public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags) public void PlayModalMovieToSurface(long rawResourceId, int surfaceSlot, long movieFlags)
@@ -788,7 +791,8 @@ public sealed class GodotAdvHost : IHost
_modalMovieWaiting = true; _modalMovieWaiting = true;
try try
{ {
if (!StartMovie(asset, rawResourceId, surfaceSlot, movieFlags, 0, modal: true)) return; if (!StartMovie(asset, rawResourceId, surfaceSlot, movieFlags, 0, modal: true,
out _)) return;
_timeline?.State("modal-movie-wait", new() _timeline?.State("modal-movie-wait", new()
{ {
["resource"] = rawResourceId, ["surface"] = surfaceSlot, ["file"] = asset.Name, ["resource"] = rawResourceId, ["surface"] = surfaceSlot, ["file"] = asset.Name,
@@ -818,8 +822,9 @@ public sealed class GodotAdvHost : IHost
} }
private bool StartMovie(AssetEntry asset, long resourceId, int surfaceSlot, long movieFlags, private bool StartMovie(AssetEntry asset, long resourceId, int surfaceSlot, long movieFlags,
long syncMask, bool modal) long syncMask, bool modal, out long? stopTimeMs)
{ {
stopTimeMs = null;
try try
{ {
var movie = _res.ReadMovie(asset); var movie = _res.ReadMovie(asset);
@@ -835,8 +840,7 @@ public sealed class GodotAdvHost : IHost
["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name, ["resource"] = resourceId, ["surface"] = surfaceSlot, ["file"] = movie.Name,
["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal, ["flags"] = movieFlags, ["sync_mask"] = syncMask, ["modal"] = modal,
}); });
_main.CallDeferred("PlayMovie", movie.Bytes, movie.Name, resourceId, asset.RawIndex); return _main.TryPlayMovie(movie.Bytes, movie.Name, resourceId, asset.RawIndex, out stopTimeMs);
return true;
} }
catch (System.Exception e) catch (System.Exception e)
{ {

View File

@@ -52,6 +52,9 @@ public partial class Main : Godot.Control
private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>(); private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
private readonly Age.Engine.Hosting.FrameClock _clock = new(); private readonly Age.Engine.Hosting.FrameClock _clock = new();
private readonly System.Collections.Generic.Dictionary<long, MovieRuntime> _movies = new(); private readonly System.Collections.Generic.Dictionary<long, MovieRuntime> _movies = new();
// 0x236 creates its graph synchronously on the VM thread so 0x23f can query timing immediately.
// Presentation ownership transfers here; _Process adopts staged decoders before sampling frames.
private readonly System.Collections.Concurrent.ConcurrentDictionary<long, MovieRuntime> _pendingMovies = new();
private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new(); private readonly System.Collections.Generic.HashSet<long> _movieFrameSeen = new();
private GodotTraceSink _trace = null!; private GodotTraceSink _trace = null!;
private PageLocatorState _locator = null!; private PageLocatorState _locator = null!;
@@ -318,6 +321,7 @@ public partial class Main : Godot.Control
_timeline?.SetFrame(_timelineFrame, _clock.NowMs); _timeline?.SetFrame(_timelineFrame, _clock.NowMs);
_host?.PulseFrame(); _host?.PulseFrame();
UpdateVoicePlaybackState(); UpdateVoicePlaybackState();
AdoptPendingMovies();
UpdateMovieFrames(); UpdateMovieFrames();
if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx)) if (!_selftest && _vm != null && _host != null && _host.ShouldRecomposite(_vm.Gfx))
Recomposite(); // native publishes retained mutations only at present/service boundaries Recomposite(); // native publishes retained mutations only at present/service boundaries
@@ -595,6 +599,8 @@ public partial class Main : Godot.Control
public override void _ExitTree() public override void _ExitTree()
{ {
DumpHistogram(); _host?.Stop(); _timeline?.Dispose(); _locator?.Dispose(); DumpHistogram(); _host?.Stop(); _timeline?.Dispose(); _locator?.Dispose();
foreach (var movie in _pendingMovies.Values) movie.Decoder.Dispose();
_pendingMovies.Clear();
foreach (var movie in _movies.Values) movie.Decoder.Dispose(); foreach (var movie in _movies.Values) movie.Decoder.Dispose();
_movies.Clear(); _movies.Clear();
} }
@@ -1162,19 +1168,35 @@ public partial class Main : Godot.Control
CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds); CreateTween().TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
} }
public void PlayMovie(byte[] mpegBytes, string assetName, long resourceId, int rawIndex) public bool TryPlayMovie(byte[] mpegBytes, string assetName, long resourceId, int rawIndex,
out long? stopTimeMs)
{ {
if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose(); stopTimeMs = null;
try try
{ {
var payload = new Age.Engine.Sys4.MoviePayload(assetName, mpegBytes); var payload = new Age.Engine.Sys4.MoviePayload(assetName, mpegBytes);
_movies[resourceId] = new MovieRuntime(assetName, rawIndex, new DirectShowMovieDecoder(payload)); var runtime = new MovieRuntime(assetName, rawIndex, new DirectShowMovieDecoder(payload));
GD.Print($"movie started {assetName} ({mpegBytes.Length} bytes from VFS)"); stopTimeMs = runtime.Decoder.StopTimeMs;
while (!_pendingMovies.TryAdd(resourceId, runtime))
if (_pendingMovies.TryRemove(resourceId, out var prior)) prior.Decoder.Dispose();
return true;
} }
catch (System.Exception e) catch (System.Exception e)
{ {
GD.Print($"movie decode failed {assetName}: {e.Message}"); GD.Print($"movie decode failed {assetName}: {e.Message}");
_host.NotifyMovieCompleted(resourceId); // release a pending 0x21c boundary on deterministic load failure _host.NotifyMovieCompleted(resourceId); // release a pending 0x21c boundary on deterministic load failure
return false;
}
}
private void AdoptPendingMovies()
{
foreach (var (resourceId, _) in _pendingMovies)
{
if (!_pendingMovies.TryRemove(resourceId, out var movie)) continue;
if (_movies.Remove(resourceId, out var prior)) prior.Decoder.Dispose();
_movies[resourceId] = movie;
GD.Print($"movie started {movie.Name} ({movie.Decoder.StopTimeMs?.ToString() ?? "unknown"} ms from VFS)");
} }
} }
@@ -1195,6 +1217,7 @@ public partial class Main : Godot.Control
public void StopMovie(long resourceId) public void StopMovie(long resourceId)
{ {
if (_pendingMovies.TryRemove(resourceId, out var pending)) pending.Decoder.Dispose();
if (_movies.Remove(resourceId, out var movie)) if (_movies.Remove(resourceId, out var movie))
{ {
movie.Decoder.Dispose(); movie.Decoder.Dispose();

View File

@@ -46,6 +46,7 @@ INFERRED: dict[int, dict] = {
0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'), 0x19a: dict(name='get-message-skip', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the current all-message skip state set by op 0x88.'),
0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."), 0x19b: dict(name='suspend-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary="() - suspend active ADV fast-forward while preserving the user's persistent all-message Skip toggle."),
0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'), 0x19c: dict(name='resume-adv-skip-service', category='input', noop=False, confidence='high', source='investigation', summary='() - enable the ADV skip service and recompute active fast-forward from persistent all-message Skip or the live read-skip channel.'),
0x1a8: dict(name='instruction-marker-noop', category='marker', noop=True, confidence='high', source='investigation', summary="Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."),
0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'), 0x1b6: dict(name='get-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return whether automatic message advance is enabled.'),
0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'), 0x1b7: dict(name='set-auto-message', category='input', noop=False, confidence='high', source='investigation', summary='(enabled) - enable or disable automatic message advance.'),
0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'), 0x1b8: dict(name='get-auto-message-time', category='input', noop=False, confidence='high', source='investigation', summary='(selector)(out) - read an Auto-message delay from engine configuration: selector 0 = post-voice AutoMessageTime0, selector 1 = unvoiced AutoMessageTime1.'),
@@ -65,6 +66,12 @@ INFERRED: dict[int, dict] = {
0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."), 0x1ff: dict(name='set-current-translation', category='draw', noop=False, confidence='high', source='investigation', summary="0x1ff (handle)(x)(y)(z) — immediately replace the retained object's current translation matrix at obj+0x16c. This is the direct-current companion to 0x220's delayed target at obj+0x1ac."),
0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'), 0x20a: dict(name='publish-adv-text-layout', category='adv', noop=False, confidence='high', source='investigation', summary='(layout_slot) - republish one ADV text layout and, while active, its current wait-indicator frame.'),
0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'), 0x21b: dict(name='line-id?', category='marker', noop=True, confidence='med', source='harness', summary='1 imm; mov->0x21b->stmt-end; near save/load-messkip — likely line/stmt id, verify not msg-control'),
0x228: dict(name='query-gfx-translation-target', category='draw', noop=False, confidence='high', source='investigation', 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.'),
0x22f: dict(name='set-gfx-position-current', category='draw', noop=False, confidence='high', source='investigation', 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."),
0x231: dict(name='animate-gfx-srcrect-loop', category='draw', noop=False, confidence='high', source='investigation', 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."),
0x232: dict(name='animate-gfx-color-loop', category='draw', noop=False, confidence='high', source='investigation', 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.'),
0x239: dict(name='animate-gfx-srcrect-target', category='draw', noop=False, confidence='high', source='investigation', 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.'),
0x23f: dict(name='query-surface-stop-time-ms', category='draw', noop=False, confidence='high', source='investigation', 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.'),
0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'), 0x258: dict(name='decl?', category='marker', noop=True, confidence='low', source='harness', summary='2 imm; runs in a chain right after script-entry 0x259, enumerating ids — prologue declaration/registration?'),
0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'), 0x259: dict(name='script-entry', category='marker', noop=True, confidence='low', source='harness', summary='zero-arg; the first instruction of a script (offset 0), opens the decl chain that 0x258 continues — script/prologue entry marker, structural'),
} }

View File

@@ -3606,14 +3606,14 @@ argc = 0
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "dev_ukn" name = "instruction-marker-noop"
category = "unknown" category = "marker"
summary = "" summary = "Zero-operand structural marker. The native shared 0xaf/0x1a8 handler only records this instruction's one-dword length and returns."
noop_headless = false noop_headless = true
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" 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."
[[opcode]] [[opcode]]
op = 0x1a9 op = 0x1a9
@@ -5768,7 +5768,7 @@ argc = 5
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421940" name = "query-gfx-translation-target"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
@@ -5953,14 +5953,14 @@ argc = 5
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421DD0" name = "set-gfx-position-current"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" 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."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
@@ -6015,7 +6015,7 @@ argc = 4
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421EA0" name = "animate-gfx-srcrect-loop"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
@@ -6051,7 +6051,7 @@ argc = 4
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00421EF0" name = "animate-gfx-color-loop"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
@@ -6227,7 +6227,7 @@ argc = 6
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u004223C0" name = "animate-gfx-srcrect-target"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
@@ -6383,23 +6383,24 @@ argc = 2
abi_source = "kelebek+decode-validated" abi_source = "kelebek+decode-validated"
[opcode.semantics] [opcode.semantics]
name = "u00422930" name = "query-surface-stop-time-ms"
category = "draw" category = "draw"
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." 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."
noop_headless = false noop_headless = false
source = "kelebek" source = "investigation"
confidence = "low" confidence = "high"
depends_on = [] depends_on = []
evidence = "" details = "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."
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."
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 1 i = 1
role = "" role = "out_stop_time_ms"
observed_types = ["l-int", "l-ptr"] observed_types = ["l-int", "l-ptr"]
[[opcode.semantics.args]] [[opcode.semantics.args]]
i = 2 i = 2
role = "" role = "surface_slot"
observed_types = ["imm", "g-int", "l-int"] observed_types = ["imm", "g-int", "l-int"]
[[opcode]] [[opcode]]