From 26b6c2137c6fcd7bfbd674b178a2012449a208c4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 20 Jul 2026 12:06:02 -0400 Subject: [PATCH] Reverse opcode 0x1ad save resume boundary --- docs/engine-ctx-reference.md | 5 +- docs/engine-re.md | 102 ++++++++++++++++++++++------------ docs/opcode-reference.md | 49 ++++++++-------- docs/phase-a-slice-plan.md | 39 +++++++++++-- docs/scjump-progression.md | 7 ++- docs/tools-reference.md | 2 +- tools/age_opcodes_himegari.py | 3 +- vm-map/engine-ctx.toml | 11 +++- vm-map/opcodes.toml | 50 ++++++++--------- 9 files changed, 166 insertions(+), 102 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index bb828ab..11e40d5 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -23,12 +23,12 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x51b78` | `anim_clock_elapsed` | `int` | global anim clock elapsed (op 0x238 zeroes) | | `0x51b7c` | `anim_clock_duration` | `int` | global anim clock total duration (op 0x238 sets) | | `0x52bd4` | `surfaces` | `void*` | surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) allocate | -| `0x53d14` | `cur_ctx_index` | `uint` | current gfx-object / script-context index (curCtx); indexes 0x78-byte records | +| `0x53d14` | `cur_ctx_index` | `uint` | current script-context index (curCtx); indexes 0x78-byte coroutine/frame records | | `0x53d28` | `frame_codebase` | `void*` | current frame codebase (PC = codebase + off*4) | | `0x53d2c` | `frame_pc` | `int` | current frame PC column (op = *(0x53d2c + curCtx*0x78)) | | `0x53d60` | `ctx_record_base` | `void*` | 0x78-byte context-record array base (coroutine/script contexts) | | `0x53d64` | `frame_script_resource_id` | `uint` | raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key | -| `0x53d88` | `cmd_type_table` | `int` | per-object cmd-type column base (write *(0x53d88 + curCtx*0x78)) | +| `0x53d88` | `frame_instruction_word_count` | `int` | current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4 | | `0x550fc` | `message_skip_display_enabled` | `int` | persistent all-message Skip state returned by op 0x19a for the ADV control-strip active overlay | | `0x55104` | `auto_message_enabled` | `int` | ADV Auto mode flag; op 0x1b6 reads, op 0x1b7 writes, adv_input_service_poll consumes | | `0x55110` | `text_history_recording_suppressed` | `uint` | high bit suppresses ADV retained-history index/text/metadata/voice recording; op 0x1bb writes 0x80000000 or zero, and HISTORY.BIN brackets itself with disable/enable | @@ -65,6 +65,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x6dbf0` | `voice_bgm_duck_control_flags` | `uint` | transient mask replaced by op 0x1cf; bit 0 suppresses automatic voice-triggered BGM attenuation | | `0x6dbf4` | `message_skip_queued_voice_id` | `int` | latest voice resource deferred by op 0xc4 while run_state_flags message-skip bit is active | | `0x6dbf8` | `message_skip_queued_voice_arg` | `int` | second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero | +| `0x9928c` | `save_frame_boundary_index` | `int` | highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it | | `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) | | `0xa0cc0` | `screen_w` | `int` | screen width (640) | | `0xa0cc4` | `screen_h` | `int` | screen height (480) | diff --git a/docs/engine-re.md b/docs/engine-re.md index 10e5a3a..5385693 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -115,8 +115,9 @@ decode when you reverse one (the generic name is a floor, not a final). > `switch_program /v2/range_00400000.bin`) before doing anything** — `run_script_inline` runs against the > GUI's active program, so a wrong-program script would mutate/measure garbage. -**Other confirmed engine-context offsets** (`ctx`/`esi`): `+0x53d14` = current gfx-object index; -`+0x53d88` = per-object cmd-type table (stride `0x78` = 120 bytes); operand-fetch helper = +**Other confirmed engine-context offsets** (`ctx`/`esi`): `+0x53d14` = current script-context index; +`+0x53d88 + index*0x78` = current decoded instruction length in dwords (the interpreter advances PC by +that value times four); operand-fetch helper = **`vm_operand_fetch`@`0x41b940`** (thiscall, `ecx=ctx`, arg = operand index → returns the operand value); **`vm_operand_write`@`0x425fb0`** = the counterpart store; **`vm_operand_lvalue`@`0x415f30`** = the companion index/pointer accessor. @@ -137,7 +138,7 @@ Ghidra name is the record. **These `ctx` offsets are now a typed struct (2026-07-09).** The canonical field map is `vm-map/engine-ctx.toml` → generated `docs/engine-ctx-reference.md`; a `run_script_inline` pass created an `EngineCtx` Ghidra struct and retyped **all 419 dispatch handlers' `this` to `EngineCtx *`**, so they -decompile `ctx->cur_ctx_index` / `ctx->cmd_type_table` / `ctx->run_state_flags` instead of `param_1 + 0x…` +decompile `ctx->cur_ctx_index` / `ctx->frame_instruction_word_count` / `ctx->run_state_flags` instead of `param_1 + 0x…` (verified: `sleep_op_0xc8`, `gfx_op_0x215_query_source_slot`). Add a field: edit `engine-ctx.toml`, run `engine_ctx_build.py --build`, re-apply the struct. (The VM global bank `G[…]` is separate — `globals.toml`.) @@ -145,7 +146,7 @@ decompile `ctx->cur_ctx_index` / `ctx->cmd_type_table` / `ctx->run_state_flags` ## Findings -### op `0x1a2` (`u00428010`) is a GRAPHICS command-buffer op — NOT save, NOT decision→scene (2026-07-07) +### op `0x1a2` (`u00428010`) is a value/descriptor registration op — NOT save, NOT decision→scene (corrected 2026-07-20) The SCJUMP slice assumed `u00428010` resolved a decision value to a scene. **That premise is wrong**, and pinning the *real* handler via the dispatch table above corrects two layers of confusion: @@ -155,14 +156,15 @@ and pinning the *real* handler via the dispatch table above corrects two layers `ctx[0x26e3f]=0x427fb0`). Op `0x1ac` is a **save-path** op — its handler formats `%s\SAVE%2.2d.DAT` (format string `0x571e70`) and is multi-operand. Reading the raw VA gave the wrong opcode. -- **Op `0x1a2`'s real handler = `FUN_0042d360`** (`= ctx[0x26c93+0x1a2] = ctx[0x26e35]`), argc 1. It: - sets the **current gfx-object cmd-type to 3** (`*(ctx+0x53d88 + ctx[0x53d14]*0x78) = 3`), fetches - operand 1, formats a key with `"%c%8.8x"` (format string `0x5714e0`) of `(3, operand)`, and calls - `FUN_0042cf70(key, &operand)`. This is a **graphics command-buffer registration op**, not save and - not scene-load. +- **Op `0x1a2`'s real handler = `FUN_0042d360`** (`= ctx[0x26c93+0x1a2] = ctx[0x26e35]`), argc 1. It + records the generic **3-dword instruction length** at `ctx+0x53d88 + curCtx*0x78`, fetches operand 1, + formats a key with `"%c%8.8x"` (format string `0x5714e0`) from `(type 3, operand lvalue descriptor)`, + and inserts the operand's current value into the separate open-addressing table at `ctx+0x5190`. + The old graphics classification came entirely from misreading the instruction-length field as a + command type. The handler is still definitively not save or scene-load. - **Consequence — the decision→scene premise is discredited.** The FIELD snippet `lookup(0x5f0ed, 0x62ccf); mov(ptr,1); lookup(0x5f0ed, 0x62ccf); u00428010(ptr)` (next op `0x21b`, - also gfx-family) is a **graphics/UI operation**, not scene sequencing. So `u00428010` does **not** + also outside the scene loader) is a **value-registration operation**, not scene sequencing. So `u00428010` does **not** resolve decision→scene. **The real decision→scene mechanism is unidentified** — it belongs with the call-script / script-load dispatch (`name-resolution.md §1`), the next target for this loop (now armed with the dispatch table to resolve the call-script handler directly). @@ -214,7 +216,7 @@ decision→scene (scenes are just `SCxxxx.BIN` records loaded by their SYS4INI i --- -### op `0x215` (`query-gfx-object?`) is a native command-buffer op — settles the render drift as (b) (2026-07-07) +### op `0x215` (`query-gfx-object?`) is a retained gfx-object query — settles the render drift as (b) (corrected 2026-07-20) **This is the canonical account of the background/sprite "drift" bug** (background pinned off-centre / bottom-right, rest grey — `Screenshot 2026-07-06 211353.png`). It supersedes the earlier "drift = @@ -226,32 +228,31 @@ Resolved via the dispatch table (`ctx[0x26c93 + 0x215]`): the registration routi VA-drift — it lands inside the unrelated `FUN_00421090`. Same lesson as `0x1a2`: never trust a Kelebek raw VA.) `FUN_0042a0b0(ctx)` does exactly two things: -1. **`*(ctx + 0x53d88 + ctx[0x53d14]*0x78) = 5`** — writes **cmd-type 5** into the *current* gfx-object - record. A **command-buffer registration** side-effect, directly parallel to op `0x1a2` (`FUN_0042d360`) - writing cmd-type 3. So `0x215` is part of the gfx command-buffer subsystem, not a pure query. +1. **`*(ctx + 0x53d88 + ctx[0x53d14]*0x78) = 5`** — records this opcode's generic encoded length + (one opcode dword plus two dwords per operand). This is interpreter bookkeeping, not a gfx side effect. 2. **`out = FUN_0047f280(FUN_0041b940(2))`** — `FUN_0041b940(2)` fetches operand 2 (the bytecode handle key); `FUN_0047f280` is a **`std::map::find`** over an engine-internal associative registry, returning the mapped value or **`0xffffffff` (not-found)**; `FUN_00425fb0(1, out)` writes it to operand 1. That - registry is **populated by sibling gfx ops** — op `0x1a2`'s handler builds a `"%c%8.8x"` key and calls - `FUN_0042cf70`, an open-addressing hash **insert** into the same kind of store. + registry is populated by the retained-object draw/geometry workers. Op `0x1a2`'s descriptor-value + table is separate and does not populate this map. -**(a) vs (b) — the verdict is (b).** The value `0x215` returns is **native command-buffer state**: "has a -gfx object already been registered under this handle?" (`≥0` = existing → use its slot; `-1` = new). That +**(a) vs (b) — the verdict is (b).** The value `0x215` returns is **native retained-object state**: "has a +gfx object already been created under this handle, and what is its source slot?" (`≥0` = existing → use its slot; +`-1` = absent). That state lives in the engine's own registry, maintained by the gfx ops, **not in the VM global bank**. So **seeding story-state globals cannot reproduce it** — the drift is *not* the Phase-B state-divergence problem. Stubbing `0x215` returns a constant → `label_12649`'s slot-select always takes one branch → every draw collapses onto slot 0 → the anchor-preserve math measures foreign-sized textures → cumulative drift. **Why the prior "state-divergence" conclusion was wrong.** It was grounded in `capture_gfx_objects.py`, -which polled the object-*record* array (`[esi+0x53d64]`) at ~2/s and saw only 3 persistent UI objects, "0 -CG objects." But (i) the branch is driven by the **map lookup** (a different structure the poll never -observed), and (ii) command-buffer records are **transient** — a 2/s poll can't prove CG records weren't -used. Absence in that capture ≠ absence of the native path. +which polled `[esi+0x53d64]` at ~2/s and mistook the engine's `0x78`-byte **script-context records** for gfx +objects. The branch is driven by the retained-object map at `ctx+0x46614+0x408`, a different structure the +probe never observed. Absence in that capture therefore says nothing about the native gfx-object path. **The fix is tractable and Frida-free.** (b) does *not* mean an opaque native state machine. The subsystem -is a **modelable data structure**: an object-record array (slot / geometry / cmd-type per object) plus a +is a **modelable data structure**: retained object records (slot / geometry / draw state) behind a handle→object registry (a `std::map`). Geometry and draw workers lazily populate that retained-object map; -query and erase workers read/remove the same entries. Op `0x1a2` also maintains an operand-descriptor hash, +query and erase workers read/remove the same entries. Op `0x1a2` also maintains a descriptor-value hash, but that is a separate structure and is not what op `0x215` queries. The opcode-level source of truth is `vm-map/opcodes.toml`. @@ -310,7 +311,7 @@ handle array** (native entry-state a cold single-scene harness skips), NOT a sto **The loop iterator `op 0x140` is a native video-service call — not statically reproducible.** Handler = **`0x4299c0`** (dispatch `ctx[0x9b74c]=0x4299c0`; created+typed `EngineCtx*`+annotated; Kelebek `u0041F9C0` is -VA-drift). It writes gfx cmd-type 9, copies operand-2/3 strings (`"LABEL"`, `"J"`) + operand-4 int, calls +VA-drift). It records the generic 9-dword instruction length, copies operand-2/3 strings (`"LABEL"`, `"J"`) + operand-4 int, calls **`(*DAT_005c6018)(8, ctx[0x54fe8], &{str,str,int})`**, and writes the returned PC-like value back to operand 1 (SC0000: `G[0x6be]`). `DAT_005c6018` is a **runtime-resolved function pointer** (all 6 xrefs are READs, no static writer) — the engine's **native video / transition / timing service**: `FUN_00405740` (a screen- @@ -350,17 +351,17 @@ stubbed. The real video-service timing remains intentionally unmodeled. returned -1 and skipped it. The corrected source-slot query now reaches `0x1f7(handle,10)` object erasure and `0x1fa(slot)` surface release; the booted regression ends with no visible `0x37` object. -#### gfx command-buffer — op contract table (2026-07-07, full family reversed) +#### gfx opcode contract table (corrected 2026-07-20; full family reversed) -Every gfx op shares one shape: **write a `cmd-type` into the current object record** (`*(ctx + 0x53d88 + -ctx[0x53d14]*0x78) = `), fetch operands via `FUN_0041b940(i)` (1-based; `docs` = the `0x1a2` variant -uses `FUN_00415f30`), then either **SET** object fields (call a native worker `FUN_0047xxxx`) or **QUERY** -object fields (write results back to output operands via `FUN_00425fb0(i, val)`). Handlers resolved through +Every handler first writes its encoded instruction length in dwords to the current **script-frame** record +(`*(ctx + 0x53d88 + ctx[0x53d14]*0x78) = 1 + 2*argc`), then fetches operands via +`FUN_0041b940(i)` (1-based). Gfx handlers then either **SET** retained-object fields (call a native worker +`FUN_0047xxxx`) or **QUERY** them (write results back via `FUN_00425fb0(i, val)`). The length write is generic +interpreter bookkeeping and is not part of the gfx contract. Handlers resolved through the dispatch table (`ctx[0x26c93+op]`); all renamed in the Ghidra project `gfx_op_0x_`. -| op | handler | cmd | dir | argc | contract | +| op | handler | words | dir | argc | contract | |---|---|---|---|---|---| -| `0x1a2` | `0x42d360` | 3 | set | 1 | operand-descriptor hash insert: key `"%c%8.8x"(3, operand-desc)` → `FUN_0042cf70`; separate from the retained object map | | `0x1f7` | `0x422270` | 5 | erase | 2 | retained-object erase: `op2>1` → `gfx_object_erase_range(op1,op2)` erases `[op1,op1+op2)`, else `gfx_object_erase(op1)` | | `0x1fa` | `0x4224a0` | 3 | set | 1 | release **surface slot** `ctx+0x52bd4[op1]` (vtbl free) + `FUN_00474e40(op1)` | | `0x1ff` | `0x4227b0` | 9 | set | 4 | 3 int→float params on obj op1 → `FUN_0047e800(op1,f2,f3,f4)` | @@ -513,13 +514,13 @@ preserving colorkey/tint/opacity behavior and never deriving opacity from transf Decoding the two already-named clock/start ops (dispatch table → `0x234`@`0x00423da0`, `0x238`@`0x004240e0`; both annotated) and grepping the SC0000 opening settles the animation model and confirms the opening exercises it: -- **`0x238 set_anim_clock` (argc 1, cmd-type 3):** `ctx+0x51b78 = 0` (elapsed), `ctx+0x51b7c = operand1` +- **`0x238 set_anim_clock` (argc 1, 3-dword instruction):** `ctx+0x51b78 = 0` (elapsed), `ctx+0x51b7c = operand1` (total duration). **A GLOBAL, NON-BLOCKING clock** — not per-object. The op only *configures* the clock; it does **not** loop/wait. The native render loop advances this clock each frame and interpolates *all* animating objects. Its own plate comment states the payoff: "our port can drive animation in the host's per-frame loop while the VM is parked at wait-for-input; no VM/host frame-lockstep." → **validates the wall-clock-tween architecture directly.** SC0000: `set-anim-clock(G[0x624bb])` @`0x123bd`, `set-anim-clock(0x190=400)` @`0x13858`. -- **`0x234 anim_start` (legacy mnemonic; argc 5, cmd-type 0xb):** following worker +- **`0x234 anim_start` (legacy mnemonic; argc 5, 11-dword instruction):** following worker `gfx_object_set_rotation_cycle` (`0x47f060`) into `gfx_object_anim_interpolate` (`0x473ed0`) corrects its ABI to `(handle)(period_ms)(axis_x)(axis_y)(axis_z)`. Period is `obj+0x228`, axis is `obj+0x244..0x24c`, and the frame-clock consumer applies @@ -570,7 +571,7 @@ was **wrong**, and it came from trusting our own `Age.Cli gfx` oracle (which exe CG draws as "slot 0"). Verified against native code + the raw bytecode: - **`draw-texture` (op `0x1fb`, handler `gfx_op_0x1fb_draw_bind`@`0x422510`) is a RETAINED bind, not a blit.** It - writes cmd-type `0x11` and calls **`gfx_object_bind_draw`@`0x47e870`**, which on the object keyed by `handle` + records its 17-dword instruction length and calls **`gfx_object_bind_draw`@`0x47e870`**, which on the object keyed by `handle` (operand 1) sets: `flag|=1` (visible), `obj+4 = source SLOT index`, `obj+8..0x14 = source rect`, `obj+0x24/28/2c = position`. Its plate comment (prior RE) already states the key fact: the object stores the **slot INDEX — a live ref to `surface[slot]`, resolved each frame at render — NOT a texture snapshot.** Objects @@ -602,7 +603,8 @@ Handler resolved via the dispatch table (`ctx[0x26c93+0xc8]` = `param_1[0x26d5b] native confirmation that the engine paces animation in its per-frame loop, not by blocking. - **Operand unit = MILLISECONDS.** `duration < 10` fast-paths through import `[0x56f0b8]`; every real scene sleep (`100`/`750`/`1000` in SC0000) is `≥ 10` → the timer-arm path. -- The handler also writes gfx **cmd-type 3** into the current object record (`ctx+0x53d88+curidx*0x78`) and runs +- The handler also records its generic **3-dword instruction length** in the current script-frame record + (`ctx+0x53d88+curidx*0x78`) and runs two **anti-tamper** checks (call `[ctx+0x5512c]`; a rotate-checksum compare of `ctx+0x55120/0x55124`; `__CxxThrowException` on mismatch — integrity work piggybacked on a hot op). Neither is needed by our model. @@ -946,7 +948,7 @@ classified as the retained-object `+0x2d0` field setter described above; `0x23d/ ### Movie-to-surface opcode `0x236` (2026-07-11) The exact ABI is `play-movie-to-surface(resource_id, surface_slot, movie_flags, sync_mask)`. Handler -`op_0x236_play_movie_to_surface@0x423ee0` is command type 9 and requires the destination texture to exist. +`op_0x236_play_movie_to_surface@0x423ee0` records a 9-dword instruction length and requires the destination texture to exist. It allocates/reuses a 0x478-byte `CMovieToTexture` object, binds the D3D device/backing texture, opens operand 1 through `asset_open_indexed_entry`, constructs a DirectShow FilterGraph, and starts it. The graph queries `IGraphBuilder`, `IMediaControl`, `IMediaPosition`, `IMediaEvent`, and `IBasicAudio`; its custom @@ -1203,6 +1205,32 @@ the missing native governor and remains fast without teleporting between blockin op `0x88` state reaches the host before the following cadence yields. Validation is engine 168/168, zero-warning Godot build, and threaded `SELFTEST OK`. +### Opcode `0x1ad` marks the numbered-save resume-frame boundary (2026-07-20) + +Opcode `0x1ad` is a zero-operand persistence marker, not an input reset or modal-UI synchronization call. +Its real dispatch handler is `op_0x1ad_mark_save_resume_frame@0x416b70`. Aside from recording the generic +one-dword instruction length at `ctx+0x53d88 + curCtx*0x78`, it performs one semantic write: +`ctx->save_frame_boundary_index = ctx->cur_ctx_index` (`ctx+0x9928c = ctx+0x53d14`). + +`context_state_serialize@0x40d320` consumes the mark for numbered-save layouts 2 and 3. A negative mark +falls back to the current frame; otherwise the serializer copies script frames `0..mark` inclusive and +forces the marked frame's saved return entry to `-1`, making that frame the top/terminal activation after +load. `op_0x2_exit_or_return_frame@0x417940` clears the mark when normal return unwinds below it. Scene/context reset also +initializes it to `-1`. Thus the mark selects both the highest saved activation and the frame at which a +loaded game resumes; the opcode itself neither reads nor writes a file. + +Corpus placement agrees with the native dataflow: 1,928 executions appear across 304 scripts. Roots such as +`CAMP`, `FIELD`, and `FORT` mark their main frame near entry; SC0000's six sites are its startup path and the +return paths from `HISTORY`, `MENU`, `HIDEWIN`, and `INPUTNAME`. Those calls re-establish the enclosing ADV +frame as the safe numbered-save resume point after modal/nested scripts finish. + +**Port implication:** the current port-owned JSON session snapshot persists only global integer/string banks +and deliberately has no active-frame or numbered-save backend. Treating `0x1ad` as a no-op is behaviorally +neutral only under that present limitation; counting it as faithfully implemented would be misleading. Its +real implementation belongs in the future unified save architecture, where the VM must serialize the active +`ExecFrame` chain and remember which frame is the resume boundary. This is the same architectural deferral as +the already-deferred profile/read-state work, not a reason to invent a seed or offset-specific shortcut. + ### ADV read-message Skip and shared `RT.DAT` history (2026-07-18) Read-message Skip is backed by an engine-owned `ReadTextDB`, not a VM-global flag and not ordinary numbered diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index e10c5c7..261baa9 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -222,9 +222,9 @@ This also names the whole call graph statically (build/callscript-names.json). ### 0x7b `coroutine-save-yield-handlers` (u0041ADB0, argc 2) -- **summary:** (handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index) + gfx cmd-type 5. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator). +- **summary:** (handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator). - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra: handler FUN_0041ebf0 (dispatch ctx[0x26c93+0x7b]) = {*(ctx+0x53d88+ctx[0x53d14]*0x78)=5; ctx[0x6da88+idx*4]=op1; ctx[0x6db28+idx*4]=op2}. Both operands are code PCs (handler labels). +- **evidence:** Ghidra: handler FUN_0041ebf0 (dispatch ctx[0x26c93+0x7b]) = {frame_instruction_word_count[idx]=5; ctx[0x6da88+idx*4]=op1; ctx[0x6db28+idx*4]=op2}. Both operands are code PCs (handler labels). ### 0x7c `coroutine-resume` (u00416A90, argc 0) - **summary:** () — scene-coroutine RESUME point. Native requires run-state bit 0x2000000 (ctx[0x6dbc8]) set — THROWS (__CxxThrowException) if unset, so it is only ever reached on a scheduler-driven re-entry, NEVER on a cold first pass (cold flow jmps over it). Restores PC=ctx[0x53d28]+ctx[0x6dbcc]*4, clears the run-bit (ctx+0xa0ce4 &= ~0x2000000), resets input/line state. SC0000 0x443 (falls into the main loop label_444). See engine-re.md §Scene-coroutine framework. @@ -256,7 +256,7 @@ This also names the whole call graph statically (build/callscript-names.json). - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra: dispatch ctx[0x26c93+0xc8]=0x420ec0; sleep_op_0xc8 + sleep_timer_arm decoded/annotated 2026-07-08. docs/engine-re.md sleep section. -Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also writes gfx cmd-type 3 + runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity). +Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also records its generic 3-dword instruction length and runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity). ### 0xd3 `begin-timed-callback-sequence` (u00425960, argc 0) - **summary:** Clear and initialize the current script frame's timed local-callback sequence. @@ -281,13 +281,23 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t ### 0x140 `coroutine-label-yield` (u0041F9C0, argc 4) - **summary:** (out)(name_str)(sub_str)(in) — scene-coroutine LOOP ITERATOR / labeled yield. Handler copies name/sub strings + the int operand and calls the NATIVE video/transition service (*DAT_005c6018)(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). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. 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 video service. See engine-re.md §Scene-coroutine framework. - **grounding:** source=investigation, confidence=med -- **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Writes gfx cmd-type 9; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service. +- **evidence:** Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Records the generic 9-dword instruction length; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service. ### 0x199 `yield-adv-coroutine` (u00414D50, argc 0) - **summary:** Yield/re-enter the registered ADV coroutine handler. The fifth standard chrome button uses this transition to enter the HIDEWIN/window-hidden flow. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x199_yield_adv_coroutine@0x416440 selects the registered coroutine yield-A or yield-B PC according to ctx+0x6dbc8, saves the current resume offset/state, and redirects the current frame PC. SC0000's x=772 ADV button invokes it; the SO001 tooltip at source x=528 reads Window hide, and the surrounding coroutine calls HIDEWIN.BIN. +### 0x1a2 `register-lvalue-value` (gfx-cmd-register, argc 1) +- **summary:** 0x1a2 (value) — register operand 1's current value under a key derived from its lvalue descriptor in the open-addressing table at ctx+0x5190. The write of 3 at ctx+0x53d88 is only this instruction's encoded dword length, not a graphics command type. This structure is separate from op 0x215's retained gfx-object map; op 0x215 does not query it. NOT save/scene. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra: handler 0x42d360 fetches operand 1's value and lvalue descriptor separately, formats the descriptor key, then calls FUN_0042cf70. By contrast op 0x215 passes ctx+0x46614 to gfx_object_query_source_slot@0x47f280, which searches the retained object map and returns obj+4. + +### 0x1ad `mark-save-resume-frame` (mark-save-resume-frame, argc 0) +- **summary:** Mark the current script context as the highest frame serialized by numbered-save layouts 2/3. The native serializer saves frames 0 through this boundary and strips the boundary frame's return target so loading resumes it as the top frame. This opcode performs no file I/O itself. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1ad_mark_save_resume_frame@0x416b70 writes decoded instruction size 1 and ctx+0x9928c=cur_ctx_index. context_state_serialize@0x40d320 uses that field (or cur_ctx_index when -1) as the inclusive frame cutoff for save layouts 2/3, serializes frames 0..cutoff, and forces the cutoff frame's saved return entry to -1. op_0x2_exit_or_return_frame@0x417940 clears the mark when unwinding below it. Corpus: 1,928 calls in 304 scripts; SC0000's six calls are at startup and immediately after HISTORY/MENU/HIDEWIN/INPUTNAME returns. + ### 0x1cc `get-adv-read-skip-state` (get-adv-read-skip-state, argc 1) - **summary:** (out) - copy the current ADV read/click-skip service state from ctx+0x6dbd4. label_1235a ORs it with 0x1c7's Ctrl/message-skip bit: zero takes 0x21c's normal transition/yield path; nonzero resets the animation service and presents the completed endpoint through 0x20c. - **grounding:** source=investigation, confidence=high @@ -299,7 +309,7 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t - **grounding:** source=investigation, confidence=high - **depends on:** 0x223, 0x1c7, 0x1cc - **depended on by:** 0x223 -- **evidence:** Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. capture_presentation_trace.py: after 0x125a6 render, 0xcb8e/0xcb98 bind and 0xd5a/0xd63/0xd73/0xd8a mode+targets execute without render; repeated gfx_render_frame begins only at 0x21c. 2026-07-10. +- **evidence:** Ghidra handler 0x417520 records the 1-dword instruction length and ORs ctx+0xa0ce4 with 0x400. capture_presentation_trace.py: after 0x125a6 render, 0xcb8e/0xcb98 bind and 0xd5a/0xd63/0xd73/0xd8a mode+targets execute without render; repeated gfx_render_frame begins only at 0x21c. 2026-07-10. SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit 0x400 parks the interpreter while gfx_render_frame repeatedly samples finite one-shot object channels and queued surface commands; op 0x224 follows after dirty state clears. Native trace proves AE001D bind, mode-1 0x203, and 0x202 targets complete in one 5 ms batch with no render, then first compose here. The port publishes and waits for visible finite one-shot channels or 0x223 commands; click forcing remains limited to the latter. @@ -310,11 +320,6 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit - **grounding:** source=investigation, confidence=high - **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. -### 0x1a2 `gfx-cmd-register` (gfx-cmd-register, argc 1) -- **summary:** 0x1a2 (value) — gfx cmd-type 3. Handler gfx_op_0x1a2_descriptor_register@0x42d360 builds a key from operand 1's lvalue descriptor and inserts its value into an open-addressing descriptor hash (vm_lvalue_descriptor_hash_insert@0x42cf70). This structure is separate from op 0x215's retained gfx-object map; op 0x215 does not query this hash. NOT save/scene. -- **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra: handler 0x42d360 fetches operand 1's value and lvalue descriptor separately, formats the descriptor key, then calls FUN_0042cf70. By contrast op 0x215 passes ctx+0x46614 to gfx_object_query_source_slot@0x47f280, which searches the retained object map and returns obj+4. - ### 0x1f7 `gfx-elem-erase` (gfx-elem-erase, argc 2) - **summary:** 0x1f7 (handle)(count) — erase retained gfx objects. Handler 0x422270 calls gfx_object_erase_range@0x47d8b0 for [handle,handle+count) when count>1, else gfx_object_erase@0x47d850. This removes entries from the same object map queried by op 0x215, so erased objects stop compositing. SC0000 uses it before op 0x1fa releases the returned surface slot. - **grounding:** source=investigation, confidence=high @@ -356,7 +361,7 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit - **evidence:** Ghidra handler 0x4228d0 packs operands 4/5 and calls worker 0x47ea00(handle,delay,duration,packed). Consumer 0x472f00: shared start +0x34; color delay/duration +0x38/+0x4c; current/target +0x60/+0x64; frame clock ctx+0xb550; bytewise integer LERP; natural or ctx+0xb55c forced completion. /v2 annotated and saved 2026-07-10. ### 0x203 `gfx-draw-color` (gfx-draw-color, argc 4) -- **summary:** 0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case. +- **summary:** 0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x4229a0; negative operands read current obj+0x60, then worker 0x47e9b0 stores op2 at obj+0x30 and ARGB at +0x60. gfx_object_composite call-site 0x47f78f passes +0x30/+0x60 directly to gfx_object_blit_d3d9; mode 1 sets D3DRS SRCALPHA/INVSRCALPHA and the packed color is the device draw modulation. Mode 2 transition setup and synchronized pixels prove 0xffffffff is identity, not solid white. SC0000 page 14 adds the mode-0 endpoint proof: after the EV052CA->EV052DA 0x223 crossfade, 0x203@0x12478 restores the base CG to mode 0 with preserved 0xffffffff; native keeps EV052DA visible while the port's tint-strength interpretation turns every texel white. @@ -380,12 +385,12 @@ SC0000 label_1235a reaches this when 0x1c7/0x1cc are zero. Native run-state bit Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is an explicit retained-state publication boundary, not a continuously visible object-store mutation. The read/message-skip branch resets the animation service then presents; the port publishes and snaps pending 0x223 state here. Normal playback branches to 0x21c, which owns repeated render/wait/resume. Headless hosts remain non-blocking. ### 0x212 `set-gfx-field64` (set-gfx-field64, argc 2) -- **summary:** 0x212 (obj_idx)(val) — gfx cmd-type 5. Handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. Sets one per-object field. See docs/engine-re.md gfx op-contract table. +- **summary:** 0x212 (obj_idx)(val) — handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. The generic instruction length is 5 dwords. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x4230c0 (dispatch ctx[0x26c93+0x212]); writes [obj+0x64]=operand2, obj from ctx+0x14d54[operand1*4]. ### 0x213 `set-gfx-xy` (set-gfx-xy, argc 3) -- **summary:** 0x213 (obj_idx)(x)(y) — gfx cmd-type 7. Handler gfx_op_0x213_set_field68_6c @0x423110: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x68)=x; *(obj+0x6c)=y (an (x,y) pair). See docs/engine-re.md gfx op-contract table. +- **summary:** 0x213 (obj_idx)(x)(y) — handler gfx_op_0x213_set_field68_6c @0x423110: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x68)=x; *(obj+0x6c)=y (an (x,y) pair). The generic instruction length is 7 dwords. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x423110; writes obj+0x68/+0x6c from operands 2/3, obj from ctx+0x14d54[operand1*4]. @@ -395,27 +400,27 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is - **evidence:** Ghidra: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280, which searches ECX+0x408 and returns object+4. gfx_object_bind_draw@0x47e870 writes the bound slot there; gfx_object_init_default@0x472810 explicitly writes zero to dword index 1. SC0000 page-58 trace: the old -1 default skipped cleanup of transform-created handle 0xcb2a, then EV050EA inherited translation (-100,0), rotation -90, and alpha 0; the native zero default makes the guard succeed and the reused object is identity/opaque. ### 0x216 `query-gfx-field?` (query-gfx-field?, argc 2) -- **summary:** 0x216 (out)(idx) — gfx cmd-type 5. Handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table. +- **summary:** 0x216 (out)(idx) — handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). The generic instruction length is 5 dwords. A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x42a0f0; reads ctx+0x46d14[operand2 * 0x14], writes operand1 via FUN_00425fb0(1,·). ### 0x217 `set-gfx-geom3` (set-gfx-geom3, argc 4) -- **summary:** 0x217 (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x217_set_geom3 @0x4231b0: SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e960. In SC0000 label_12649 it writes the anchor vector G[0x6249b/c/d] INTO the object; op 0x218 reads it back. See docs/engine-re.md gfx op-contract table. +- **summary:** 0x217 (handle)(a)(b)(c) — handler gfx_op_0x217_set_geom3 @0x4231b0 SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e960; its generic instruction length is 9 dwords. In SC0000 label_12649 it writes the anchor vector G[0x6249b/c/d] INTO the object; op 0x218 reads it back. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x4231b0 (dispatch ctx[0x26c93+0x217]); FUN_0047e960(op1,(float)op2,(float)op3,(float)op4). label_12649 sites e.g. 0x00c67 handle=G[0x62457], vec=G[0x6249b/c/d]. ### 0x218 `get-gfx-geom3?` (get-gfx-geom3?, argc 4) -- **summary:** 0x218 (handle)(out_a)(out_b)(out_c) — gfx cmd-type 9. Handler gfx_op_0x218_query_geom3 @0x42a130: GETS a stored 3-vector from object `handle` (FUN_0047f360) into out_a/b/c. In label_12649 it reads the object's anchor vector back into G[0x6249b/c/d] — a stubbed DRIVER of the render drift (stale anchor → bad centering). See docs/engine-re.md gfx op-contract table. +- **summary:** 0x218 (handle)(out_a)(out_b)(out_c) — handler gfx_op_0x218_query_geom3 @0x42a130 GETS a stored 3-vector from object `handle` (FUN_0047f360) into out_a/b/c; its generic instruction length is 9 dwords. In label_12649 it reads the object's anchor vector back into G[0x6249b/c/d] — a stubbed DRIVER of the render drift (stale anchor → bad centering). See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x42a130; FUN_0047f360(obj op1) + 3x FUN_00550850→FUN_00425fb0(2/3/4). label_12649 site 0x00c8f handle=G[0x62457] → G[0x6249b/c/d]. ### 0x219 `set-gfx-geom3-b` (set-gfx-geom3-b, argc 4) -- **summary:** 0x219 (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x219_set_geom3 @0x423240: SETS a 3-vector (int→float) on object `handle` via native worker FUN_0047e910 (sibling of 0x217, a different per-object vector). See docs/engine-re.md gfx op-contract table. +- **summary:** 0x219 (handle)(a)(b)(c) — handler gfx_op_0x219_set_geom3 @0x423240 SETS a 3-vector (int→float) on object `handle` via native worker FUN_0047e910 (sibling of 0x217, a different per-object vector); its generic instruction length is 9 dwords. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x423240 (was unanalyzed; function created this session; dispatch ctx[0x26c93+0x219]); FUN_0047e910(op1,(float)op2,(float)op3,(float)op4). ### 0x21a `get-gfx-geom3-b?` (get-gfx-geom3-b?, argc 4) -- **summary:** 0x21a (handle)(out_a)(out_b)(out_c) — gfx cmd-type 9. Handler gfx_op_0x21a_query_geom3 @0x42a1b0: GETS a stored 3-vector from object `handle` (FUN_0047f2e0) into out_a/b/c. In label_12649 it reads the object's position vector into G[0x62498/9/a] — a stubbed DRIVER of the render drift. See docs/engine-re.md gfx op-contract table. +- **summary:** 0x21a (handle)(out_a)(out_b)(out_c) — handler gfx_op_0x21a_query_geom3 @0x42a1b0 GETS a stored 3-vector from object `handle` (FUN_0047f2e0) into out_a/b/c; its generic instruction length is 9 dwords. In label_12649 it reads the object's position vector into G[0x62498/9/a] — a stubbed DRIVER of the render drift. See docs/engine-re.md gfx op-contract table. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra handler 0x42a1b0; FUN_0047f2e0(obj op1) + 3x→FUN_00425fb0(2/3/4). label_12649 site 0x00c86 handle=G[0x62457] → G[0x62498/9/a]. @@ -493,7 +498,7 @@ Native handler gfx_op_0x20c_present_frame -> gfx_render_frame @0x4820b0. This is The handler requires an existing destination texture, allocates/reuses a 0x478-byte movie-to-texture object for the surface, opens operand 1 through the native indexed-asset 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 sound route 0/1/2/3, otherwise set:DependMovieSound is used; SC0000's low value 2 is retained as native movie mode state. Operand 4 is stored as the movie sync/device mask at object+0x42c; SC0000 passes 0. Static layer preparation after 0x236 does not terminate the retained movie; 0x21c services it through EOF and subsequent surface cleanup stops/detaches it. ### 0x238 `set-anim-clock` (set-anim-clock, argc 1) -- **summary:** (duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. cmd-type 3. 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 ### 0x239 `u004223C0` (u004223C0, argc 6) @@ -506,7 +511,7 @@ The handler requires an existing destination texture, allocates/reuses a 0x478-b - **grounding:** source=kelebek, confidence=low ### 0x242 `set-gfx-field2d0` (set-gfx-field2d0, argc 2) -- **summary:** 0x242 (handle)(value) — command type 5. Get-or-create the retained gfx object and write value to obj+0x2d0. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown. +- **summary:** 0x242 (handle)(value) — get-or-create the retained gfx object and write value to obj+0x2d0; the generic instruction length is 5 dwords. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown. - **grounding:** source=investigation, confidence=high - **evidence:** Ghidra /v2: op_0x242_set_object_field2d0@0x4249d0 fetches operands 2 and 1 and calls gfx_object_set_field2d0@0x47f1a0; the worker calls gfx_object_get_or_create then stores operand 2 at returned object+0x2d0. @@ -1044,10 +1049,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it " - **summary:** — - **grounding:** source=kelebek, confidence=low -### 0x1ad `u004154F0` (u004154F0, argc 0) -- **summary:** — -- **grounding:** source=kelebek, confidence=low - ### 0x1ae `u0041CED0` (u0041CED0, argc 3) - **summary:** — - **grounding:** source=kelebek, confidence=low diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index f523540..a2a2689 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -284,12 +284,12 @@ with the rest of the screen grey). Root cause = the stubbed native op **`0x215`* slot 0 (its return drives `label_12649`'s slot-select). **The canonical decode + verdict now lives in `docs/engine-re.md` (op `0x215` section)** — don't duplicate it -here. In brief: `0x215`'s real handler `FUN_0042a0b0` (Ghidra) writes cmd-type 5 into the current gfx-object -record and returns a **`std::map::find`** over an engine-internal command-buffer registry (populated by sibling -gfx ops like `0x1a2`). That return is **native command-buffer state, not the VM global bank** → seeding +here. In brief: `0x215`'s real handler `FUN_0042a0b0` (Ghidra) records its generic 5-dword instruction length +and returns a **`std::map::find`** over the retained gfx-object registry populated by draw/geometry workers. +That return is **native retained-object state, not the VM global bank** → seeding story-state **cannot** fix it. So this is **(b) a genuine native op**, *not* (a) the Phase-B state-divergence -problem. The prior conclusion in this doc — grounded in a 2/s `capture_gfx_objects.py` poll of the object-*record* -array — was wrong: it observed the wrong structure (not the lookup map) and can't rule out transient records. +problem. The prior conclusion in this doc — grounded in a 2/s `capture_gfx_objects.py` poll that mistook +script-context records for gfx objects — was wrong: it observed the wrong structure, not the lookup map. **Resolution had TWO halves** (canonical decode in `docs/engine-re.md`, op `0x215` + "The render drift's SECOND half"; don't duplicate here): @@ -401,7 +401,7 @@ dramatic holds, not the rapid burst's pacer. **RE (Ghidra):** `sleep_op_0xc8`@`0x420ec0` is **non-blocking** — it arms a main-loop-polled timer (`sleep_timer_arm`@`0x44cff0`; start = ms tick, duration = operand). **Operand unit = milliseconds.** (Also -carries anti-tamper + a gfx cmd-type-3 write, neither needed host-side.) `0x20c` = `gfx_op_0x20c_present_frame` +carries anti-tamper + a generic 3-dword instruction-length write, neither needed host-side.) `0x20c` = `gfx_op_0x20c_present_frame` → host-implicit (our compositor presents continuously) → `noop_headless`. **Implemented:** `IHost.Sleep(long)` + VM `case "sleep"` forwarding the raw operand; the 9 non-Godot hosts no-op @@ -1977,3 +1977,30 @@ should be classified before choosing between it and the smaller two-call support Validation: all 206 engine tests pass; opcode and EngineCtx lints, vm0 RECOVER, the zero-warning Godot build, and threaded `SELFTEST OK` are clean. + +### Slice A2b-0x1ad investigation — numbered-save resume-frame marker (2026-07-20) + +The six-call SC0000 `0x1ad` cluster is now classified at high confidence. The zero-operand handler +`0x416b70` stores the current script-context index in `ctx+0x9928c`. Numbered-save serializer layouts 2/3 +use that index as the inclusive high frame, copy activations `0..mark`, and clear the marked activation's +saved return target so it becomes the top frame after load. Opcode `0x2` clears the mark when unwinding below +it. Across the corpus, 1,928 calls in 304 scripts place the marker at main-script entries and after modal +script returns; SC0000's sites are startup plus `HISTORY`/`MENU`/`HIDEWIN`/`INPUTNAME` return paths. + +No runtime implementation landed. The current `GameSession` JSON format persists global banks only and has +no active `ExecFrame` chain or numbered-save lifecycle, so `0x1ad` cannot have its native observable effect +without choosing the unified save backend already being deliberately deferred. It remains an effectful gap, +now explicitly grouped with persistence work rather than UI support. + +The investigation also corrected a foundational native-field label: `ctx+0x53d88 + curCtx*0x78` is the +decoded instruction length in dwords (`1 + 2*argc`) used by `adv_interpreter_tick` to advance the PC, not a +gfx command type. The canonical EngineCtx field, affected opcode prose, native RE, SCJUMP note, and legacy +Frida-tool description now reflect that distinction; no completed host gfx behavior changes as a result. + +**Next:** leave `0x1ad` and `0x1cb` together behind the future save/profile boundary. Investigate the paired +zero-operand `0x1f6`/`0x20e` calls next: both bracket SC0000's ADV scene setup/teardown paths and are the +largest repeated non-persistence cluster still unclassified. + +Validation: all 206 engine tests pass; opcode and EngineCtx tests/lints, vm0 RECOVER, and `git diff --check` +are clean. SC0000 coverage deliberately remains 119/129 distinct opcodes handled (92.2%), with `0x1ad`'s +six calls retained as a gap until numbered saves serialize active execution frames. diff --git a/docs/scjump-progression.md b/docs/scjump-progression.md index 0a330d2..9820e27 100644 --- a/docs/scjump-progression.md +++ b/docs/scjump-progression.md @@ -41,9 +41,10 @@ Run: `py -3.11 -X utf8 tools/scjump_decode.py --verify`. ## The native decision→scene boundary (deferred) A FIELD snippet does `lookup-array(ptr, 0x5f0ed, 0x62ccf)` then `u00428010(ptr)`, which the spec -guessed was the scene resolver. **Correction (2026-07-07, via Ghidra):** `u00428010` (op `0x1a2`) is a -**graphics command-buffer op** (its real handler `FUN_0042d360` sets gfx cmd-type 3 and builds a -`"%c%8.8x"` key) — not save, not scene-load. So that snippet is a **graphics/UI operation, not the +guessed was the scene resolver. **Correction (refined 2026-07-20, via Ghidra):** `u00428010` (op `0x1a2`) +registers an operand's current value under a `"%c%8.8x"` key derived from its lvalue descriptor in a +separate open-addressing table. Its write of 3 at `ctx+0x53d88` is the instruction length, not a gfx +command type. So that snippet is a **value-registration operation, not the decision→scene dispatch**. The real decision→scene mechanism is **still unidentified** and belongs with the call-script / script-load dispatch (`name-resolution.md §1`). See `docs/engine-re.md` for the verified handler analysis and the opcode-dispatch table that will crack call-script next. diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 2da7d98..be0aadb 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -189,7 +189,7 @@ branching/state can shift page ordinals between runs. Resolve a reported page wi | `tools/frida/dump_engine.py` | ★ **Dump the UNPACKED engine code** from the live process for offline static RE (native handlers). `AGE.EXE` unpacks in-place at `0x400000`; Kelebek VAs map `VA−0x400000` = file-off. Validated via the AGF-decoder landmark `+0x74f1f`. | `py -3.11 -u -X utf8 tools/frida/dump_engine.py [pid]` | running game → `build/engine-dump/{manifest.json,range_.bin}` | | `tools/frida/map_imports.py` (+ `map_imports_full.py`) | ★ **Name dynamically-resolved Win32 APIs** in the Ghidra image. Read-only: maps live-process module exports → `{addr→dll!Func}`, scans the `0x400000` module for pointer matches → `RVA→name` (ASLR-stable). `--recon` = clustering report (the gate); default writes the map. Applied to `/v2` via a `run_script_inline` pass → 248 `imp__` labels at the RVA `0x16f000` IAT (validated: CreateFileA/SetFilePointer/timeGetTime). Pure scan/cluster logic unit-tested (`test_map_imports.py`). | `py -3.11 -u -X utf8 tools/frida/map_imports.py [--recon]` | running game → `build/import-map.json` (+ `-singletons.json`) | | `tools/frida/probe_handlers.py` | Probe which region the interpreter executes from (module vs heap). Confirmed: **operand-fetch `+0x1b940` fires ~8500/s ⇒ interpreter runs from the module `0x400000`** (handlers hookable by dump address). | `py -3.11 -u -X utf8 tools/frida/probe_handlers.py [pid]` | running game → stdout (per-hook fire counts) | -| `tools/frida/capture_gfx_objects.py` | Capture the native gfx object-manager state: grab engine ctx (`esi` via operand-fetch `ecx`), poll the object-record array `[esi+0x53d64]` (20×120B; `field[0]=0xffffffff`=free, cmd-type at rec+0x24). **⚠ Its "0 CG records ⇒ drift is state-divergence" reading was DISPROVEN** (Ghidra: op 0x215 read settles the drift as a native command-buffer op — `docs/engine-re.md`; the poll observed the record array, not the lookup map that drives the branch, and cmd-buffer records are transient). Kept as a runtime-observation tool. | `py -3.11 -u -X utf8 tools/frida/capture_gfx_objects.py [pid] [secs]` | running game → `build/gfx-objects.jsonl` | +| `tools/frida/capture_gfx_objects.py` | Legacy/misnamed probe: grab engine ctx (`esi` via operand-fetch `ecx`) and poll `[esi+0x53d64]`. Ghidra later proved these are 20×120-byte **script-context records**, with the current instruction length at record `+0x24`, not gfx objects or command types. Its former "0 CG records ⇒ drift is state-divergence" conclusion is invalid; op 0x215 queries the separate retained-object map described in `docs/engine-re.md`. Kept only for raw runtime context observation and historical reproducibility. | `py -3.11 -u -X utf8 tools/frida/capture_gfx_objects.py [pid] [secs]` | running game → `build/gfx-objects.jsonl` | | `tools/frida/probe_frame_cadence.py` | **Frame-cadence probe** (`docs/engine-re.md` "Frame cadence — live measurement"): plain-JS hook on operand-fetch `0x41b940` (grab ctx + count operand reads) + system-DLL message/timing hooks; auto-buckets by Ctrl/skip-bit. Measured ~1,788 **operand fetches/sec** normal, ~4× fast-forward; this is not an opcode count. **Read-only/import-only — never CModule-hook the hot interpreter (crashes the game).** Play actively during capture; hold Ctrl the back half. | `py -3.11 -u -X utf8 tools/frida/probe_frame_cadence.py [secs] [proc]` | running game → `build/frida-frame-cadence.jsonl` + stdout report | | `tools/frida/probe_present.py` | **Present-rate probe:** grab ctx, scan it for the D3D9 device (d3d9-vtable object with a full ~119-method table), hook `IDirect3DDevice9::Present`/`EndScene` (+ GDI-blit fallback). Found: **D3D9, UNCAPPED** (`Present` ~1908/sec, no vsync; no `ddraw`; 2D StretchRect compositor) ⇒ no fixed frame rate. Click 2–3× at start to grab ctx. | `py -3.11 -u -X utf8 tools/frida/probe_present.py [secs]` | running game → `build/frida-present.jsonl` + stdout report | | `tools/frida/trace_engine_ops.py` | **Engine op-path tracer** for the differential oracle (`docs/engine-re.md` "Differential offset-path oracle"): per executed op, read `cur_ctx_index@0x53d14`/`frame_pc@0x53d2c`/`frame_codebase@0x53d28` → emit `(codebase, offset=(pc−codebase)/4)`. **Use `--hook operand` (0x41b940, proven-safe)** — `--hook tick` (0x410fb0) sees `ecx≠ctx` (0 entries). Writes `build/tracer-live.flag` when the hook is installed → launch in the background, gate the New-Game trigger on the flag (else the scene-entry burst is missed). | `py -3.11 -u -X utf8 tools/frida/trace_engine_ops.py [--hook operand\|tick] [secs]` | running game → `build/engine-optrace.jsonl` | diff --git a/tools/age_opcodes_himegari.py b/tools/age_opcodes_himegari.py index ebfc01b..d3f6902 100644 --- a/tools/age_opcodes_himegari.py +++ b/tools/age_opcodes_himegari.py @@ -4,7 +4,7 @@ from __future__ import annotations INFERRED: dict[int, dict] = { - 0x7b: dict(name='coroutine-save-yield-handlers', category='control', noop=False, confidence='high', source='investigation', summary='(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index) + gfx cmd-type 5. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator).'), + 0x7b: dict(name='coroutine-save-yield-handlers', category='control', noop=False, confidence='high', source='investigation', summary='(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator).'), 0x7c: dict(name='coroutine-resume', category='control', noop=False, confidence='high', source='investigation', summary='() — scene-coroutine RESUME point. Native requires run-state bit 0x2000000 (ctx[0x6dbc8]) set — THROWS (__CxxThrowException) if unset, so it is only ever reached on a scheduler-driven re-entry, NEVER on a cold first pass (cold flow jmps over it). Restores PC=ctx[0x53d28]+ctx[0x6dbcc]*4, clears the run-bit (ctx+0xa0ce4 &= ~0x2000000), resets input/line state. SC0000 0x443 (falls into the main loop label_444). See engine-re.md §Scene-coroutine framework.'), 0x86: dict(name='set-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='(resource_id) - load an indexed cursor asset and install it as the active custom cursor.'), 0x87: dict(name='clear-cursor-resource', category='input', noop=False, confidence='high', source='investigation', summary='Clear the active custom cursor and refresh the OS cursor when the game window is active.'), @@ -35,6 +35,7 @@ INFERRED: dict[int, dict] = { 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 the NATIVE video/transition service (*DAT_005c6018)(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). DAT_005c6018 is runtime-resolved (all xrefs READ) = SAME class as the DirectDraw workers we don't model. 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 video service. See engine-re.md §Scene-coroutine framework."), 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.'), 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.'), + 0x1a2: dict(name='register-lvalue-value', category='control', noop=False, confidence='high', source='investigation', summary="0x1a2 (value) — register operand 1's current value under a key derived from its lvalue descriptor in the open-addressing table at ctx+0x5190. The write of 3 at ctx+0x53d88 is only this instruction's encoded dword length, not a graphics command type. This structure is separate from op 0x215's retained gfx-object map; op 0x215 does not query it. NOT save/scene."), 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.'), 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.'), diff --git a/vm-map/engine-ctx.toml b/vm-map/engine-ctx.toml index efc9412..b397351 100644 --- a/vm-map/engine-ctx.toml +++ b/vm-map/engine-ctx.toml @@ -96,7 +96,7 @@ note = "surface array base [~1000 slots]; create/set-texture (0x1f8/0x1f9) alloc offset = 0x53d14 name = "cur_ctx_index" type = "uint" -note = "current gfx-object / script-context index (curCtx); indexes 0x78-byte records" +note = "current script-context index (curCtx); indexes 0x78-byte coroutine/frame records" [[field]] offset = 0x53d28 name = "frame_codebase" @@ -119,9 +119,9 @@ type = "uint" note = "raw packed SYS4/AAI resource id for this 0x78-byte script frame; persisted ReadTextDB script key" [[field]] offset = 0x53d88 -name = "cmd_type_table" +name = "frame_instruction_word_count" type = "int" -note = "per-object cmd-type column base (write *(0x53d88 + curCtx*0x78))" +note = "current decoded instruction length in dwords for each 0x78-byte script frame; interpreter advances PC by this value * 4" [[field]] offset = 0x550fc name = "message_skip_display_enabled" @@ -303,6 +303,11 @@ name = "message_skip_queued_voice_arg" type = "int" note = "second argument retained with message_skip_queued_voice_id; Himegari op 0xc4 stores zero" [[field]] +offset = 0x9928c +name = "save_frame_boundary_index" +type = "int" +note = "highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it" +[[field]] offset = 0x9b24c name = "dispatch_table" type = "void*" diff --git a/vm-map/opcodes.toml b/vm-map/opcodes.toml index 2107d72..0083216 100644 --- a/vm-map/opcodes.toml +++ b/vm-map/opcodes.toml @@ -1249,12 +1249,12 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "coroutine-save-yield-handlers" category = "control" -summary = "(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index) + gfx cmd-type 5. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator)." +summary = "(handler1_pc)(handler2_pc) — scene-coroutine: save the two per-frame yield/resume handler PCs. Native writes op1→ctx[0x6da88+idx*4], op2→ctx[0x6db28+idx*4] (idx=ctx[0x53d14] script-context index); its generic handler prologue records the 5-dword instruction length. SC0000 0x79: `0x7b label_3c9 label_41e` registers the ADV per-frame render→poll→yield handlers. Part of the scene-coroutine framework (see engine-re.md §Scene-coroutine framework); pairs with 0x7c (resume) + 0x140 (loop iterator)." noop_headless = false source = "investigation" confidence = "high" depends_on = [] -evidence = "Ghidra: handler FUN_0041ebf0 (dispatch ctx[0x26c93+0x7b]) = {*(ctx+0x53d88+ctx[0x53d14]*0x78)=5; ctx[0x6da88+idx*4]=op1; ctx[0x6db28+idx*4]=op2}. Both operands are code PCs (handler labels)." +evidence = "Ghidra: handler FUN_0041ebf0 (dispatch ctx[0x26c93+0x7b]) = {frame_instruction_word_count[idx]=5; ctx[0x6da88+idx*4]=op1; ctx[0x6db28+idx*4]=op2}. Both operands are code PCs (handler labels)." [[opcode.semantics.args]] i = 1 @@ -2046,7 +2046,7 @@ abi_source = "kelebek+decode-validated" name = "sleep" category = "control" summary = "Pause the current script for milliseconds while retained presentation continues." -details = "Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also writes gfx cmd-type 3 + runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity)." +details = "Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_timer_arm @0x44cff0 at ctx+0x5f304 = active flag + start tick + duration) that the engine main loop polls, resuming the script when elapsed. Operand UNIT = MILLISECONDS (start = ms tick source DAT_0056f3d4, timeGetTime/GetTickCount class). duration<10 fast-paths via [0x56f0b8]; all real scene sleeps (100/750/1000) are >=10. The handler also records its generic 3-dword instruction length and runs anti-tamper checks, neither needed host-side. Port equivalent: the Godot host parks the VM thread for duration ms while the presentation compositor continues. Sleep is one proven presentation-capable service boundary; ordinary AE setup runs burst-fast to 0x21c and is not paced per opcode. Headless hosts no-op it (parity)." noop_headless = false source = "investigation" confidence = "high" @@ -2894,7 +2894,7 @@ noop_headless = false source = "investigation" confidence = "med" depends_on = [] -evidence = "Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Writes gfx cmd-type 9; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service." +evidence = "Ghidra: handler 0x4299c0 (dispatch ctx[0x9b74c]=0x4299c0; created+typed EngineCtx*+annotated; Kelebek u0041F9C0 = VA-drift). Records the generic 9-dword instruction length; op2→local_204, op3→local_104, op4→local_208; (*DAT_005c6018)(8, ctx[0x54fe8], &local_210) → FUN_00425fb0(1,ret). DAT_005c6018: 6 xrefs all READ, no static writer; FUN_00405740 (screen-fade) calls it w/ cmd 3, branches on ret 1/2 = transition progress = native video service." [[opcode.semantics.args]] i = 1 @@ -3464,9 +3464,9 @@ argc = 1 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "gfx-cmd-register" -category = "draw" -summary = "0x1a2 (value) — gfx cmd-type 3. Handler gfx_op_0x1a2_descriptor_register@0x42d360 builds a key from operand 1's lvalue descriptor and inserts its value into an open-addressing descriptor hash (vm_lvalue_descriptor_hash_insert@0x42cf70). This structure is separate from op 0x215's retained gfx-object map; op 0x215 does not query this hash. NOT save/scene." +name = "register-lvalue-value" +category = "control" +summary = "0x1a2 (value) — register operand 1's current value under a key derived from its lvalue descriptor in the open-addressing table at ctx+0x5190. The write of 3 at ctx+0x53d88 is only this instruction's encoded dword length, not a graphics command type. This structure is separate from op 0x215's retained gfx-object map; op 0x215 does not query it. NOT save/scene." noop_headless = false source = "investigation" confidence = "high" @@ -3710,19 +3710,19 @@ observed_types = ["l-int"] [[opcode]] op = 0x1ad -label = "u004154F0" +label = "mark-save-resume-frame" argc = 0 abi_source = "kelebek+decode-validated" [opcode.semantics] -name = "u004154F0" -category = "unknown" -summary = "" +name = "mark-save-resume-frame" +category = "control" +summary = "Mark the current script context as the highest frame serialized by numbered-save layouts 2/3. The native serializer saves frames 0 through this boundary and strips the boundary frame's return target so loading resumes it as the top frame. This opcode performs no file I/O itself." noop_headless = false -source = "kelebek" -confidence = "low" +source = "investigation" +confidence = "high" depends_on = [] -evidence = "" +evidence = "Ghidra /v2: op_0x1ad_mark_save_resume_frame@0x416b70 writes decoded instruction size 1 and ctx+0x9928c=cur_ctx_index. context_state_serialize@0x40d320 uses that field (or cur_ctx_index when -1) as the inclusive frame cutoff for save layouts 2/3, serializes frames 0..cutoff, and forces the cutoff frame's saved return entry to -1. op_0x2_exit_or_return_frame@0x417940 clears the mark when unwinding below it. Corpus: 1,928 calls in 304 scripts; SC0000's six calls are at startup and immediately after HISTORY/MENU/HIDEWIN/INPUTNAME returns." [[opcode]] op = 0x1ae @@ -4840,7 +4840,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "gfx-draw-color" category = "draw" -summary = "0x203 (handle)(mode)(alpha)(color) — gfx cmd-type 9. Worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case." +summary = "0x203 (handle)(mode)(alpha)(color) — worker stores the D3D blend selector at obj+0x30 and STATIC packed color at obj+0x60; the handler's ctx+0x53d88 write is the generic 9-dword instruction length. Negative alpha/RGB preserve current static bytes. Mode 0 is the opaque textured path: preserved 0xffffffff is identity (the alpha byte is not tint strength); mode 1 is SRCALPHA/INVSRCALPHA with ARGB alpha opacity and multiplicative RGB modulation; mode 2 is the 0x223 transition-source identity path. Surfaceless mode-0 fill consumption remains a distinct case." noop_headless = false source = "investigation" confidence = "high" @@ -5202,7 +5202,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-gfx-field64" category = "draw" -summary = "0x212 (obj_idx)(val) — gfx cmd-type 5. Handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. Sets one per-object field. See docs/engine-re.md gfx op-contract table." +summary = "0x212 (obj_idx)(val) — handler gfx_op_0x212_set_field64 @0x4230c0: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x64)=val. The generic instruction length is 5 dwords. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5228,7 +5228,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-gfx-xy" category = "draw" -summary = "0x213 (obj_idx)(x)(y) — gfx cmd-type 7. Handler gfx_op_0x213_set_field68_6c @0x423110: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x68)=x; *(obj+0x6c)=y (an (x,y) pair). See docs/engine-re.md gfx op-contract table." +summary = "0x213 (obj_idx)(x)(y) — handler gfx_op_0x213_set_field68_6c @0x423110: obj=[ctx+0x14d54 + obj_idx*4]; if obj: *(obj+0x68)=x; *(obj+0x6c)=y (an (x,y) pair). The generic instruction length is 7 dwords. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5285,7 +5285,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "query-gfx-field?" category = "draw" -summary = "0x216 (out)(idx) — gfx cmd-type 5. Handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table." +summary = "0x216 (out)(idx) — handler gfx_op_0x216_query_table46d14 @0x42a0f0: out = *(ctx+0x46d14 + idx*0x14). The generic instruction length is 5 dwords. A per-object field query over a stride-0x14 table. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5311,7 +5311,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-gfx-geom3" category = "draw" -summary = "0x217 (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x217_set_geom3 @0x4231b0: SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e960. In SC0000 label_12649 it writes the anchor vector G[0x6249b/c/d] INTO the object; op 0x218 reads it back. See docs/engine-re.md gfx op-contract table." +summary = "0x217 (handle)(a)(b)(c) — handler gfx_op_0x217_set_geom3 @0x4231b0 SETS a 3-vector (int→float a,b,c) on object `handle` via native worker FUN_0047e960; its generic instruction length is 9 dwords. In SC0000 label_12649 it writes the anchor vector G[0x6249b/c/d] INTO the object; op 0x218 reads it back. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5347,7 +5347,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "get-gfx-geom3?" category = "draw" -summary = "0x218 (handle)(out_a)(out_b)(out_c) — gfx cmd-type 9. Handler gfx_op_0x218_query_geom3 @0x42a130: GETS a stored 3-vector from object `handle` (FUN_0047f360) into out_a/b/c. In label_12649 it reads the object's anchor vector back into G[0x6249b/c/d] — a stubbed DRIVER of the render drift (stale anchor → bad centering). See docs/engine-re.md gfx op-contract table." +summary = "0x218 (handle)(out_a)(out_b)(out_c) — handler gfx_op_0x218_query_geom3 @0x42a130 GETS a stored 3-vector from object `handle` (FUN_0047f360) into out_a/b/c; its generic instruction length is 9 dwords. In label_12649 it reads the object's anchor vector back into G[0x6249b/c/d] — a stubbed DRIVER of the render drift (stale anchor → bad centering). See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5383,7 +5383,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-gfx-geom3-b" category = "draw" -summary = "0x219 (handle)(a)(b)(c) — gfx cmd-type 9. Handler gfx_op_0x219_set_geom3 @0x423240: SETS a 3-vector (int→float) on object `handle` via native worker FUN_0047e910 (sibling of 0x217, a different per-object vector). See docs/engine-re.md gfx op-contract table." +summary = "0x219 (handle)(a)(b)(c) — handler gfx_op_0x219_set_geom3 @0x423240 SETS a 3-vector (int→float) on object `handle` via native worker FUN_0047e910 (sibling of 0x217, a different per-object vector); its generic instruction length is 9 dwords. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5419,7 +5419,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "get-gfx-geom3-b?" category = "draw" -summary = "0x21a (handle)(out_a)(out_b)(out_c) — gfx cmd-type 9. Handler gfx_op_0x21a_query_geom3 @0x42a1b0: GETS a stored 3-vector from object `handle` (FUN_0047f2e0) into out_a/b/c. In label_12649 it reads the object's position vector into G[0x62498/9/a] — a stubbed DRIVER of the render drift. See docs/engine-re.md gfx op-contract table." +summary = "0x21a (handle)(out_a)(out_b)(out_c) — handler gfx_op_0x21a_query_geom3 @0x42a1b0 GETS a stored 3-vector from object `handle` (FUN_0047f2e0) into out_a/b/c; its generic instruction length is 9 dwords. In label_12649 it reads the object's position vector into G[0x62498/9/a] — a stubbed DRIVER of the render drift. See docs/engine-re.md gfx op-contract table." noop_headless = false source = "investigation" confidence = "high" @@ -5482,7 +5482,7 @@ noop_headless = false source = "investigation" confidence = "high" depends_on = [0x223, 0x1c7, 0x1cc] -evidence = "Ghidra handler 0x417520 sets cmd-type 1 and ORs ctx+0xa0ce4 with 0x400. capture_presentation_trace.py: after 0x125a6 render, 0xcb8e/0xcb98 bind and 0xd5a/0xd63/0xd73/0xd8a mode+targets execute without render; repeated gfx_render_frame begins only at 0x21c. 2026-07-10." +evidence = "Ghidra handler 0x417520 records the 1-dword instruction length and ORs ctx+0xa0ce4 with 0x400. capture_presentation_trace.py: after 0x125a6 render, 0xcb8e/0xcb98 bind and 0xd5a/0xd63/0xd73/0xd8a mode+targets execute without render; repeated gfx_render_frame begins only at 0x21c. 2026-07-10." [[opcode]] op = 0x21d @@ -6198,7 +6198,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-anim-clock" category = "draw" -summary = "(duration) — set the GLOBAL animation clock: native ctx+0x51b78=0 (elapsed), +0x51b7c=duration. cmd-type 3. 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." noop_headless = false source = "investigation" confidence = "high" @@ -6441,7 +6441,7 @@ abi_source = "kelebek+decode-validated" [opcode.semantics] name = "set-gfx-field2d0" category = "draw" -summary = "0x242 (handle)(value) — command type 5. Get-or-create the retained gfx object and write value to obj+0x2d0. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown." +summary = "0x242 (handle)(value) — get-or-create the retained gfx object and write value to obj+0x2d0; the generic instruction length is 5 dwords. SC0000's common CG loader passes zero after draw binding. This field does not reset transform or color channels; its downstream purpose remains unknown." noop_headless = false source = "investigation" confidence = "high"