Implement movie surface stop-time queries
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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'),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]]
|
||||||
|
|||||||
Reference in New Issue
Block a user