From 682f65318b1b5d62d505d7d5a537af055dd62bdb Mon Sep 17 00:00:00 2001 From: gamer147 Date: Wed, 29 Jul 2026 15:08:05 -0400 Subject: [PATCH] Document final opcode investigations --- docs/engine-ctx-reference.md | 3 + docs/engine-re.md | 91 +++++++++++++++---- docs/global-reference.md | 4 +- docs/opcode-reference.md | 72 +++++++++------ docs/phase-b-framework.md | 30 ++++++ engine/Age.Engine/Model/GfxState.cs | 3 +- engine/Age.Engine/Vm/VirtualMachine.cs | 2 +- tools/age_opcodes_himegari.py | 2 +- vm-map/engine-ctx.toml | 15 +++ vm-map/globals.toml | 11 +++ vm-map/opcodes.toml | 121 +++++++++++++++---------- 11 files changed, 259 insertions(+), 95 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index dd93f11..bfe36d7 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -65,6 +65,9 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x55120` | `anti_tamper_a` | `int` | anti-tamper checksum operand | | `0x55124` | `anti_tamper_b` | `int` | anti-tamper checksum operand | | `0x5512c` | `anti_tamper_fp` | `int` | anti-tamper (import fn ptr / result) | +| `0x55130` | `int_queue_slots` | `void*` | base of ten scene-context-owned integer FIFO pointers used by ops 0x132-0x134; their <=10 validation bug makes id 10 alias int_stack_slots[0] | +| `0x55158` | `int_stack_slots` | `void*` | base of ten scene-context-owned integer LIFO pointers used by ops 0x137-0x139; their <=10 validation bug makes id 10 alias numeric_glyph_styles[0] | +| `0x55180` | `numeric_glyph_styles` | `int` | base of ten 20-byte decimal-glyph atlas records used by ops 0x13a/0x23b; opcode validation also admits one out-of-range record | | `0x55248` | `ret_stack_a` | `void*` | per-frame return stack (op 0x8f call pushes) | | `0x552e8` | `ret_stack_b` | `void*` | per-frame return stack (companion) | | `0x5f304` | `sleep_timer` | `int` | sleep timer object (op 0xc8; +8 active, +0x14 start-ms, +0x18 duration) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 69b941c..6c33fba 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -1666,6 +1666,46 @@ After the 213/213 video/audio corpus gate and clean audible LOGO/OP/CHAPTER acce `DirectShowMovieDecoder`, its COM/temp-file adapter, compatibility test, and managed Windows annotations. Native AGE's DirectShow behavior remains relevant evidence for opcode semantics; it is no longer port code. +### Integer LIFO family `0x137`-`0x139` and positioned movie playback `0x241` (2026-07-29) + +`op_0x137_reset_int_stack@0x421940` is the LIFO counterpart to the implemented integer FIFO family +`0x132`-`0x134`. It destroys the pointer selected from `EngineCtx+0x55158`, allocates a 0x14-byte object, +and calls `int_stack_construct@0x407410`. The object starts with a 0x100-dword buffer, a 0x100-dword +growth quantum, and top index -1. The unobserved paired operations are +`op_0x138_push_int_stack@0x421a40`, whose `int_stack_push@0x408860` grows and appends, and +`op_0x139_try_pop_int_stack@0x4297a0`, which removes `data[top]` and writes a success flag. +On an empty pop the release handler still writes the reused incoming `EngineCtx *` local to `out_value`; +that output is implementation garbage unless success is nonzero. + +The storage has a native bounds bug worth preserving as documentation, not as portable memory corruption. +Construction, scene reset, and destruction iterate exactly ten queue pointers at `+0x55130` followed by ten +stack pointers at `+0x55158`. All six queue/stack handlers admit unsigned ids through 10. Queue id 10 +therefore aliases stack slot 0, while stack id 10 aliases the first dword of the numeric-glyph-style table +at `+0x55180`. Himegari's queue and stack sites use only id 0. Its sole `0x137` is +`CALLBACK_LOAD@0xf2`, immediately before the helper that rebuilds the eight retained ADV layers; no +Himegari script calls `0x138` or `0x139`. + +`op_0x241_play_movie_to_surface_at_position@0x4247e0` is an exact five-operand extension of `0x236`: + +`(movie_resource, surface_slot, movie_and_sound_flags, start_delay_ms, position_ms)`. + +It reuses the same movie-to-texture allocation, existing-render-target requirement, packed asset open, +sound routing/volume setup, and nonblocking `movie_play_configure` lifecycle. Between graph open and +configuration it calls `IMediaPosition::put_CurrentPosition(position_ms / 1000.0)`. The start-delay +operand remains distinct and is forwarded unchanged after the seek. + +The sole call, `CALLBACK_LOAD@0x189`, clarifies its purpose. Ordinary ADV movie setup writes opcode +`0x23f`'s stop position into the eight-cell global `adv_layer_movie_stop_time_ms` (`0x329e`) immediately +after `0x236`; the load callback passes that value minus one to `0x241`. This is terminal-frame +reconstruction after a numbered load, not restoration of a separately sampled live playback cursor. + +The current FFmpeg backend has no initial-position seam. A faithful implementation should add a synchronous +pre-play seek for both video and audio, discard keyframe preroll until the requested timestamp, then rebase +the existing decoder pacing/completion timeline at that point. Simply decoding from zero until +`stop_time_ms-1` would make load latency proportional to the movie length; seeking video without matching +audio would replay stale audio during restoration. Once that bounded decoder primitive exists, the rest of +`0x241` can delegate to the existing `0x236` host/surface lifecycle. + ### Movie-mask transition `0x24d` and tiled-surface edge `0x248` (2026-07-29) The adjacent opcode numbers do not represent one subsystem. `op_0x248_set_tiled_surface_edge_length` @@ -1686,17 +1726,25 @@ retain the value for state parity without physically reproducing the tiling. `(native stop time in milliseconds) / operand 12`, retiming the complete movie to the requested transition duration. 3. `movie_texture_allocate_transition_mask@0x415d90` allocates an operand-7 by operand-8 byte mask and - initializes it to zero when operand 9 is one, or 255 otherwise. + initializes it to zero when operand 9 is one, or 255 otherwise. While mask mode is active, + `movie_texture_renderer_receive_sample@0x4628d0` does not upload ordinary color pixels: for every + bottom-up RGB24 pixel it copies byte `+1`, the decoded **green channel**, directly into the mask. 4. `gfx_movie_mask_transition_register@0x47f560` records a type-1 retained command keyed by operand 1. The record identifies the old/source range (operands 3 and 4), scratch movie surface, mask mode, start delay, and destination rectangle `{x=operand 5, y=operand 6, width=operand 7, height=operand 8}`. -The retained surface-command compositor's type-1 branch captures the old and new object sets, updates the -byte mask from decoded movie frames, and composites the two captures per pixel within that rectangle. It -keeps its blocking/redraw state set until the movie completes, at which point it clears the command and -mask. Operand 1 is the immediately preceding new `draw-texture` range key; operand 3 is the old range that -the script erases after its `0x21c`/`0x224` presentation boundary. +`retained_gfx_surface_command_composite@0x47fbc0` captures the old and new object sets for type 1. +`gfx_apply_movie_transition_mask@0x476e50` preserves source RGB while applying each mask byte to packed +pixel alpha inside the requested rectangle. The exact 32-bit operation leaves `source_argb & 0x00ffffff` +unchanged and sets the output alpha byte to the high byte of +`((source_argb >> 8) * mask_byte)`; this includes the native packed-integer carry from the high color bytes +and should not be rounded into a generic `alpha * luminance / 255` formula. The 16-bit path performs the +analogous 4-bit-alpha operation, and pixels outside the rectangle copy unchanged. The compositor keeps its +blocking/redraw state set while the scratch movie exists. At completion it flips the mask to the terminal +fill (mode 1: 255; other modes: 0), clears the active command, and releases the blocking lifetime. Operand 1 +is the immediately preceding new `draw-texture` range key; operand 3 is the old range that the script erases +after its `0x21c`/`0x224` presentation boundary. Both shipped calls are in DEBUG.BIN. They use scratch slots 45/46, old ranges 10/20, new keys 11/21, mode zero, zero delay, and a 1000 ms duration. Resource `0x325e` is `TEST.AGF`; its `00 00 01 BA` @@ -1704,10 +1752,13 @@ header identifies an MPEG program stream despite the extension. One call uses a -184, confirming that the position operands are not unsigned dimensions. Retail TITLE cannot enter this post-exit developer menu, although the port deliberately exposes DEBUG through its F4 diagnostic route. -The implementation boundary is consequently larger than ordinary movie playback or a scalar crossfade: -the port needs movie-frame luminance/mask extraction, old/new range capture, per-pixel composition, and -blocking lifetime cleanup. The two handlers and their exact helpers/global are named and commented in -the saved `/v2` Ghidra image. +The implementation boundary is consequently larger than ordinary movie playback or a scalar crossfade, +but no mask heuristic remains: the source is specifically the decoded green byte. The port needs a +movie-frame-to-mask publication path, old/new range capture, per-pixel alpha composition, and +movie-completion-driven blocking cleanup. The software compositor can serve as the correctness oracle; +the GPU path can fall back while a type-1 command is active, as it already does for whole-screen +transitions. The handlers, sample worker, compositor, and exact helpers are named and commented in the +saved `/v2` Ghidra image. **`0x248` port implementation (2026-07-29):** `GfxState` retains the complete signed dword with native zero initialization. The setter neither publishes a retained mutation nor rebuilds surfaces that already @@ -3642,7 +3693,9 @@ the port: - `op_0x13a_register_numeric_glyph_style@0x421ab0` takes `(style_index, surface_slot, atlas_x, atlas_y, digit_width, digit_height)`. It accepts style indices `0..10` and stores the remaining five operands in the 20-byte record at - `EngineCtx+0x55180+style_index*0x14`; an invalid index raises the standard script error. The corpus has + `EngineCtx+0x55180+style_index*0x14`; an invalid index raises the standard script error. Scene reset + clears only 200 bytes here, exactly ten records. Handler-admitted record 10 starts at `+0x55248` and + aliases the return-stack table. All literal Himegari registrations use indices 0..9. The corpus has 74 registrations in 24 scripts, including eight in `DRAWCHP.BIN`. - `op_0x23b_draw_decimal_glyphs@0x424190` takes `(base_handle, style_index, value, x, y, digit_capacity, flags)`. It first erases the destination handle @@ -3654,9 +3707,10 @@ the port: them for the field HUD's turn/control/mana/level/HP/SP/FS values. This path creates ordinary retained graphics objects, so the existing atlas decode and compositor are -the correct backend; it is not an immediate `GodotAdvHost.DrawTexture` raster operation. The port now -models all 11 EngineCtx style records in `GfxState` and implements both dispatches. Each `0x23b` call erases -its full destination-handle capacity and then uses the ordinary +the correct backend; it is not an immediate `GodotAdvHost.DrawTexture` raster operation. The port models +all eleven handler-addressable indices as safe independent `GfxState` records rather than reproducing +native index 10's adjacent-memory overwrite. Each `0x23b` call erases its full destination-handle capacity +and then uses the ordinary `BindDraw` path for every displayed digit, preserving surface replacement, z-order, and compositor effects. The absent unit and weapon names are a separate layout-compute gap. `DRAWCHP.BIN` does populate both @@ -3799,11 +3853,13 @@ combat state; the unified profile service now handles them through the ordinary The DEBUGMAP symptom "selected unit can wait on its origin, but has no blue reachable tiles and cannot move" is caused by the only three effectful gaps in `MVSEEK.BIN`, not by `CALCSCOPE` or FIELD input. AGE -provides 11 context-owned integer FIFO slots: +admits queue ids `0..10`, but scene reset physically constructs ten context-owned FIFO pointers: - `op_0x132_reset_int_queue@0x4217d0` validates `queue_id <= 10`, destroys any existing object in the selected slot, and allocates a fresh 0x1c-byte FIFO. `int_queue_construct@0x4074c0` allocates 0x100 dwords, uses another 0x100 dwords as its growth quantum, and zeros the read/end/high-water indices. + The ten-entry initialization loop covers `EngineCtx+0x55130..+0x55157`; admitted id 10 therefore + aliases integer-stack slot 0 at `+0x55158`. - `op_0x133_enqueue_int@0x4218d0` validates the slot and calls `int_queue_enqueue@0x408930`. The helper appends at the end, first compacting consumed entries when possible or growing storage when necessary. - `op_0x134_try_dequeue_int@0x429620` writes `(success=1, value)` and advances the read index when the FIFO @@ -3817,8 +3873,9 @@ origin valid but left `0x134`'s zero-initialized success local unchanged, so the first loop test. This exactly explained why clicking the occupied tile still reached Wait while neither reachable overlays nor movement targets existed; `ATSEEK` was blocked identically. -The port now retains 11 VM-lifetime integer FIFO slots and implements reset/enqueue/try-dequeue with native -signed-dword behavior. It diagnoses invalid or never-reset slots; shipped scripts always reset queue 0 +The port retains all eleven handler-addressable queue ids as safe independent VM-lifetime slots rather than +reproducing id 10's adjacent stack overwrite, and implements reset/enqueue/try-dequeue with native +signed-dword behavior. It diagnoses ids above 10 or never-reset slots; shipped scripts always reset queue 0 first. On empty dequeue it writes `success=0` and retains the value destination rather than reproducing the native handler's unusable implementation-pointer value. Focused tests cover independent slots, FIFO order, signed values, empty reads, and reset replacement. Real-script tests seed a bounded passable grid and prove diff --git a/docs/global-reference.md b/docs/global-reference.md index d45ce9f..b22cd0f 100644 --- a/docs/global-reference.md +++ b/docs/global-reference.md @@ -1,7 +1,7 @@ # Global Variable Reference (generated) -4278 globals (446 curated, 3832 auto shape-inferred). Source of truth: `vm-map/globals.toml`. +4278 globals (447 curated, 3831 auto shape-inferred). Source of truth: `vm-map/globals.toml`. ## choice-output @@ -61,6 +61,7 @@ | `0x6d3` | story_event_flags | high | investigation | Shared one-based story/event flag bank. Progression, recruitment, item, and stage logic read or write individual cells; STINIT object prerequisite ids are decremented by one before SETOBJ tests this array. | | `0x2e49` | character_voice_suppressed | high | investigation | Base of the per-character voice enable/suppress settings. INITCONFIG zero-fills all 13 cells and registers each with the shared profile service; LOADCONFIG restores them. CONFIG indexes the table to preview a character voice and write 0/1. ROOM reads cell 0 before assigning its selected greeter's greeting/farewell voice ids, so the port's former scalar interpretation of zero-int-range (writing 13 into the base cell) suppressed those voices on every natural boot. This names the script-visible setting array without choosing a persistence backend for op 0x1a2/0x1a3. | | `0x3239` | adv_layer_surface_slots | high | investigation | Shared ADV graphics-layer surface-slot registry. Scene setup initializes the three banks. The CG loader uses the primary slot for a fresh retained object, alternates between columns zero and one when replacing an already-bound layer, and uses column two for the transition/crossfade surface and cleanup. The corpus has 2,657 table-base accesses across 309 scripts. Columns: 0=primary_surface_slot, 1=alternate_surface_slot, 2=transition_surface_slot. | +| `0x329e` | adv_layer_movie_stop_time_ms | high | investigation | Per-ADV-layer movie stop positions. The common movie-layer loader writes opcode 0x23f's stop time immediately after opcode 0x236 opens the graph. CALLBACK_LOAD passes stop_time_ms-1 to opcode 0x241 so a numbered load reconstructs the movie layer at its terminal frame; this is not a separately sampled live playback cursor. | | `0x453b` | training_action_text | high | investigation | TRINIT's six-string row for each of 21 training/sexual-magic actions. TRAIN renders columns 0..2 for the action description and cost/reward summary, or columns 3..5 for the unmet-condition hint. Columns: 0=description_line_1, 1=description_line_2, 2=description_line_3, 3=locked_hint_line_1, 4=locked_hint_line_2, 5=locked_hint_line_3. | | `0x65ce` | skill_acquired_flags | high | investigation | Persistent acquired-skill flags. ADDSKILL sets the selected skill after resolving the unit's equipped-skill slots; FORT checks the flag before granting a skill; CHMENU combines it with skill_change_catalog_eligible to build the available skill-change catalog. | | `0x673c` | party_slot_flags | high | investigation | Per-party-slot state flags for slots 0..99. UNITECH creates the initial unit by setting slot 2 to 0x13; CALCARR counts slots whose flags intersect 0x6, and CHMENU includes slots with bit 1 set. Exact meanings of the remaining bits are not yet classified. | @@ -360,7 +361,6 @@ | `0x712` | — | low | auto-shape | array | | `0x3276` | — | low | auto-shape | array | | `0x328a` | — | low | auto-shape | array | -| `0x329e` | — | low | auto-shape | array | | `0x32b2` | — | low | auto-shape | array | | `0x32c6` | — | low | auto-shape | array | | `0x32da` | — | low | auto-shape | array | diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index f24c19e..78b09ce 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -337,9 +337,9 @@ Implemented with domain-preserving addressed-array access, native signed 32-bit ### 0x132 `reset-int-queue` (reset-int-queue, argc 1) - **summary:** (queue_id) - destroy any existing queue in the selected engine slot and replace it with an empty integer FIFO. Valid queue ids are 0..10. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x132_reset_int_queue@0x4217d0 fetches queue_id, rejects values above 10, invokes the existing object's virtual destructor, allocates 0x1c bytes, and calls int_queue_construct@0x4074c0. The constructor allocates 0x100 dwords, sets capacity and growth quantum to 0x100, and zeros the read/end/high-water indices. The only corpus sites are ATSEEK@0x32 and MVSEEK@0x145, immediately before packing and enqueueing the origin coordinate for their flood fills. +- **evidence:** Ghidra /v2: op_0x132_reset_int_queue@0x4217d0 fetches queue_id, rejects values above 10, invokes the existing object's virtual destructor, allocates 0x1c bytes, and calls int_queue_construct@0x4074c0. The constructor allocates 0x100 dwords, sets capacity and growth quantum to 0x100, and zeros the read/end/high-water indices. scene_context_init_reset@0x40b560 constructs exactly ten queue pointers at EngineCtx+0x55130 before the ten stack pointers at +0x55158, so admitted queue id 10 aliases stack slot 0. The only corpus sites are ATSEEK@0x32 and MVSEEK@0x145, both using id 0 before their flood fills. -Implemented as 11 VM-lifetime queue slots. Reset replaces the selected queue with an empty FIFO pre-sized to the native 0x100-dword initial capacity; invalid ids halt with a diagnostic. +Implemented as 11 safe logical queue slots so the handler-admitted 0..10 script ABI cannot corrupt adjacent host state. Reset replaces the selected queue with an empty FIFO pre-sized to the native 0x100-dword initial capacity; invalid ids halt with a diagnostic. Native physically owns only ten independent slots and its id-10 address aliases stack slot 0. ### 0x133 `enqueue-int` (enqueue-int, argc 2) - **summary:** (queue_id, value) - append one integer to the selected engine FIFO, compacting consumed entries or growing its storage when required. @@ -365,6 +365,29 @@ Implemented as FIFO TryDequeue: nonempty writes success=1 plus the signed dword; - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x136_handler@0x429730 fetches operand 2 as an unsigned bit index, rejects values >=32 through the native script-error path, fetches operand 1, and writes value & ~(1 << index). HIDEWIN.BIN clears index 1 at 0x154 after testing mask 0x2. +### 0x137 `reset-int-stack` (reset-int-stack, argc 1) +- **summary:** (stack_id) - destroy the selected native integer LIFO and replace it with an empty stack. +- **grounding:** source=investigation, confidence=high +- **depended on by:** 0x138, 0x139 +- **evidence:** Ghidra /v2: op_0x137_reset_int_stack@0x421940 destroys the pointer at EngineCtx+0x55158+id*4, allocates 0x14 bytes, and calls int_stack_construct@0x407410. The constructor allocates 0x100 dwords, records a 0x100-dword growth quantum, and initializes top=-1. scene_context_init_reset@0x40b560 constructs exactly ten entries [0,10), while the handler checks only id<=10; id 10 therefore lands at +0x55180, the numeric-glyph-style table. Corpus: CALLBACK_LOAD@0xf2 is the sole site and resets stack 0 before rebuilding the eight retained ADV layers. + +AGE owns ten independent stack pointers at EngineCtx+0x55158 and reconstructs all ten on every scene-context reset. The handler's unsigned range check accidentally admits id 10 as well as 0..9; that address aliases the first dword of the numeric-glyph-style table rather than an eleventh stack. Himegari uses only id 0. As with the port's queue and numeric-style families, a portable implementation should expose the handler-admitted id 10 as safe independent logical state rather than reproduce native adjacent-memory corruption. + +### 0x138 `push-int-stack` (push-int-stack, argc 2) +- **summary:** (stack_id, value) - push one signed integer onto the selected native LIFO, growing its dword buffer by 0x100 entries when full. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x137 +- **depended on by:** 0x139 +- **evidence:** Ghidra /v2: op_0x138_push_int_stack@0x421a40 admits stack ids <=10 and calls int_stack_push@0x408860. The helper grows capacity by the stored 0x100-dword quantum when top+2 reaches capacity, increments top, and stores the value. The ten-slot/id-10 alias caveat is documented on op 0x137. No Himegari script calls this opcode. + +### 0x139 `try-pop-int-stack` (try-pop-int-stack, argc 3) +- **summary:** (stack_id, out_success, out_value) - pop the most recently pushed integer and write success=1, or write success=0 when empty. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x137, 0x138 +- **evidence:** Ghidra /v2: op_0x139_try_pop_int_stack@0x4297a0 reads top at stack+0x10; when top>=0 it reads data[top] and decrements top, then writes success and value. When top<0, the local holding the value was never replaced from the incoming EngineCtx pointer, yet operand 3 is still written. No Himegari script calls this opcode. + +On success native writes the removed value. On empty, the release handler still writes its reused `this`/EngineCtx local into out_value, so that cell is implementation garbage and must not be consumed unless out_success is nonzero. A portable compatibility implementation should preserve out_value on failure rather than expose a host pointer. The ten-slot/id-10 alias caveat is documented on op 0x137. + ### 0x191 `absolute-value` (absolute-value, argc 2) - **summary:** Write the signed 32-bit absolute value of operand 2 to operand 1. - **grounding:** source=investigation, confidence=high @@ -760,12 +783,12 @@ The handler uses an alpha step of 16 and timer interval=argument when argument < - **evidence:** Ghidra /v2: op_0x131_handler@0x4295e0 calls the settings getter with `message:MesWinAlpha` and writes the result. HISTORY.BIN and the shared ADV redraw path compute (16-value)<<4 for the control-strip alpha. ### 0x13a `register-numeric-glyph-style` (u0041F3A0, argc 6) -- **summary:** Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height). +- **summary:** Register a decimal-glyph atlas style as (surface slot, source x/y, digit width/height); the handler admits style indices 0..10. - **grounding:** source=investigation, confidence=high - **depended on by:** 0x23b -- **evidence:** Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to the selected five-dword record at EngineCtx+0x55180 after enforcing style_index<11. Corpus: 74 sites in 24 scripts; DRAWCHP has eight registrations immediately before its stylized HUD-number draws. +- **evidence:** Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to EngineCtx+0x55180+style_index*0x14 after enforcing style_index<11. scene_context_init_reset@0x40b560 clears 200 bytes at +0x55180, exactly ten records; record 10 therefore overlaps ret_stack_a at +0x55248. Corpus: 74 sites in 24 scripts; all literal style registrations are 0..9. -The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. An index outside [0,10] raises the engine's script error. +The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. Scene reset clears only ten physical records (200 bytes, indices 0..9); the admitted index 10 begins at EngineCtx+0x55248 and aliases the return-stack table. Himegari's literal registrations use only 0..9. The port safely models all eleven handler-addressable logical records without reproducing the overflow. ### 0x141 `set-message-window-alpha` (set-message-window-alpha, argc 1) - **summary:** (value) - write the configured `message:MesWinAlpha` value read by opcode 0x131. Himegari's CONFIG menu bounds the value to 0..16; larger values make the inverse-width opacity bar shorter. @@ -960,7 +983,7 @@ Implemented through IHost.PlayModalMovieToSurface. Its operand uses the same nat - **summary:** (command_key)(target_slot)(range_a_start)(range_a_count)(range_b_start)(range_b_count)(delay_ms)(duration_ms) — queue a type-0 timed alpha transition command in the separate ctx+0x414 command map. This is render-target/surface presentation state, not an object affine matrix. The render frame composites the two handle ranges into target_slot and ramps alpha 0->1 after delay over duration. - **grounding:** source=investigation, confidence=high - **depends on:** 0x20c, 0x21c, 0x1c7, 0x1cc -- **depended on by:** 0x20c, 0x21c, 0x21d +- **depended on by:** 0x20c, 0x21c, 0x21d, 0x24d - **evidence:** Ghidra handler 0x423620 -> gfx_queue_surface_alpha_transition@0x47f440. Record fields: type +0=0, start +4=0, delay +8=arg7, duration +0xc=arg8, slot +0x10=arg2, range A +0x14/+0x1c=args3/4, range B +0x18/+0x20=args5/6. gfx_render_frame@0x47fbc0 initializes start from retained-gfx owner+0xb550 (EngineCtx+0x51b64) and consumes type 0 as an alpha ramp. SC0000 executes one shared-helper site at 0x129e7. ### 0x224 `clear-gfx-command-queue` (clear-gfx-command-queue, argc 0) @@ -1030,6 +1053,7 @@ Port status (2026-07-28): implemented as an independent retained-object scale cy ### 0x236 `play-movie-to-surface` (play-movie-to-surface, argc 4) - **summary:** (packed_resource_id)(surface_slot)(movie_flags)(start_delay_ms) - synchronously open a universal packed SYS4INI/AAI movie and construct its DirectShow graph, then start asynchronous frame delivery into the retained destination surface after the requested service-tick delay. The opcode itself is non-blocking: the VM advances to the next instruction. SC0000's 0x33 and BTL's 0x2axx/0x2bxx MVB ids are already absolute base-catalog indexes. - **grounding:** source=investigation, confidence=high +- **depended on by:** 0x241, 0x24d - **evidence:** Ghidra /v2 op_0x236_play_movie_to_surface@0x423ee0 fetches operand 1 and passes it unchanged to movie_to_texture_open_asset_graph@0x463e20, which passes it unchanged to asset_open_indexed_entry@0x44f390. The opener directly indexes the flat base table or selected AAI table and has no scene input. The handler leaves playback pending through movie_play_configure@0x4625e0; movie_start_pending_after_sync_delay@0x4633b0 records the first service tick and calls the shared IMediaControl::Run worker after operand 4's delay, giving non-modal playback the same DirectShow first-sample preroll as 0x20f. SC0000 native operand capture and exact 0x13c8->0x13d1 trace prove nonblocking behavior. BTL's live 0x2b21 site supplies 0x2af1/0x2af5/0x2bca/0x2bd8/0x2bde, the exact base entries MVB001/MVB004/MVB914/MVB958/MVB955. The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface without replacing its resource identity/color key, opens operand 1 through the native universal packed-id reader, builds FilterGraph/IGraphBuilder/IMediaControl/IMediaPosition/IMediaEvent/IBasicAudio, and presents bottom-up RGB samples through the movie texture renderer. Operand 3 selects movie/sound routing policy: bits 0x10000/0x20000/0x40000/0x80000 force mute/music/SE/voice routes, otherwise set:DependMovieSound selects the normal movie route; SC0000's low value 2 is retained as native movie mode state. Godot maps those categories to audio buses and sends FFmpeg-decoded timestamped stereo float PCM through a per-playback AudioStreamGenerator. The first image is latched before audio start; video cadence is rebased to its first decoded PTS for both audio-bearing and video-only streams, while audio retains every sample from its own timestamp zero. Operand 4 delays the pending graph start in native service ticks. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches both decoder and audio output. The port type-checks the selected ResolvePacked record as MPEG without adding a scene base or fallback. @@ -1055,7 +1079,7 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b - **depends on:** 0x13a - **evidence:** Ghidra /v2: op_0x23b_draw_decimal_glyphs@0x424190 reads the five-dword style record at EngineCtx+0x55180, erases the destination handle range, and calls gfx_object_bind_draw once per displayed digit. Corpus: 147 sites in 24 scripts; DRAWCHP contains 22 calls for the field HUD, including turn/control/mana/level/HP/SP/FS values visible as blank in the DEBUGMAP discrepancy. -First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. Invalid or unregistered style indices raise the engine's script error. +First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. The handler admits style index 10 even though native physically clears only records 0..9; see op 0x13a's adjacent-memory alias note. Indices above 10 and unregistered styles raise the engine's script error. ### 0x23c `sample-frame-time` (sample-frame-time, argc 0) - **summary:** Explicitly shift the shared current retained-presentation timestamp to the previous field, then sample the native monotonic millisecond clock as the new current timestamp. The active outer engine tick normally performs the same update; this opcode does not create a distinct timer or fixed-rate animation cadence. @@ -1072,10 +1096,19 @@ For each fixed slot in [42,1000), the handler stops/releases the movie-to-textur ### 0x23f `query-surface-stop-time-ms` (u00422930, argc 2) - **summary:** (out_stop_time_ms)(surface_slot) — query the DirectShow stop position retained by a loaded movie surface, convert seconds to integer milliseconds by truncating toward zero, and write -1 when the movie slot is empty. Port-only host decoder failure is modeled as an explicitly completed, zero-duration movie. - **grounding:** source=investigation, confidence=high +- **depended on by:** 0x241 - **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. Native has no meaningful answer for a port host that cannot build a graph for a valid shipped MPEG. Port safety extension: op 0x236 normalizes missing host metadata to stop time 0 and marks decoder failure completed, while a truly empty movie slot still returns -1. +### 0x241 `play-movie-to-surface-at-position` (play-movie-to-surface-at-position, argc 5) +- **summary:** (packed_resource_id)(surface_slot)(movie_flags)(start_delay_ms)(position_ms) - perform the same non-modal movie-to-retained-surface open as opcode 0x236, seek the graph to the requested millisecond position, then arm playback. +- **grounding:** source=investigation, confidence=high +- **depends on:** 0x236, 0x23f +- **evidence:** Ghidra /v2: op_0x241_play_movie_to_surface_at_position@0x4247e0 uses the same 0x478-byte movie-to-texture object, destination render-target check, packed asset open, sound-route/volume path, and movie_play_configure worker as op 0x236. Between open and configure it calls movie+0x414 IMediaPosition vtable+0x20 put_CurrentPosition with operand5/1000.0; operand4 is then passed unchanged as the start delay. Corpus: sole site CALLBACK_LOAD@0x189 receives the layer resource, surface and flags from globals 0x3276/0x3239/0x328a, delay 0, and global 0x329e[layer]-1. The ordinary ADV creation path writes op 0x23f stop_time_ms into 0x329e immediately after op 0x236. + +The seek is applied through IMediaPosition::put_CurrentPosition before movie_play_configure records the flags and start delay. CALLBACK_LOAD does not restore a separately sampled live playback cursor: the ADV setup path stores opcode 0x23f's stop time in global array 0x329e, and load passes stop_time_ms-1. The shipped use therefore reconstructs the movie layer at its terminal frame after a numbered load. The portable FFmpeg seam currently has no initial-position parameter; implementation requires a pre-play seek with keyframe-preroll discard and matching audio positioning, then can reuse the existing 0x236 surface binding, routing, completion, and delayed-start lifecycle. + ### 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. - **grounding:** source=investigation, confidence=high @@ -1103,9 +1136,12 @@ Port status (2026-07-29): implemented as retained signed-dword graphics configur - **evidence:** Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, unchanged packed operand, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its only relevant distinction is mode-1 gfx_surface_mode1_ctor, a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits logical dimensions into ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes/uploads regions; gfx_tiled_surface_blit@0x4316b0 subdivides logical source rectangles. Corpus literals include FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A. The former claim that only 0x249 bypasses scene normalization was wrong because native never performs scene normalization for 0x1f9 either. ### 0x24d `play-movie-mask-transition` (play-movie-mask-transition, argc 12) -- **summary:** Open a movie into a scratch surface, retime it to the requested duration, use its decoded frames as a per-pixel mask, and register a blocking retained-surface transition from an old object range to a newly drawn range within the supplied rectangle. +- **summary:** Open a movie into a scratch surface, retime it to the requested duration, copy each decoded frame's green channel into a byte-per-pixel mask, and register a blocking retained-surface transition from an old object range to a newly drawn range within the supplied rectangle. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x24d_play_movie_mask_transition@0x424db0 opens operand 10 through movie_to_texture_open_asset_graph, configures delay operand 11, sets IMediaPosition::put_Rate to native_stop_time_ms / operand12_ms, allocates a width*height byte mask through movie_texture_allocate_transition_mask@0x415d90, and calls gfx_movie_mask_transition_register@0x47f560 with operands 1-9/11. The retained compositor's type-1 branch applies the movie-updated mask between the old and new object ranges and holds its blocking dirty state until playback completes. Both DEBUG.BIN sites use TEST.AGF (an MPEG program stream), delay 0, and duration 1000 ms. +- **depends on:** 0x236, 0x223 +- **evidence:** Ghidra /v2: op_0x24d_play_movie_mask_transition@0x424db0 opens operand 10 through movie_to_texture_open_asset_graph, configures delay operand 11, sets IMediaPosition::put_Rate to native_stop_time_ms / operand12_ms, allocates a width*height byte mask through movie_texture_allocate_transition_mask@0x415d90, and calls gfx_movie_mask_transition_register@0x47f560 with operands 1-9/11. movie_texture_renderer_receive_sample@0x4628d0's mask-active branch copies byte +1 of every bottom-up RGB24 sample pixel, i.e. its green channel, directly into that mask. The retained compositor's type-1 branch applies the movie-updated mask between the old and new object ranges and holds its blocking dirty state until playback completes. Both DEBUG.BIN sites use TEST.AGF (an MPEG program stream), delay 0, and duration 1000 ms. + +The RGB24 sample callback copies the bottom-up green byte verbatim. In the native 32-bit mask compositor, captured RGB remains unchanged and the output alpha byte is the high byte of `((source_argb >> 8) * mask_byte)`; this records the exact packed-integer operation, including its low-color carry, rather than approximating it as a scalar luminance crossfade. Mode 1 initializes the mask to 0 and completes at 255; all other modes initialize at 255 and complete at 0. ### 0x24e `set-gfx-animation-service-flags` (set-gfx-animation-service-flags, argc 1) - **summary:** Replace the retained graphics animation-service flags with operand 1. BTL brackets combat presentation with values 1 and 0; GAMECLEAR uses 3 and 0. @@ -1959,20 +1995,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting - **grounding:** source=kelebek, confidence=low - **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table. -### 0x137 `u0041F1C0` (u0041F1C0, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - -### 0x138 `u0041F2B0` (u0041F2B0, argc 2) -- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. -- **grounding:** source=kelebek, confidence=low -- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table. - -### 0x139 `u0041F310` (u0041F310, argc 3) -- **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. -- **grounding:** source=kelebek, confidence=low -- **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table. - ### 0x13b `u0041F440` (u0041F440, argc 7) - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **grounding:** source=kelebek, confidence=low @@ -2209,10 +2231,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting - **grounding:** source=kelebek, confidence=low - **evidence:** Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table. -### 0x241 `u00422B80` (u00422B80, argc 5) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x244 `u00416360` (u00416360, argc 0) - **summary:** Broader AGE-catalog compatibility stub; the port currently traces and skips it. - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-b-framework.md b/docs/phase-b-framework.md index 8e4deb9..5c9db2f 100644 --- a/docs/phase-b-framework.md +++ b/docs/phase-b-framework.md @@ -1045,6 +1045,36 @@ checking, and the Himegari-targeted threaded `SELFTEST OK`. **NEXT:** rerank `0x137`, `0x241`, and developer-only `0x24d` ×2 before choosing the next slice. +**FINAL OPCODE INVESTIGATION COMPLETE (2026-07-29):** all three remaining contracts are now +implementation-ready. + +- `0x137(stack_id)` replaces one of AGE's ten integer LIFOs with an empty 0x100-dword stack. Its exact + unobserved siblings are `0x138(stack_id,value)` push and `0x139(stack_id,out_success,out_value)` pop. + AGE's handlers accidentally admit id 10 even though reset constructs only slots 0..9, aliasing adjacent + EngineCtx storage; Himegari uses only id 0. The sole call is `CALLBACK_LOAD@0xf2`. +- `0x241(movie,surface,flags,delay_ms,position_ms)` is `0x236` plus + `IMediaPosition::put_CurrentPosition` before playback configuration. CALLBACK_LOAD passes the + per-layer stop time recorded by `0x23f`, minus one, so its shipped job is restoring the terminal movie + frame after numbered load rather than resuming an arbitrary saved cursor. +- `0x24d` retains the already-decoded type-1 movie transition ABI. The remaining uncertainty is closed: + AGE copies each decoded RGB24 pixel's green byte into the transition mask, then applies that byte to + captured pixel alpha inside the supplied rectangle. Completion flips the mask to its terminal fill and + releases the blocking command. Both calls remain DEBUG.BIN-only. + +The implementation order is correspondingly clear. First land the complete `0x137`-`0x139` stack family: +it is a small VM-only analogue of the existing FIFO and closes the normal load-path gap. Next land `0x241` +with one reusable FFmpeg initial-seek primitive for video and audio, including keyframe-preroll discard. +Finish with `0x24d` as the dedicated diagnostic compositor slice: publish movie green-channel masks, +capture old/new ranges, use the software renderer as the per-pixel oracle, and fall the GPU path back while +the type-1 command is active. + +The inventory remains 3 opcodes / 4 instructions because this was investigation only. Opcode, global, and +EngineCtx sources and generated references are current; `/v2` names/comments and the applied EngineCtx +fields are saved. + +**NEXT:** implement the coherent `0x137`-`0x139` integer-stack family, then `0x241`, then the DEBUG-only +`0x24d` compositor slice. + ## Later Phase B breadth **INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static diff --git a/engine/Age.Engine/Model/GfxState.cs b/engine/Age.Engine/Model/GfxState.cs index c0012e2..b8647ee 100644 --- a/engine/Age.Engine/Model/GfxState.cs +++ b/engine/Age.Engine/Model/GfxState.cs @@ -922,7 +922,8 @@ public sealed class GfxState } } - /// Op 0x13a: replace one of the native engine's eleven decimal-glyph atlas styles. + /// Op 0x13a: replace one of eleven safely isolated handler-addressable styles. + /// Native physically clears ten records; its admitted index 10 aliases return-stack storage. public bool RegisterNumericGlyphStyle(int styleIndex, int surfaceSlot, int atlasX, int atlasY, int digitWidth, int digitHeight) { diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 2a48759..2f48145 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -1617,7 +1617,7 @@ public sealed class VirtualMachine return pc + 1; } case "u0041EF00": - case "reset-int-queue": // 0x132 (queue_id): destroy/recreate one of 11 native FIFO slots + case "reset-int-queue": // 0x132: 11 safe logical slots; native's admitted id 10 aliases stack 0 { int queueId = unchecked((int)Read(a[0])); if ((uint)queueId >= (uint)_intQueues.Length) diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index aadee4b..7d4600d 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -45,7 +45,7 @@ INFERRED: dict[int, dict] = { 0x10b: dict(name='map-mouse-button', category='input', noop=False, confidence='high', source='investigation', summary='(button_slot)(physical_button) - map a physical mouse button to a logical button slot; polling emits the slot as logical action slot+4.'), 0x10c: dict(name='map-keyboard-scancode', category='input', noop=False, confidence='high', source='investigation', summary='(action)(dik_scan_code) - map a DirectInput keyboard scan code to a logical input action.'), 0x10d: dict(name='consume-mouse-wheel-delta', category='input', noop=False, confidence='high', source='investigation', summary='(out) - return the accumulated signed mouse-wheel delta and clear it.'), - 0x13a: dict(name='register-numeric-glyph-style', category='draw', noop=False, confidence='high', source='investigation', summary='Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height).'), + 0x13a: dict(name='register-numeric-glyph-style', category='draw', noop=False, confidence='high', source='investigation', summary='Register a decimal-glyph atlas style as (surface slot, source x/y, digit width/height); the handler admits style indices 0..10.'), 0x140: dict(name='coroutine-label-yield', category='control', noop=False, confidence='med', source='investigation', summary="(out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls AGERC host command 8 through g_pfnAgercHostCallback(8, ctx[0x54fe8], &{name,sub,in}); writes the returned PC-like value to operand 1. In SC0000 label_462 'ループ開始' (@0x46d): `out=G[0x6be]=LABEL('J',G[0x6be])`; loop runs the intro-setup body (incl. call label_125bd = slot-table fill G[0x3239..0x324e]=4..11) and jmps back until out==G[0x6c3] (a per-scene exit-PC immediate) → mov aba5c 0 → content. The gate G[0xaba5c]==1 that opens this loop is NATIVE scene-entry state (no script sets it to 1). PORT = HOST-MODEL IMPLEMENTED: synthesize the ADV scene-entry gate, run the LABEL/J setup body once, then return the structurally discovered per-scene terminal; do not emulate the service dialog. See engine-re.md §Scene-coroutine framework."), 0x143: dict(name='run-mounted-append-autoruns', category='control', noop=False, confidence='high', source='investigation', summary='() - enqueue record zero from every mounted nonzero AAI selector in ascending selector order, then execute those packed scripts serially before resuming the caller.'), 0x199: dict(name='yield-adv-coroutine', category='control', noop=False, confidence='high', source='investigation', summary='Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow.'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index fdd6b43..2ec2ab4 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -303,6 +303,21 @@ name = "anti_tamper_fp" type = "int" note = "anti-tamper (import fn ptr / result)" [[field]] +offset = 0x55130 +name = "int_queue_slots" +type = "void*" +note = "base of ten scene-context-owned integer FIFO pointers used by ops 0x132-0x134; their <=10 validation bug makes id 10 alias int_stack_slots[0]" +[[field]] +offset = 0x55158 +name = "int_stack_slots" +type = "void*" +note = "base of ten scene-context-owned integer LIFO pointers used by ops 0x137-0x139; their <=10 validation bug makes id 10 alias numeric_glyph_styles[0]" +[[field]] +offset = 0x55180 +name = "numeric_glyph_styles" +type = "int" +note = "base of ten 20-byte decimal-glyph atlas records used by ops 0x13a/0x23b; opcode validation also admits one out-of-range record" +[[field]] offset = 0x55248 name = "ret_stack_a" type = "void*" diff --git a/vm-map/globals.toml b/vm-map/globals.toml index bef7eac..70a48d7 100644 --- a/vm-map/globals.toml +++ b/vm-map/globals.toml @@ -1297,6 +1297,17 @@ source = "investigation" confidence = "high" depends_on = ["0x62450", "0x62452", "0x62455"] +[[global]] +address = "0x329e" +name = "adv_layer_movie_stop_time_ms" +category = "data-table" +type = "int[8]" +value_domain = "-1 for an empty movie surface, otherwise opcode 0x23f's truncated stop position in milliseconds" +usage = "Per-ADV-layer movie stop positions. The common movie-layer loader writes opcode 0x23f's stop time immediately after opcode 0x236 opens the graph. CALLBACK_LOAD passes stop_time_ms-1 to opcode 0x241 so a numbered load reconstructs the movie layer at its terminal frame; this is not a separately sampled live playback cursor." +source = "investigation" +confidence = "high" +depends_on = ["0x3239", "0x62450"] + [[global]] address = "0x62424" name = "adv_gfx_resource_id" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 4542c98..1d37787 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -2687,8 +2687,8 @@ noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra /v2: op_0x132_reset_int_queue@0x4217d0 fetches queue_id, rejects values above 10, invokes the existing object's virtual destructor, allocates 0x1c bytes, and calls int_queue_construct@0x4074c0. The constructor allocates 0x100 dwords, sets capacity and growth quantum to 0x100, and zeros the read/end/high-water indices. The only corpus sites are ATSEEK@0x32 and MVSEEK@0x145, immediately before packing and enqueueing the origin coordinate for their flood fills." -details = "Implemented as 11 VM-lifetime queue slots. Reset replaces the selected queue with an empty FIFO pre-sized to the native 0x100-dword initial capacity; invalid ids halt with a diagnostic." +evidence = "Ghidra /v2: op_0x132_reset_int_queue@0x4217d0 fetches queue_id, rejects values above 10, invokes the existing object's virtual destructor, allocates 0x1c bytes, and calls int_queue_construct@0x4074c0. The constructor allocates 0x100 dwords, sets capacity and growth quantum to 0x100, and zeros the read/end/high-water indices. scene_context_init_reset@0x40b560 constructs exactly ten queue pointers at EngineCtx+0x55130 before the ten stack pointers at +0x55158, so admitted queue id 10 aliases stack slot 0. The only corpus sites are ATSEEK@0x32 and MVSEEK@0x145, both using id 0 before their flood fills." +details = "Implemented as 11 safe logical queue slots so the handler-admitted 0..10 script ABI cannot corrupt adjacent host state. Reset replaces the selected queue with an empty FIFO pre-sized to the native 0x100-dword initial capacity; invalid ids halt with a diagnostic. Native physically owns only ten independent slots and its id-10 address aliases stack slot 0." [[opcode.semantics.args]] i = 1 @@ -2808,23 +2808,24 @@ observed_types = ["imm", "l-int", "l-ptr"] [[opcode]] op = 0x137 -label = "u0041F1C0" +label = "reset-int-stack" argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u0041F1C0" -category = "unknown" -summary = "" +name = "reset-int-stack" +category = "compute" +summary = "(stack_id) - destroy the selected native integer LIFO and replace it with an empty stack." +details = "AGE owns ten independent stack pointers at EngineCtx+0x55158 and reconstructs all ten on every scene-context reset. The handler's unsigned range check accidentally admits id 10 as well as 0..9; that address aliases the first dword of the numeric-glyph-style table rather than an eleventh stack. Himegari uses only id 0. As with the port's queue and numeric-style families, a portable implementation should expose the handler-admitted id 10 as safe independent logical state rather than reproduce native adjacent-memory corruption." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x137_reset_int_stack@0x421940 destroys the pointer at EngineCtx+0x55158+id*4, allocates 0x14 bytes, and calls int_stack_construct@0x407410. The constructor allocates 0x100 dwords, records a 0x100-dword growth quantum, and initializes top=-1. scene_context_init_reset@0x40b560 constructs exactly ten entries [0,10), while the handler checks only id<=10; id 10 therefore lands at +0x55180, the numeric-glyph-style table. Corpus: CALLBACK_LOAD@0xf2 is the sole site and resets stack 0 before rebuilding the eight retained ADV layers." [[opcode.semantics.args]] i = 1 -role = "" +role = "stack_id" observed_types = ["imm"] [[opcode]] @@ -2836,13 +2837,13 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "register-numeric-glyph-style" category = "draw" -summary = "Register one of 11 decimal-glyph atlas styles as (surface slot, source x/y, digit width/height)." -details = "The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. An index outside [0,10] raises the engine's script error." +summary = "Register a decimal-glyph atlas style as (surface slot, source x/y, digit width/height); the handler admits style indices 0..10." +details = "The five-dword definition is stored at EngineCtx+0x55180+style_index*0x14. Opcode 0x23b consumes it to turn an integer into retained draw objects, one atlas cell per decimal digit. Scene reset clears only ten physical records (200 bytes, indices 0..9); the admitted index 10 begins at EngineCtx+0x55248 and aliases the return-stack table. Himegari's literal registrations use only 0..9. The port safely models all eleven handler-addressable logical records without reproducing the overflow." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to the selected five-dword record at EngineCtx+0x55180 after enforcing style_index<11. Corpus: 74 sites in 24 scripts; DRAWCHP has eight registrations immediately before its stylized HUD-number draws." +evidence = "Ghidra /v2: op_0x13a_register_numeric_glyph_style@0x421ab0 writes operands 2..6 to EngineCtx+0x55180+style_index*0x14 after enforcing style_index<11. scene_context_init_reset@0x40b560 clears 200 bytes at +0x55180, exactly ten records; record 10 therefore overlaps ret_stack_a at +0x55248. Corpus: 74 sites in 24 scripts; all literal style registrations are 0..9." [[opcode.semantics.args]] i = 1 @@ -6404,7 +6405,7 @@ abi_source = "kelebek+decode-validated" name = "draw-decimal-glyphs" category = "draw" summary = "Draw an integer as decimal glyph objects from a style registered by opcode 0x13a." -details = "First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. Invalid or unregistered style indices raise the engine's script error." +details = "First erase digit_capacity objects beginning at base_handle. Then split value by signed division/modulo 10 and bind at most digit_capacity retained objects using adjacent digit-width cells from the registered atlas. Flags bit 0 zero-pads, bit 1 centers the used digits, and bit 2 left-aligns them; with no alignment bit the value is right-aligned in the capacity. The handler admits style index 10 even though native physically clears only records 0..9; see op 0x13a's adjacent-memory alias note. Indices above 10 and unregistered styles raise the engine's script error." noop_headless = false source = "investigation" confidence = "high" @@ -6508,43 +6509,44 @@ observed_types = ["imm", "g-int", "l-int"] [[opcode]] op = 0x241 -label = "u00422B80" +label = "play-movie-to-surface-at-position" argc = 5 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u00422B80" -category = "unknown" -summary = "" +name = "play-movie-to-surface-at-position" +category = "draw" +summary = "(packed_resource_id)(surface_slot)(movie_flags)(start_delay_ms)(position_ms) - perform the same non-modal movie-to-retained-surface open as opcode 0x236, seek the graph to the requested millisecond position, then arm playback." +details = "The seek is applied through IMediaPosition::put_CurrentPosition before movie_play_configure records the flags and start delay. CALLBACK_LOAD does not restore a separately sampled live playback cursor: the ADV setup path stores opcode 0x23f's stop time in global array 0x329e, and load passes stop_time_ms-1. The shipped use therefore reconstructs the movie layer at its terminal frame after a numbered load. The portable FFmpeg seam currently has no initial-position parameter; implementation requires a pre-play seek with keyframe-preroll discard and matching audio positioning, then can reuse the existing 0x236 surface binding, routing, completion, and delayed-start lifecycle." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "" +source = "investigation" +confidence = "high" +depends_on = [0x236, 0x23f] +evidence = "Ghidra /v2: op_0x241_play_movie_to_surface_at_position@0x4247e0 uses the same 0x478-byte movie-to-texture object, destination render-target check, packed asset open, sound-route/volume path, and movie_play_configure worker as op 0x236. Between open and configure it calls movie+0x414 IMediaPosition vtable+0x20 put_CurrentPosition with operand5/1000.0; operand4 is then passed unchanged as the start delay. Corpus: sole site CALLBACK_LOAD@0x189 receives the layer resource, surface and flags from globals 0x3276/0x3239/0x328a, delay 0, and global 0x329e[layer]-1. The ordinary ADV creation path writes op 0x23f stop_time_ms into 0x329e immediately after op 0x236." [[opcode.semantics.args]] i = 1 -role = "" +role = "universal packed movie resource id" observed_types = ["l-ptr"] [[opcode.semantics.args]] i = 2 -role = "" +role = "surface slot" observed_types = ["l-ptr"] [[opcode.semantics.args]] i = 3 -role = "" +role = "movie flags and sound-route policy" observed_types = ["l-ptr"] [[opcode.semantics.args]] i = 4 -role = "" +role = "start delay ms" observed_types = ["imm"] [[opcode.semantics.args]] i = 5 -role = "" +role = "initial playback position ms" observed_types = ["l-int"] [[opcode]] @@ -6652,12 +6654,13 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "play-movie-mask-transition" category = "draw" -summary = "Open a movie into a scratch surface, retime it to the requested duration, use its decoded frames as a per-pixel mask, and register a blocking retained-surface transition from an old object range to a newly drawn range within the supplied rectangle." +summary = "Open a movie into a scratch surface, retime it to the requested duration, copy each decoded frame's green channel into a byte-per-pixel mask, and register a blocking retained-surface transition from an old object range to a newly drawn range within the supplied rectangle." +details = "The RGB24 sample callback copies the bottom-up green byte verbatim. In the native 32-bit mask compositor, captured RGB remains unchanged and the output alpha byte is the high byte of `((source_argb >> 8) * mask_byte)`; this records the exact packed-integer operation, including its low-color carry, rather than approximating it as a scalar luminance crossfade. Mode 1 initializes the mask to 0 and completes at 255; all other modes initialize at 255 and complete at 0." noop_headless = false source = "investigation" confidence = "high" -depends_on = [] -evidence = "Ghidra /v2: op_0x24d_play_movie_mask_transition@0x424db0 opens operand 10 through movie_to_texture_open_asset_graph, configures delay operand 11, sets IMediaPosition::put_Rate to native_stop_time_ms / operand12_ms, allocates a width*height byte mask through movie_texture_allocate_transition_mask@0x415d90, and calls gfx_movie_mask_transition_register@0x47f560 with operands 1-9/11. The retained compositor's type-1 branch applies the movie-updated mask between the old and new object ranges and holds its blocking dirty state until playback completes. Both DEBUG.BIN sites use TEST.AGF (an MPEG program stream), delay 0, and duration 1000 ms." +depends_on = [0x236, 0x223] +evidence = "Ghidra /v2: op_0x24d_play_movie_mask_transition@0x424db0 opens operand 10 through movie_to_texture_open_asset_graph, configures delay operand 11, sets IMediaPosition::put_Rate to native_stop_time_ms / operand12_ms, allocates a width*height byte mask through movie_texture_allocate_transition_mask@0x415d90, and calls gfx_movie_mask_transition_register@0x47f560 with operands 1-9/11. movie_texture_renderer_receive_sample@0x4628d0's mask-active branch copies byte +1 of every bottom-up RGB24 sample pixel, i.e. its green channel, directly into that mask. The retained compositor's type-1 branch applies the movie-updated mask between the old and new object ranges and holds its blocking dirty state until playback completes. Both DEBUG.BIN sites use TEST.AGF (an MPEG program stream), delay 0, and duration 1000 ms." [[opcode.semantics.args]] i = 1 @@ -8679,37 +8682,63 @@ evidence = "Not observed in Himegari's script corpus; ABI label/argc come from K [[opcode]] op = 0x138 -label = "u0041F2B0" +label = "push-int-stack" argc = 2 observed_in_himegari = false abi_source = "kelebek" [opcode.semantics] -name = "u0041F2B0" -category = "unknown" -summary = "Broader AGE-catalog compatibility stub; the port currently traces and skips it." +name = "push-int-stack" +category = "compute" +summary = "(stack_id, value) - push one signed integer onto the selected native LIFO, growing its dword buffer by 0x100 entries when full." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table." +source = "investigation" +confidence = "high" +depends_on = [0x137] +evidence = "Ghidra /v2: op_0x138_push_int_stack@0x421a40 admits stack ids <=10 and calls int_stack_push@0x408860. The helper grows capacity by the stored 0x100-dword quantum when top+2 reaches capacity, increments top, and stores the value. The ten-slot/id-10 alias caveat is documented on op 0x137. No Himegari script calls this opcode." + +[[opcode.semantics.args]] +i = 1 +role = "stack_id" +observed_types = [] + +[[opcode.semantics.args]] +i = 2 +role = "value" +observed_types = [] [[opcode]] op = 0x139 -label = "u0041F310" +label = "try-pop-int-stack" argc = 3 observed_in_himegari = false abi_source = "kelebek" [opcode.semantics] -name = "u0041F310" -category = "unknown" -summary = "Broader AGE-catalog compatibility stub; the port currently traces and skips it." +name = "try-pop-int-stack" +category = "compute" +summary = "(stack_id, out_success, out_value) - pop the most recently pushed integer and write success=1, or write success=0 when empty." +details = "On success native writes the removed value. On empty, the release handler still writes its reused `this`/EngineCtx local into out_value, so that cell is implementation garbage and must not be consumed unless out_success is nonzero. A portable compatibility implementation should preserve out_value on failure rather than expose a host pointer. The ten-slot/id-10 alias caveat is documented on op 0x137." noop_headless = false -source = "kelebek" -confidence = "low" -depends_on = [] -evidence = "Not observed in Himegari's script corpus; ABI label/argc come from Kelebek's AGE table." +source = "investigation" +confidence = "high" +depends_on = [0x137, 0x138] +evidence = "Ghidra /v2: op_0x139_try_pop_int_stack@0x4297a0 reads top at stack+0x10; when top>=0 it reads data[top] and decrements top, then writes success and value. When top<0, the local holding the value was never replaced from the incoming EngineCtx pointer, yet operand 3 is still written. No Himegari script calls this opcode." + +[[opcode.semantics.args]] +i = 1 +role = "stack_id" +observed_types = [] + +[[opcode.semantics.args]] +i = 2 +role = "out_success" +observed_types = [] + +[[opcode.semantics.args]] +i = 3 +role = "out_value" +observed_types = [] [[opcode]] op = 0x13b