Document final opcode investigations

This commit is contained in:
gamer147
2026-07-29 15:08:05 -04:00
parent 79b8d53fcc
commit 7ab2ed87da
11 changed files with 259 additions and 95 deletions

View File

@@ -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