feat: model ADV coroutines and retained effect teardown
This commit is contained in:
@@ -6,7 +6,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch-
|
||||
|
||||
| offset | name | type | note |
|
||||
|---|---|---|---|
|
||||
| `0x408` | `gfx_obj_registry` | `int` | gfx object registry (std::map handle->object); op 0x1a2 insert / 0x215 find |
|
||||
| `0x408` | `gfx_obj_registry` | `int` | retained gfx-object map (std::map handle->object); geometry/draw get-or-create, 0x215 returns obj+4 source slot, 0x1f7 erases |
|
||||
| `0x40c` | `sys4ini_count` | `int` | SYS4INI record count |
|
||||
| `0x410` | `archive_name_table` | `void*` | archive-name table base (arc_id*0x100 indexes it) |
|
||||
| `0x414` | `sys4ini_records` | `void*` | SYS4INI 80-byte record base {name[64],arc_id,file_number,offset,size}; record = base + id*0x50 |
|
||||
|
||||
@@ -123,7 +123,7 @@ companion index/pointer accessor.
|
||||
|
||||
**Hot-helper naming pass (2026-07-09, lever #2).** Ghidra's Function ID analyzer names **0** functions on
|
||||
this image (the bundled FidDbs don't cover the VC9/VS2008 static runtime; ~3,660 of 4,428 funcs stay
|
||||
`FUN_`), and the library workers we actually touch were already hand-named (`gfx_registry_map_find`,
|
||||
`FUN_`), and the library workers we actually touch were already hand-named (`gfx_object_query_source_slot`,
|
||||
etc.). So "STL/CRT auto-naming" had little to add — but the recon (rank unnamed funcs by call-count)
|
||||
surfaced the real win: ubiquitous **documented-but-unnamed helpers**. Named the top 5 (~2,400 call
|
||||
sites): `vm_operand_fetch`@`0x41b940` (1021 refs), `vm_operand_write`@`0x425fb0` (188),
|
||||
@@ -138,7 +138,7 @@ Ghidra name is the record.
|
||||
`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…`
|
||||
(verified: `sleep_op_0xc8`, `gfx_op_0x215_register_query`). Add a field: edit `engine-ctx.toml`, run
|
||||
(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`.)
|
||||
|
||||
---
|
||||
@@ -250,45 +250,39 @@ used. Absence in that capture ≠ absence of the native 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
|
||||
handle→object registry (a hash map). The gfx ops are inserts/queries/writes against these, and the inserts
|
||||
are **bytecode-driven** — so a faithful host-side model, with the gfx ops (`0x1a2`, `0x215`, and the
|
||||
`0x212–0x21a` family) *executed* instead of stubbed, rebuilds the state from the same scripts. The opcode-
|
||||
level summary lives in `vm-map/opcodes.toml` op `0x215`.
|
||||
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,
|
||||
but that is a separate structure and is not what op `0x215` queries. The opcode-level source of truth is
|
||||
`vm-map/opcodes.toml`.
|
||||
|
||||
#### The query registry is SEPARATE from the geometry object store (2026-07-07) — the retained-mode "2nd CG off-screen" fix
|
||||
#### Op `0x215` queries the retained gfx-object's source slot (corrected 2026-07-09)
|
||||
|
||||
Modelling the gfx ops (above) exposed a subtle but decisive point that the first retained-mode
|
||||
implementation got wrong. There are **two distinct native structures**, and they must stay distinct:
|
||||
The decisive caller/callee detail is the owner pointer. Op `0x215`'s handler passes
|
||||
`ECX = ctx+0x46614` to `gfx_object_query_source_slot` (`0x47f280`); that worker searches
|
||||
`ECX+0x408`. `draw-texture` passes the same owner to `gfx_object_bind_draw` (`0x47e870`), whose
|
||||
`gfx_object_get_or_create` uses the same `owner+0x408` map and writes the source surface slot to
|
||||
`obj+4`. Therefore:
|
||||
|
||||
1. **The op-`0x215` query registry** — a `std::map<handle,value>` **populated ONLY by op `0x1a2`**
|
||||
(`FUN_0042cf70` hash insert; native stores `map[handle] = handle`). `0x215` does `map.find(handle)`
|
||||
→ the found value (which equals the handle, and for small system/UI handles doubles as their surface
|
||||
slot) or `0xffffffff` = **-1**.
|
||||
2. **The geometry object store** — per-handle V18/V24/V16c/color/draw-bind, touched lazily by the
|
||||
geometry SET ops and `draw-texture` (`gfx_object_get_or_create`). This feeds the compositor.
|
||||
- absent handle → `0x215` returns `0xffffffff` (-1);
|
||||
- geometry-only/unbound object → its default source slot is -1;
|
||||
- draw-bound object → `0x215` returns the live source slot from `obj+4`.
|
||||
|
||||
The first `GfxState` conflated them: `GetOrCreate` (called by *every* geometry/draw op) also assigned a
|
||||
fabricated per-object slot via an `AcquireSlot()` allocator, and `QuerySlot` (op `0x215`) returned it.
|
||||
That is a fiction with **no basis in the engine** — the native `0x215` never allocates a slot.
|
||||
The earlier “op `0x215` reads a registry populated only by `0x1a2`” conclusion was wrong: it conflated
|
||||
the retained-object `std::map` with `0x1a2`'s open-addressing operand-descriptor hash. The useful part of
|
||||
the earlier fix remains: `GetOrCreate` must not fabricate a slot. A fresh object stays unbound (-1) until
|
||||
`draw-texture` supplies its real source slot.
|
||||
|
||||
Consequence, traced end-to-end in `SC0000` `label_12649` (the CG-load subroutine): a CG handle
|
||||
(`0xcb2a` = `INIT2`'s `G[0x62456]`, idx 1) is **never `0x1a2`-registered**. Real engine → `0x215` returns
|
||||
`-1` → the **fresh branch** runs → anchor comes from the `INIT2` arrays (`G[0x62469+idx]=400`,
|
||||
`G[0x6247d+idx]=600`) → `dst = anchor − (w/2, h) = (0,0)`. Correct. But with the fabricated allocator the
|
||||
*second* pass over the same handle found it "existing" (slot `4`) → the **existing branch** ran
|
||||
`get-texture-size(4)` on a slot whose surface was never loaded (the bytecode's own slot table
|
||||
`rec[s3]`/`G[0x3239]` gave slot `0`) → size `0` → `anchor = pos(0,0) + 0` → `dst = (0−400, 0−600) =
|
||||
(−400,−600)` — the CG rendered off-screen. This is the bug that had been mis-attributed to "geometry
|
||||
accumulation / drift" several times.
|
||||
|
||||
**Fix (branch `feat/gfx-command-buffer`):** `GfxState` keeps a separate `_registry` (a `HashSet<long>`)
|
||||
populated only by `Register(handle)` (op `0x1a2`); `QuerySlot` returns `handle` if registered else `-1`,
|
||||
and no longer consults the geometry store or invents slots. Verified: `Age.Cli gfx --boot SC0000.BIN`
|
||||
→ all event CGs `dst=(0,0)`, zero `(−400,−600)` draws; Godot `--boot --shot` pages 1/2/4 render the
|
||||
opening event CGs full-screen; engine 44/44; sweep parity 284 exit / 13 STEP-LIMIT unchanged.
|
||||
This also explains the reported magic-circle retention end-to-end. `AE001H.AGF` (resource `0x37`) is
|
||||
bound to the ritual object's surface slot. At the post-effect cleanup (SC0000 `0x3321`), the script queries that object with
|
||||
`0x215`, sign-tests the returned slot, then executes `0x1f7(handle, 10)` followed by
|
||||
`0x1fa(returned_slot)`. Native `0x1f7` removes the retained object group from this same map; `0x1fa`
|
||||
releases the surface slot. The port's old separate-registry model returned -1, skipped the guarded cleanup,
|
||||
and left the circle compositing. `GfxState.QuerySlot` now returns `GfxObject.SourceSlot`; VM op `0x1fa`
|
||||
clears that surface slot. The booted SC0000 regression ends with no visible resource `0x37`; live
|
||||
clicked-path validation confirmed the corrected disappearance on 2026-07-10.
|
||||
|
||||
Note a **second, still-latent** gap this uncovered: `label_125bd` (which fills `rec[s3]`/`G[0x3239]` with
|
||||
the per-object slots 4..13, called at `SC0000` `0x50f`) does **not** execute in a cold single-scene run —
|
||||
the eight per-object slots 4..11, called at `SC0000` `0x50f`) does **not** execute in a cold single-scene run —
|
||||
the scene coroutine framework (ops `0x7b`/`0x140` + the `G[0xaba5c]==1` re-entry gate) routes cold flow
|
||||
past it, so every fresh CG is assigned slot `0`. It doesn't break the *opening* (one full-screen CG shown
|
||||
at a time, so sharing slot 0 is harmless and the fresh-branch geometry is correct regardless), but a scene
|
||||
@@ -296,7 +290,7 @@ with several simultaneous distinct-slot objects would need the setup to run. Tra
|
||||
work, separate from this fix.
|
||||
|
||||
**⇒ Scene-coroutine framework — INVESTIGATION COMPLETE (2026-07-09).** The mechanism behind the slot-0
|
||||
collapse is now fully understood; what remains is a *host-model design choice*, not more RE. Summary:
|
||||
collapse is fully understood; the native finding and the implemented host-model disposition follow:
|
||||
|
||||
**The gate `G[0xaba5c]` is NATIVE scene-entry state — no script sets it to 1.** Across the whole corpus
|
||||
(429 references in 150 files) *every* `aba5c` reference is a read or a write of `0`; nothing anywhere writes
|
||||
@@ -306,7 +300,7 @@ handle array** (native entry-state a cold single-scene harness skips), NOT a sto
|
||||
**Corrected roles of the two branches** (the earlier head-start had them inverted). At SC0000 `0x450`
|
||||
`eq local0 = (aba5c==1)`; `0x457 jcc local0 label_462 <fallthrough>`:
|
||||
- **`aba5c==1` → `label_462` "ループ開始" (loop start)** = the scene's **intro/setup LOOP**. Its body
|
||||
`label_491` runs `call label_125bd` (@`0x50f`, the slot-table fill `G[0x3239..0x3256]=4..13`) plus ADV
|
||||
`label_491` runs `call label_125bd` (@`0x50f`, the slot-table fill `G[0x3239..0x324e]=4..11`) plus ADV
|
||||
state init, UI-slot clears (a loop over `G[0x3239]`), intro draw — then `jmp label_462` (@`0x711`). A real
|
||||
loop, exited only when its iterator makes `G[0x6be]==exit-PC` (→ `mov aba5c 0`, `jmp label_45e`).
|
||||
- **`aba5c!=1` → `label_45e → call label_71b`** = the **scene CONTENT**: `label_71b` is a `switch(G[0x62ccf])`
|
||||
@@ -343,15 +337,18 @@ slot fix:** the same `0x7b`/`0x7c` + handler machinery is the ADV frame loop, so
|
||||
the seam for the interactive-ADV backlog (`0x90` hotspots / EMPTY scenes) and likely fixes the stuck magic
|
||||
circle (scene-phase cleanup). **Permanently out of static scope:** the real intro-transition *timing/pacing*
|
||||
inside the loop (it lives in the native video service) — we approximate it host-side, as everywhere else.
|
||||
**Open for the spec (a choice, not RE):** how `G[0x6be]` initialises before the loop and the exact "run body
|
||||
once then terminate" mechanic (the terminal must satisfy `G[0x6be]==G[0x6c3]`, and `G[0x6c3]` is a per-scene
|
||||
immediate = that scene's exit-PC, so the host model can't hardcode `0x45e`). Context records = the 0x78-byte
|
||||
coroutine records at `ctx+0x53d14`/`0x53d88` (see §"Frame cadence").
|
||||
**Host model implemented (2026-07-09).** `VirtualMachine` recognizes only the ADV `"LABEL","J"` form
|
||||
(138 corpus scripts; all have the same terminal-check shape), synthesizes `G[0xaba5c]=1` on top-level
|
||||
scene entry, forces one setup-body pass even if `G[0x6be]` is stale, then returns the terminal immediate
|
||||
discovered from the following `mov`/`eq` pair. Thus no SC0000 offset is hardcoded. Op `0x7b` retains the
|
||||
saved handler PCs as frame metadata; op `0x7c` is a host-scheduler marker because `IHost.FrameYield` and
|
||||
`FrameClock` already own per-frame pacing. `TITLE.BIN`'s unrelated `"BIN","SC????.BIN"` service remains
|
||||
stubbed. The real video-service timing remains intentionally unmodeled.
|
||||
|
||||
**Revealed issue (2026-07-08, post slot-fix): a magic-circle effect persists across the scene transition**
|
||||
(screenshot: opening ritual circle still overlaid on the arena BG). A retained object not released at the
|
||||
phase change — plausibly the same coroutine/lifecycle gap (scene-phase cleanup), or a separate release/clear
|
||||
op. Verify once the coroutine framework runs.
|
||||
**Magic-circle retention fixed in the host model (2026-07-09).** The effect is `AE001H.AGF`
|
||||
(resource `0x37`). SC0000 already contains the correct teardown, but the port's wrong `0x215` query
|
||||
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)
|
||||
|
||||
@@ -363,15 +360,15 @@ the dispatch table (`ctx[0x26c93+op]`); all renamed in the Ghidra project `gfx_o
|
||||
|
||||
| op | handler | cmd | dir | argc | contract |
|
||||
|---|---|---|---|---|---|
|
||||
| `0x1a2` | `0x42d360` | 3 | set | 1 | registry **insert**: key `"%c%8.8x"(3, operand-desc)` → `FUN_0042cf70` |
|
||||
| `0x1f7` | `0x422270` | 5 | erase | 2 | registry **erase** (teardown, NOT create): `op2>1` → `gfx_registry_erase_range(op1,op2)` erases `[op1,op1+op2)`, else `gfx_registry_erase(op1)`. Objects are created lazily by the geometry SET ops. |
|
||||
| `0x1fa` | `0x4224a0` | 3 | set | 1 | release element `[ctx+0x52bd4 + op1*4]` (vtbl free) + `FUN_00474e40(op1)` |
|
||||
| `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)` |
|
||||
| `0x202` | `0x4228d0` | 0xb | set | 5 | blit obj op1 with (op2,op3) + **packed ARGB** from op4(alpha)/op5(color) → `FUN_0047ea00` |
|
||||
| `0x203` | `0x4229a0` | 9 | set | 4 | draw obj op1 with op2 + packed color(op3/op4) → `FUN_0047e9b0` |
|
||||
| `0x212` | `0x4230c0` | 5 | set | 2 | `obj[ctx+0x14d54 + op1*4] -> +0x64 = op2` |
|
||||
| `0x213` | `0x423110` | 7 | set | 3 | `obj[0x14d54+op1*4] -> +0x68 = op2 ; +0x6c = op3` (an (x,y) pair) |
|
||||
| `0x215` | `0x42a0b0` | 5 | **query** | 2 | registry **find**(op2 handle) → op1 (value / `0xffffffff`). **Drives slot-select.** |
|
||||
| `0x215` | `0x42a0b0` | 5 | **query** | 2 | retained-object **find**(op2 handle) → op1 = obj+4 source slot / `0xffffffff`. **Drives setup and teardown.** |
|
||||
| `0x216` | `0x42a0f0` | 5 | **query** | 2 | read `[ctx+0x46d14 + op2*0x14]` → op1 |
|
||||
| `0x217` | `0x4231b0` | 9 | set | 4 | 3 int→float on obj op1 → `FUN_0047e960` (SETS a geom 3-vector) |
|
||||
| `0x218` | `0x42a130` | 9 | **query** | 4 | `FUN_0047f360(obj op1)` → op2,op3,op4 (GETS a geom 3-vector) |
|
||||
@@ -395,7 +392,7 @@ anchor math reads garbage). Both read object state the SET ops (`0x217`/`0x219`/
|
||||
bytecode-driven, all host-modelable.
|
||||
|
||||
**Model implication for the host-side reimplementation (Phase 2 input).** The subsystem is a set of
|
||||
per-object records keyed by handle, carrying: a **slot** (from the `0x215` registry), a **position 3-vector**
|
||||
per-object records keyed by handle, carrying: a live source **slot** (written by draw-texture, queried by `0x215`), a **position 3-vector**
|
||||
(`0x21a` get / a matching set), and an **anchor 3-vector** (`0x218` get / `0x217` set), plus color/blit
|
||||
params (`0x202`/`0x203`). The native workers (`FUN_0047xxxx` = the DirectDraw/surface layer) need **not** be
|
||||
modelled — only the object-record data model, so the QUERY ops return what the SET ops stored. That makes
|
||||
@@ -403,8 +400,9 @@ modelled — only the object-record data model, so the QUERY ops return what the
|
||||
correct `dst`/`w`/`h`. Ancillary per-object tables observed: `ctx+0x14d54` (obj pointers, fields `+0x64/
|
||||
+0x68/+0x6c`), `ctx+0x46d14` (stride `0x14`), `ctx+0x52bd4` (element pointers), plus the `0x408` registry.
|
||||
|
||||
Worker functions decoded + annotated in the Ghidra project (2026-07-07): `gfx_registry_erase`(`0x47d850`),
|
||||
`gfx_registry_erase_range`(`0x47d8b0`), `gfx_object_get_or_create`(`0x47ddb0`, inserts a zeroed default via
|
||||
Worker functions decoded + annotated in the Ghidra project (updated 2026-07-09): `gfx_object_erase`(`0x47d850`),
|
||||
`gfx_object_erase_range`(`0x47d8b0`), `gfx_object_query_source_slot`(`0x47f280`),
|
||||
`gfx_object_get_or_create`(`0x47ddb0`, inserts a zeroed default via
|
||||
`gfx_object_init_default`@`0x472810`), the setters `gfx_set_vec18/24/16c`(`0x47e960/e910/e800`), the getters
|
||||
`gfx_get_vec18/24`(`0x47f360/f2e0`).
|
||||
|
||||
@@ -438,25 +436,22 @@ animation/tween** — and two members were already named in prior RE (`0x234 gfx
|
||||
(`0x2bd`→`0x4251c0`, `0x2bf`→`0x425240`. The handful of `0x417xxx` handlers are trivial/marker-shaped — the
|
||||
default-handler neighbourhood — and are almost certainly no-ops or arg-poppers; triage before modelling.)
|
||||
|
||||
**Contract (decoded, representative ops `0x220`/`0x21e`, both `argc 6`, annotated in Ghidra):** same shape as
|
||||
the geometry family — write **cmd-type `0xd`** into the current object record, fetch operands 1..6, call a
|
||||
transform worker with `(int op1=handle, int op2, int op3, float op4, float op5, float op6)`. `0x220` uses raw
|
||||
floats (worker `0x47ecc0`); **`0x21e` normalizes the 3 floats by `/_DAT_00571c28`** (runtime-init divisor,
|
||||
static 0) so operand `0x64`=100 → a fraction → **scale/percentage** (worker `gfx_anim_set_channel`@`0x47eaa0`).
|
||||
The worker calls the SAME `gfx_object_get_or_create` our `GfxState` already models, then arms an animation
|
||||
channel on the object record: `obj+0x3c = op2`, `obj+0x50 = op3`, `obj+0x68 = 1` (enable), `obj+0xac =
|
||||
vec3(op4,op5,op6)` (the transform target), and raises global dirty flags `ctx+0xb558/+0xb560`. Corpus idiom:
|
||||
`0x220 (handle=0xcb20+k) 800 500 0 0 0` (size a CG object), `0x21e (handle) (val) 100 100 100 100` (scale/color
|
||||
channels). **`0x234 anim_start` + `0x238 set_anim_clock` imply a per-frame clock that interpolates these
|
||||
targets over time** — i.e. this is what makes `AE*` fades/effects *animate* rather than snap.
|
||||
**Contract (corrected 2026-07-09, representative ops `0x21e`/`0x220`, both `argc 6`):** these are
|
||||
independent matrix channels, not two encodings of one vec3 property.
|
||||
|
||||
**Model implication (Phase-2 input, mirrors the geometry family):** the DirectDraw workers need NOT be
|
||||
modelled — extend the host `GfxState` object with the transform/anim fields (a transform `vec3` target + the
|
||||
two scalar params + enable + an animation clock), have the SET ops (`0x21e/0x220/0x234/0x238/…`) write them and
|
||||
the compositor apply the transform per-frame, stepping the clock on `anim_start`/`set_anim_clock`. This is a
|
||||
spec/plan-worthy chunk (~18 effectful handlers + workers `0x47eaa0/0x47ecc0` + the per-frame stepping); the op
|
||||
map above is the de-risked starting point. `tools/scene_opcode_coverage.py SC0000` measures the GAP shrink as
|
||||
each lands.
|
||||
- `0x21e` normalizes operands 4–6, then `gfx_object_set_scale_channel` (`0x47eaa0`) stores timing at
|
||||
`obj+0x3c/+0x50` and calls `0x48af1d`, which writes the three values onto a 4×4 matrix diagonal at
|
||||
`obj+0xac`: a **scale matrix**.
|
||||
- `0x220` passes raw operands 4–6 to `gfx_object_set_translation_channel` (`0x47ecc0`), stores timing at
|
||||
`obj+0x44/+0x58`, and calls `0x48afb1`, which writes them into matrix entries 12–14 at
|
||||
`obj+0x1ac`: a **translation matrix**.
|
||||
- `gfx_object_apply_transform_channels` (`0x472f00`) interpolates and combines both matrices separately.
|
||||
Neither third component is opacity.
|
||||
|
||||
**Port implication:** `GfxState` must ultimately retain separate scale and translation matrices/timing.
|
||||
The current single `AnimTarget` plus Godot `TZ/100 = opacity` approximation is native-inaccurate and is
|
||||
now tracked as transform-compositor debt. It was not the cause of the lingering circle: that was the skipped
|
||||
`0x215`/`0x1f7`/`0x1fa` teardown above.
|
||||
|
||||
##### `anim_start`/`set_anim_clock` decoded + opening confirmed (2026-07-07, animation-slice Task 1)
|
||||
|
||||
@@ -481,11 +476,10 @@ both annotated) and grepping the SC0000 opening settles the animation model and
|
||||
**Corrected host model (supersedes the "per-object clock" wording above):**
|
||||
- **Global clock** (from `0x238`): one `AnimClockDurationTicks` + a generation/reset marker the host watches to
|
||||
reset its wall-clock `elapsed` to 0. The host tweens all armed objects over this duration.
|
||||
- **Per-object** (from `0x21e`/`0x220` = set transform directly; `0x234` = animate toward a target): the object's
|
||||
transform target vec3 + the two scalar params + enable + a per-object generation (bumped by `anim_start`).
|
||||
- **Residual (empirical, Task 6):** *which* vec3 component is opacity vs scale vs position lives in the DirectDraw
|
||||
draw-worker we deliberately don't model. Determine it empirically from the animating channel + screenshot, not
|
||||
by RE'ing the surface layer.
|
||||
- **Per-object correction:** `0x21e` is scale and `0x220` is translation; they occupy distinct matrices and
|
||||
timing fields. No component of either channel is opacity.
|
||||
- **Residual:** finish separating these channels in `GfxState`/Godot and identify the remaining color/alpha
|
||||
channel consumers. Do not reuse transform Z as alpha.
|
||||
|
||||
##### The opening render path is RETAINED, not immediate-mode (2026-07-08, ground-truth correction)
|
||||
|
||||
@@ -778,14 +772,11 @@ Diagnosed with the new `--gfx-log` compositor/op trace (docs/tools-reference.md)
|
||||
(`0x215`) which returns **-1** for the (correctly-unregistered) CG/effect handles → the fallback at
|
||||
SC0000 `label_12649` does `G[0x62452] = lookup-array-2d(rec[s3]=G[0x3239], G[0x62450], 3, 0)` = **0**
|
||||
because the slot table `rec[s3]`/`G[0x3239]` is **empty**. That table is filled by `call label_125bd`
|
||||
(SC0000 `0x50f`, slots 4..13), which is reached **only through the scene-coroutine framework** — the
|
||||
(SC0000 `0x50f`, slots 4..11), which is reached **only through the scene-coroutine framework** — the
|
||||
`G[0xaba5c]` gate (`0x450`) + op `0x140` (`u0041F9C0`, coroutine LABEL/yield `"LABEL" "J"` @ `0x46d`).
|
||||
**Op `0x140` is stubbed** (SC0000 GAP list) → the coroutine re-entry never routes through `label_125bd`
|
||||
→ slot table stays 0 → all layers collapse into slot 0. This is exactly the "second still-latent gap"
|
||||
flagged above (§"the render drift's second half"), now confirmed as the cause of the visible grey BG on
|
||||
multi-object pages. **Fix = implement the scene-coroutine framework (`0x7b`/`0x7c`/`0x140` + `G[0xaba5c]`
|
||||
gate) so `label_125bd` runs** (or, as a targeted unblock, run `label_125bd`/seed `G[0x3239]` directly).
|
||||
NOT a compositor/z-order/blend bug. Diagnostics: `AGE_DIAG_SETTEX=1` env → VM logs each `set-texture`
|
||||
**Fixed by the bounded scene-coroutine host model:** `0x140` runs the setup body once, `label_125bd`
|
||||
fills the eight slot records, and SC0000 resource `0x23` loads into assigned slot 5 instead of slot 0.
|
||||
This was not a compositor/z-order/blend bug. Diagnostics: `AGE_DIAG_SETTEX=1` env → VM logs each `set-texture`
|
||||
slot operand + `query-gfx-object?` result.
|
||||
|
||||
### Differential offset-path oracle — engine-vs-VM control-flow diff (2026-07-09)
|
||||
|
||||
@@ -52,6 +52,16 @@ Companion op 0x8f `call` is INTRA-script (a local JSR), not cross-script -- see
|
||||
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).
|
||||
- **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).
|
||||
|
||||
### 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.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra: handler FUN_00417cb0 (dispatch ctx[0x26c93+0x7c]). Guards on (ctx[0x6dbc8] & 0x2000000)==0 → throw; else restores PC = ctx[0x53d2c-slot] = ctx[0x53d28]+ctx[0x6dbcc]*4, ctx[0xa0ce4]=ctx[0x6dbc8]&0xfdffffff, clears input state (ctx[0x13bdc]/0xc6f8=-1 etc.).
|
||||
|
||||
### 0x8f `call` (call, argc 1)
|
||||
- **summary:** intra-script subroutine call (local JSR): PC = frame.codebase + operand*4; pushes a return address on the per-frame return stack. NOT cross-script (that is call-script 0x03).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
@@ -64,17 +74,22 @@ This also names the whole call graph statically (build/callscript-names.json).
|
||||
|
||||
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 blocks the VM background thread <duration> ms while the per-frame compositor keeps presenting -> correctly reproduces the explicit one-shot dramatic pauses. NOTE: does NOT pace the rapid opening AE* burst (those draws have no sleep between them; their real pacer is unknown). Headless hosts no-op it (parity).
|
||||
|
||||
### 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.
|
||||
|
||||
## draw
|
||||
|
||||
### 0x1a2 `gfx-cmd-register` (gfx-cmd-register, argc 1)
|
||||
- **summary:** 0x1a2 (handle) — gfx cmd-type 3. Handler gfx_op_0x1a2_registry_insert @0x42d360: builds key '%c%8.8x'(3, operand-desc) and INSERTS operand 1 into the op-0x215 query registry (FUN_0042cf70, open-addressing hash; native stores map[handle]=handle). This is the SOLE populator of the registry op 0x215 queries — the geometry SET/draw ops (0x217/0x219/0x1ff/0x1fb/0x202/...) do NOT register. VM impl: GfxState.Register(handle) (a separate set from the geometry object store). Conflating the two (registering on every GetOrCreate) was the retained-mode geometry bug: CG handles wrongly read back as 'existing' and collapsed to (-400,-600). NOT save/scene (raw Kelebek VA 0x428010 drifted to op 0x1ac save handler). See docs/engine-re.md gfx op-contract table.
|
||||
- **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: real handler FUN_0042d360 (via dispatch table ctx[0x26c93+op]); sets *(ctx+0x53d88+ctx[0x53d14]*0x78)=3, sprintf("%c%8.8x",3,op1), FUN_0042cf70 (hash insert; counterpart of op 0x215 find). NOT save/scene (raw Kelebek VA 0x428010 drifted to op 0x1ac save handler). See docs/engine-re.md
|
||||
- **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) — gfx cmd-type 5. Handler gfx_op_0x1f7_elem_erase @0x422270: ERASES registry handles — if count>1 → gfx_registry_erase_range(handle,count) [erase [handle, handle+count)], else gfx_registry_erase(handle). It is a TEARDOWN/erase, NOT a create (corrects the earlier 'gfx-elem-create' reading). In label_12649 it runs after a 0x215 slot-query, before 0x1fa releases the slot. Objects are created lazily by the geometry SET ops (gfx_object_get_or_create). See docs/engine-re.md gfx op-contract table.
|
||||
- **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
|
||||
- **evidence:** Ghidra handler 0x422270 (dispatch ctx[0x26c93+0x1f7]); count>1 → gfx_registry_erase_range @0x47d8b0 (loops gfx_registry_erase @0x47d850 over [op1,op1+op2)), else gfx_registry_erase(op1). gfx_registry_erase does map.find+erase on the ctx+0x408 registry.
|
||||
- **evidence:** Ghidra handler 0x422270; gfx_object_erase_range@0x47d8b0 loops gfx_object_erase@0x47d850. Both operate on owner+0x408, the retained-object map also used by gfx_object_get_or_create/draw and gfx_object_query_source_slot.
|
||||
|
||||
### 0x1f8 `create-texture` (create-texture, argc 4)
|
||||
- **summary:** Allocate/prepare a texture slot: (slot, width, height, flag). e.g. `create-texture 0xd 0x190 0x1e 0x0` = slot 13, 400x30.
|
||||
@@ -87,7 +102,7 @@ Native handler sleep_op_0xc8 @0x420ec0 is NON-BLOCKING: it arms a timer (sleep_t
|
||||
- **evidence:** SC0000 Frida-confirmed 17/17 (0x25->EV052CA, 0x2e->EV052DB, 0x36->BG030A background); resolution rule validated on 586/595 captured loads. Traced in CG-load subroutine label_12649 as `set-texture G[0x62424] <slot> -1`.
|
||||
|
||||
### 0x1fa `gfx-elem-release` (gfx-elem-release, argc 1)
|
||||
- **summary:** 0x1fa (idx) — gfx cmd-type 3. Handler gfx_op_0x1fa_elem_release @0x4224a0: releases the element at [ctx+0x52bd4 + idx*4] (virtual free, then nulls the slot) + FUN_00474e40(idx). In label_12649 it clears the working slot G[0x62452] after a 0x215/0x1f7 pair. See docs/engine-re.md gfx op-contract table.
|
||||
- **summary:** 0x1fa (surface_slot) — release the surface at ctx+0x52bd4[slot] (virtual free, then null) and call FUN_00474e40(slot). It releases a surface slot, not a retained object handle. SC0000 feeds it the slot returned by op 0x215 after op 0x1f7 erases the associated object group.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra handler 0x4224a0 (dispatch ctx[0x26c93+0x1fa]); frees ctx+0x52bd4[operand1*4] via vtbl, then FUN_00474e40(operand1).
|
||||
|
||||
@@ -134,9 +149,9 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
- **evidence:** Ghidra handler 0x423110; writes obj+0x68/+0x6c from operands 2/3, obj from ctx+0x14d54[operand1*4].
|
||||
|
||||
### 0x215 `query-gfx-object?` (query-gfx-object?, argc 2)
|
||||
- **summary:** 0x215 (out)(handle_id) — native graphics command-buffer op. Real handler FUN_0042a0b0 (Ghidra-resolved via the dispatch table ctx[0x26c93+op]; Kelebek's 0x421160 is VA-drift, lands in an unrelated fn). Does TWO things: (1) writes cmd-type 5 into the CURRENT gfx-object record `[ctx+0x53d88 + ctx[0x53d14]*0x78]` (a command-buffer registration, parallel to op 0x1a2→type 3); (2) returns `out = map.find(handle_id)` over an engine-internal associative registry (found value, else 0xffffffff=not-found), sign-tested (gre/lt 0) to drive label_12649's slot-select branch + set working slot G[0x62452]. So `out` is NATIVE COMMAND-BUFFER STATE (the registry is populated by sibling gfx ops — op 0x1a2→FUN_0042cf70 is the hash insert), NOT the VM global bank → seeding story-state CANNOT reproduce it. Stubbed → constant return → every draw collapses to slot 0 → anchor-preserve reads foreign-sized textures → the cumulative bg/sprite drift. SETTLES the drift as (b) a genuine native op, NOT (a) state-divergence. Faithful fix = model the gfx command-buffer (record array + handle→object registry) and run the gfx ops instead of stubbing — static/Frida-free (handlers now readable; inserts are bytecode-driven). Full decode + verdict: docs/engine-re.md (op 0x215 section). RETAINED-MODE FIX (2026-07-07): the registry MUST be separate from the geometry object store — it is populated ONLY by op 0x1a2, never by the geometry SET/draw ops. VM: QuerySlot returns the registered value (=handle) or -1, NOT a fabricated per-object slot. CG handles are never 0x1a2-registered → query returns -1 → label_12649 takes its FRESH branch (anchor from the INIT2 arrays) → dst=(0,0). The prior GfxState.GetOrCreate-assigns-AcquireSlot model made CG handles read as 'existing' → existing branch called get-texture-size on the wrong slot (0) → dst=(-400,-600) off-screen (the '2nd CG off-screen' bug).
|
||||
- **summary:** 0x215 (out_slot)(handle) — query the retained gfx-object map. Handler gfx_op_0x215_query_source_slot@0x42a0b0 calls gfx_object_query_source_slot@0x47f280 with owner ctx+0x46614. The worker searches owner+0x408, the same map populated by geometry/draw workers, and returns obj+4: the live source-surface slot written by draw-texture, or -1 if absent. A geometry-only object remains unbound and returns -1. SC0000 uses a successful result for existing-object geometry and query-guarded teardown: op 0x1f7 erases the object group and op 0x1fa releases this slot. VM fix 2026-07-09 restores AE001H magic-circle cleanup.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra: real handler FUN_0042a0b0 = {*(ctx+0x53d88+ctx[0x53d14]*0x78)=5; out=FUN_0047f280(FUN_0041b940(2))}. FUN_0047f280 = std::map::find (returns mapped value or 0xffffffff); FUN_0041b940(2) = operand-fetch of operand 2 (the handle key); FUN_00425fb0(1,val) = operand-write to `out`. Registry populated by op 0x1a2 handler FUN_0042d360 → FUN_0042cf70 (open-addressing hash insert). Bytecode sites: SC0000 label_12649 (0x12670) + label_123ef (0x12419/0x12450), handle-ids from 0x62455[idx] (±offset); result gre/lt 0 branches slot-select. Record table 0x3239 (label_125bd @0x0050f) assigns per-object slots 4..13.
|
||||
- **evidence:** Ghidra asm: handler 0x42a0b0 passes ECX=ctx+0x46614 to 0x47f280. That worker searches ECX+0x408 and returns resolved object+4. gfx_object_bind_draw@0x47e870, called with the same owner, get-or-creates in owner+0x408 and writes source slot to object+4. SC0000 post-effect cleanup 0x3321 queries G[0x62457], sign-tests, then executes 0x1f7(handle,10) + 0x1fa(returned_slot); AE001H res 0x37 was bound to that handle.
|
||||
|
||||
### 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.
|
||||
@@ -164,12 +179,14 @@ Native handler gfx_op_0x20c_present_frame (dispatch ctx[0x26c93+0x20c]) -> gfx_r
|
||||
- **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].
|
||||
|
||||
### 0x21e `set-anim-transform-norm` (set-anim-transform-norm, argc 6)
|
||||
- **summary:** (handle)(p1)(p2)(x)(y)(z) — set sprite transform channel, NORMALIZED (float operands /_DAT_00571c28 ~percent); cmd-type 0xd, worker gfx_anim_set_channel@0x47eaa0. SC0000 opening @0xf73+ on INIT2 CG handles. Cluster 0x21c-0x243. Handler 0x423350; Kelebek VA 0x421450 is drift.
|
||||
- **summary:** (handle)(delay)(duration)(sx)(sy)(sz) — set the normalized SCALE-matrix channel. Handler 0x423350 normalizes sx/sy/sz; gfx_object_set_scale_channel@0x47eaa0 stores timing at obj+0x3c/+0x50 and constructs a diagonal 4x4 scale matrix at obj+0xac. Separate from op 0x220 translation; neither channel is opacity.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra 0x47eaa0 calls matrix builder 0x48af1d, which writes sx/sy/sz to diagonal entries 0/5/10 and identity entry 15. gfx_object_apply_transform_channels@0x472f00 consumes this independently of obj+0x1ac.
|
||||
|
||||
### 0x220 `set-anim-transform-abs` (set-anim-transform-abs, argc 6)
|
||||
- **summary:** (handle)(p1)(p2)(x)(y)(z) — set sprite transform channel, ABSOLUTE (raw floats); cmd-type 0xd, worker 0x47ecc0. Twin of 0x21e. SC0000 opening @0x18a5+ on INIT2 CG handles. Cluster 0x21c-0x243. Handler 0x4234e0; Kelebek VA 0x4215D0 is drift.
|
||||
- **summary:** (handle)(delay)(duration)(tx)(ty)(tz) — set the absolute TRANSLATION-matrix channel. Handler 0x4234e0 passes raw floats to gfx_object_set_translation_channel@0x47ecc0, which stores timing at obj+0x44/+0x58 and constructs an identity 4x4 matrix with translation at obj+0x1ac. Separate from op 0x21e scale; neither channel is opacity.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra 0x47ecc0 calls matrix builder 0x48afb1, which writes identity diagonal and tx/ty/tz to entries 12/13/14. gfx_object_apply_transform_channels@0x472f00 combines this independently of obj+0xac.
|
||||
|
||||
### 0x228 `u00421940` (u00421940, argc 5)
|
||||
- **summary:** 0x228 query-position (succ)(handle)(outX)(outY)(outZ): read the object's current computed position into vars (worker FUN_0047cdd0). C# VM: writes V24 + success flag. See docs/engine-re.md §SC0000 anim cluster.
|
||||
@@ -444,14 +461,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
|
||||
|
||||
### 0x7b `u0041ADB0` (u0041ADB0, argc 2)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x7c `u00416A90` (u00416A90, argc 0)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x7f `u00414C60` (u00414C60, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -684,10 +693,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=med
|
||||
|
||||
### 0x140 `u0041F9C0` (u0041F9C0, argc 4)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x141 `u0041FAA0` (u0041FAA0, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -429,7 +429,7 @@ anchor doubling — a geometry issue independent of timing.)
|
||||
**Open (the real burst pacer):** what advances the rapid opening CG/AE\* burst frame-to-frame is **unknown** —
|
||||
not `sleep`, not `present-frame` (only 2× in the whole scene), not the coroutine ops (absent from the burst).
|
||||
Next: profile the **real Godot run** (`--trace-histogram`) of SC0000's `0x3958–0x3973` loop + gfx-op sequence.
|
||||
The scene-coroutine framework (`0x7b`/`0x7c`/`0x140` + `G[0xaba5c]` gate + `label_125bd`) remains deferred.
|
||||
The scene-coroutine framework was deferred here; the bounded host model is completed below (2026-07-09).
|
||||
|
||||
### Diagnostics framework extended (2026-07-08)
|
||||
|
||||
@@ -598,7 +598,7 @@ fixed" with "opening looks right."
|
||||
**Next candidates (deferred).** (a) Graphics geometry/blend fidelity — AE* alpha/blend + per-frame
|
||||
compositing + cold-object anchors (the thing that makes the paced opening actually *look* right). (b) Wire
|
||||
the Ctrl `Speed` multiplier (ADV-mode-scope RE). (c) Full scene-coroutine framework (`0x7b`/`0x7c`/`0x140`)
|
||||
for interactive multi-object scenes. (d) Model `0xcd get-input-type` (name-entry interactivity, the separate
|
||||
for interactive multi-object scenes (**completed below, 2026-07-09**). (d) Model `0xcd get-input-type` (name-entry interactivity, the separate
|
||||
input gap noted above).
|
||||
|
||||
### A2b — Blend & transparency (slice A) ✅ DONE (2026-07-08)
|
||||
@@ -651,3 +651,47 @@ plan `.../plans/2026-07-08-sc0000-anim-transform-cluster.md`, branch `feat/anim-
|
||||
`0x236` timed/movie op, and the rare `0x21c/0x21d/0x224/0x242/0x243/0x23d/0x20a/0x20e` tail. Adjacent
|
||||
non-cluster gaps remain: `draw-string 0x204`×205 (on-screen text) and `play-sound-effect`. **Whole-scene
|
||||
visual validation is the user's call** (they deferred confirmation until the scene is coherent).
|
||||
|
||||
### A2b -- Scene-coroutine host model (2026-07-09) -- DONE
|
||||
|
||||
The native mechanism is fully reversed in `docs/engine-re.md` under Scene-coroutine framework.
|
||||
The port deliberately models its observable ADV lifecycle instead of emulating the runtime-resolved
|
||||
video service behind op `0x140`.
|
||||
|
||||
**Bounded model for this slice:**
|
||||
|
||||
1. Detect only the corpus-wide ADV form `0x140 out "LABEL" "J" in`. `TITLE.BIN`'s unrelated
|
||||
`"BIN" "SC????.BIN"` use remains unmodeled and must not acquire ADV scene-entry behavior.
|
||||
2. On a top-level entry at offset zero, synthesize native scheduler state `G[0xaba5c]=1` for a script
|
||||
containing that ADV form. A captured global-write snapshot cannot supply it because the native scene
|
||||
loader does not set it through the script operand-write helper.
|
||||
3. At each ADV labeled-yield site, force exactly one setup-body iteration, then return the terminal value
|
||||
encoded by the site's following `mov terminal, immediate` + `eq terminal, out` sequence. This handles a
|
||||
stale prior-scene `out` value and avoids hardcoding SC0000's `0x45e`; all 138 corpus ADV sites share the
|
||||
same shape.
|
||||
4. Record op `0x7b`'s two saved handler PCs as frame metadata. Consume op `0x7c` as the host-scheduler
|
||||
resume marker: the port's existing `IHost.FrameYield`/`FrameClock` path supplies per-frame pacing, so it
|
||||
does not recursively execute the native render/poll/yield bytecode handlers.
|
||||
|
||||
**Acceptance gates:** a synthetic stale-terminal scene runs its setup body once and reaches content; a
|
||||
non-ADV `0x140` remains unchanged/stub-reported; real SC0000 executes op `0x140` twice, clears
|
||||
`G[0xaba5c]`, and fills the slot-table columns (`G[0x3239..0x324e] = 4..11`) before content. Then run the engine
|
||||
suite, corpus sweep, Godot self-test, and live/pixel validation of the previously grey multi-layer page.
|
||||
|
||||
**Result.** The bounded model landed in `VirtualMachine`/`ExecFrame` with four focused tests. SC0000 now
|
||||
executes `0x140` twice, runs `label_125bd` once, clears the native entry gate, and loads resource `0x23`
|
||||
into assigned slot 5 rather than the broken slot 0. Static corpus validation found 138 ADV sites and zero
|
||||
shape mismatches; the one non-ADV `TITLE.BIN` site remains stub-reported.
|
||||
|
||||
**Verified:** engine **85/85**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot threaded
|
||||
self-test `SELFTEST OK` (3 lines, full handling). The native transition timing remains host-approximated.
|
||||
SC0000 coverage is now **77/129 handled (59.7%)**, with 52 GAP ops / 613 GAP instructions.
|
||||
|
||||
**Follow-up — magic-circle teardown fixed (2026-07-09).** Ghidra caller analysis corrected op `0x215`:
|
||||
it queries the retained gfx-object map and returns `obj+4`, the source surface slot written by
|
||||
`draw-texture`; it does not query op `0x1a2`'s descriptor hash. The old host model returned -1 for CG
|
||||
handles, so SC0000 skipped its explicit `0x1f7(handle,10)` + `0x1fa(slot)` cleanup and left
|
||||
`AE001H.AGF` (resource `0x37`) visible. `GfxState.QuerySlot` now returns the bound source slot,
|
||||
`0x1f7` erases retained objects, and `0x1fa` clears the surface. A booted SC0000 integration regression
|
||||
asserts no visible resource `0x37` remains; engine suite **86/86**. **Live clicked-path validation
|
||||
confirmed the fix on 2026-07-10:** the magic circle now disappears at the intended transition.
|
||||
|
||||
@@ -240,7 +240,7 @@ Expected: named imports carry parameter types.
|
||||
|
||||
- [ ] **Step B8: Save, verify annotations intact, update docs, commit**
|
||||
|
||||
Confirm a sample of pre-existing renames/plate comments still present (`get_function_by_address` on `gfx_op_0x215_register_query`, `sleep_op_0xc8`). Run `mcp__ghidra__save_program`.
|
||||
Confirm a sample of pre-existing renames/plate comments still present (`get_function_by_address` on `gfx_op_0x215_query_source_slot`, `sleep_op_0xc8`). Run `mcp__ghidra__save_program`.
|
||||
Edit `docs/engine-re.md` (runbook: IAT reconstruction is done; note graft vs fallback) and `docs/tools-reference.md` (`apply_imports.py`).
|
||||
|
||||
```bash
|
||||
|
||||
@@ -71,21 +71,18 @@ Expected: an explicit STRONG/WEAK verdict with the number.
|
||||
|
||||
- [ ] **Step 1: Seed `vm-map/lib-functions.toml` with the known library functions**
|
||||
|
||||
Include the library functions we've identified in RE (grep `engine-re.md` for `std::map`, `FUN_0047f280`, `FUN_0042cf70`, hash insert/find, `operator new`, etc.). Start conservative — only addresses we're confident about:
|
||||
Include only actual library functions identified in RE. **Correction (2026-07-09): do not seed
|
||||
`0x47f280` or `0x42cf70` here** — later caller/field analysis proved they are engine-purpose-specific
|
||||
`gfx_object_query_source_slot` and `vm_lvalue_descriptor_hash_insert`, not generic STL helpers.
|
||||
Start conservative — only addresses and roles we're confident about:
|
||||
```toml
|
||||
# vm-map/lib-functions.toml -- CANONICAL registry of identified statically-linked library functions.
|
||||
# Generated: build/lib-functions.json + docs/lib-functions-reference.md via tools/lib_functions_build.py --build.
|
||||
# Applied to /v2 via run_script_inline (rename; never clobbers USER_DEFINED). Grows as we identify more.
|
||||
[[func]]
|
||||
address = 0x47f280
|
||||
name = "std_map_find"
|
||||
note = "std::map::find over the gfx object registry (op 0x215 handler calls it; returns value or 0xffffffff)"
|
||||
source = "native-RE"
|
||||
confidence = "high"
|
||||
[[func]]
|
||||
address = 0x42cf70
|
||||
name = "gfx_registry_hash_insert"
|
||||
note = "open-addressing hash insert into the op-0x215 query registry (op 0x1a2 handler calls it)"
|
||||
address = 0x5502be
|
||||
name = "operator_new"
|
||||
note = "VC9 operator new; malloc + new-handler retry + bad_alloc"
|
||||
source = "native-RE"
|
||||
confidence = "high"
|
||||
```
|
||||
@@ -111,7 +108,8 @@ Expected: N renamed (or skipped if we'd already named them), 0 clobbers.
|
||||
|
||||
- [ ] **Step 6: Validate + commit**
|
||||
|
||||
`decompile_function 0x42a0b0` (`gfx_op_0x215_register_query`) — its registry-find call should read `std_map_find(...)`.
|
||||
`decompile_function 0x42a0b0` (`gfx_op_0x215_query_source_slot`) — verify its purpose-specific
|
||||
`gfx_object_query_source_slot(...)` call was not clobbered by the library pass.
|
||||
```bash
|
||||
git add vm-map/lib-functions.toml tools/lib_functions_build.py tools/test_lib_functions.py docs/lib-functions-reference.md
|
||||
git commit -m "re: curated library-function registry (lib-functions.toml) + apply"
|
||||
|
||||
@@ -35,7 +35,7 @@ where `disp = 0x9b24c + op*4` (word index `0x26c93 + op`; e.g. `ctx[0x26e3f]=0x4
|
||||
3. For each real `(op, handler_va)`:
|
||||
- `create_function` at `handler_va` if none exists.
|
||||
- **Preserve good names:** if the function already has a non-`FUN_`/`LAB_` name (e.g.
|
||||
`gfx_op_0x215_register_query`, `sleep_op_0xc8`), do **not** rename — only ensure a plate comment
|
||||
`gfx_op_0x215_query_source_slot`, `sleep_op_0xc8`), do **not** rename — only ensure a plate comment
|
||||
records the opcode. Rename only raw `FUN_xxxx`/`LAB_xxxx` → `op_0xNN_handler`.
|
||||
- `set_plate_comment`: `opcode 0xNN dispatch handler; resolved via ctx[0x26c93+op] in FUN_00413860`.
|
||||
4. Emit `build/op-handler-map.json` (`{ "0x1ac": {"handler": "0x427fb0", "name": "..."}, ... }`).
|
||||
|
||||
@@ -51,8 +51,9 @@ is a known library function." **Small by design** (a handful to low-dozens), not
|
||||
|
||||
- Task 1: report the newly-named count; spot-check 2–3 (e.g. does `FUN_0047f280` now carry a
|
||||
`std::map`-family name? does a known `operator new` site read named?).
|
||||
- Task 2 (if taken): decompile `gfx_op_0x215_register_query` — its `gfx_registry_map_find(...)` /
|
||||
`FUN_0047f280` call should read as the curated `std_map_find`; `--lint` clean; 0 clobbers.
|
||||
- Task 2 (if taken): do not classify `0x47f280` or `0x42cf70` as generic STL helpers. Later RE proved
|
||||
they are purpose-specific `gfx_object_query_source_slot` and `vm_lvalue_descriptor_hash_insert`;
|
||||
decompile `gfx_op_0x215_query_source_slot` to verify the former remains named; `--lint` clean; 0 clobbers.
|
||||
- `save_program` succeeds.
|
||||
|
||||
## Scope & boundaries
|
||||
|
||||
Reference in New Issue
Block a user