diff --git a/docs/engine-re.md b/docs/engine-re.md index ea0ea6a..dd45736 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -188,7 +188,7 @@ 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 | set | 2 | element create/select by handle op1; `op2>1` → `FUN_0047d8b0(op1,op2)` (array), else `FUN_0047d850(op1)` | +| `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)` | | `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` | @@ -227,6 +227,29 @@ 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 +`gfx_object_init_default`@`0x472810`), the setters `gfx_set_vec18/24/16c`(`0x47e960/e910/e800`), the getters +`gfx_get_vec18/24`(`0x47f360/f2e0`). + +### The render drift's SECOND half: missing system-boot state (2026-07-07, resolved) + +Implementing the gfx ops (above) was necessary but not sufficient — a cold single-scene run of SC0000 still +drifted. Runtime tracing found **the CG handle array `G[0x62455..0x6245c]` was all zeros**, so every CG +collapsed onto object `0` and its geometry accumulated. Those handles are set by the **boot script `INIT2`** +(mov `0x62455=0xcb20 … 0x6245c=0xcbc0`), which is call-scripted by the real entrypoint **`SYSTEM4.BIN`** +(`LOADCONFIG → INITCONFIG → INIT2 → LOGO → OP → INIT → TITLE → …`). Our harness teleports straight into +SC0000, skipping that boot. **Fix:** run the system-boot state prefix (`INITCONFIG/INIT2/INIT`, skipping the +UI scripts) before the scene — `Age.Cli gfx --boot` and Godot `--boot` (both via `GameSession`). With boot, +the CGs **de-collapse and render correctly** (screenshot-confirmed). **This is the synthesis of the old +(a)-vs-(b) debate: the drift needed BOTH the native gfx ops (b) AND boot state (a) — specifically INIT2's +handle array, never before identified (it is not a story flag).** Note two distinct boots: our Phase-B +`--boot` runs the *data* `*INIT` scripts (skills/items/…); this is the *system* boot (`SYSTEM4` prefix) — a +"full boot" should run both. **Residual:** the `AE*` fade/flash effects still draw opaque (alpha/blend +deferred — Phase 2 scope), and some object-slot CGs start with a zero anchor (cold gfx objects vs the real +game's warm ones; default object geometry is confirmed `(0,0)` in `gfx_object_init_default`, so it is not a +missing-default bug). See `docs/phase-a-slice-plan.md` A2b-Geometry. + --- ## Native walls backlog (targets for this loop) @@ -236,9 +259,7 @@ correct `dst`/`w`/`h`. Ancillary per-object tables observed: `ctx+0x14d54` (obj via `call-script`/the same SYS4INI-index loader, so the open question is only where the decision value is turned into a scene *id* (a caller of SCJUMP; re-aimed away from `u00428010`). - **op `0x60`** (`u0041A270`) — the rand-like value gating 1732/1755 SCJUMP decisions. -- **gfx command-buffer** — the `0x212–0x21a` positioned-object subsystem (`scjump`-unrelated; the - rendering drift). **Understood, not unidentified** (see the op `0x215` finding above): it's a native - record array + a handle→object hash registry that the gfx ops insert/query/write. The drift verdict is - settled — (b), a genuine native op, not state-divergence. Remaining work is *implementation*, not RE: - model the registry + record array host-side and execute the gfx ops (`0x1a2`/`0x215`/`0x212–0x21a`) - instead of stubbing — static, Frida-free. +- ~~**gfx command-buffer**~~ — **DONE** (the `0x212–0x21a` positioned-object subsystem = the rendering + drift): all 14 ops reversed + implemented against a host-side `GfxState`, and the missing INIT2 boot + state supplied via `--boot`. CGs render (screenshot-confirmed). See the op `0x215` finding + "The render + drift's SECOND half" above. Remaining: `AE*` alpha/blend (deferred) and cold-object anchors. diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index 60061d9..ca43200 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -273,8 +273,8 @@ measured `0×0`, and the anchor-preserve math (`base' = center − (w_new/2, h_n corruption. Fix: seed `_slotDims[0] = (800,600)` (and record `create-texture(w,h)` dims) so the first CG's anchor stays an identity. This is the faithful stand-in for the skipped boot-time primary-surface creation. -**Post-opening bg/sprite drift — root cause SETTLED as (b) a native command-buffer op via Ghidra -(2026-07-07). ⚠ Corrects an earlier wrong "state-divergence" verdict here.** Symptom (screenshot +**Post-opening bg/sprite drift — ✅ RESOLVED (2026-07-07): native gfx ops + missing INIT2 boot state.** +⚠ Corrects an earlier wrong "state-divergence-only" verdict here. Symptom (screenshot `Screenshot 2026-07-06 211353.png`): everything blits through slot 0 as an immediate-mode canvas; the anchor-preserve base globals **accumulate drift** across differently-sized textures (`BG030A→(300,500)`, next→`(450,100)`, →`(800,350)`… marching bottom-right; the background ends up pinned off-centre / bottom-right @@ -289,12 +289,24 @@ story-state **cannot** fix it. So this is **(b) a genuine native op**, *not* (a) 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. -**Fix (Frida-free, implementation not RE):** model the gfx command-buffer host-side (record array + -handle→object hash registry) and *execute* the gfx ops (`0x1a2`/`0x215`/`0x212–0x21a`) instead of stubbing; -the inserts are bytecode-driven, so the state rebuilds from the same scripts. Fades/alpha (`AE*`, -`0x202/0x203`) + green chromakey + true multi-surface remain deferred; the compositor is built to accept alpha -later. The full-screen event-CG opening path (`EV052*` at `(0,0)`) is correct and unaffected — the drift is -specifically the `BG*`/sprite draws routed through `label_12649`. +**Resolution had TWO halves** (canonical decode in `docs/engine-re.md`, op `0x215` + "The render drift's +SECOND half"; don't duplicate here): +1. **Native gfx ops (b):** all 14 command-buffer ops (`0x1a2`,`0x1f7`,`0x1fa`,`0x1ff`,`0x202`,`0x203`, + `0x212`,`0x213`,`0x215`–`0x21a`) reversed + implemented against a host-side `GfxState` (VM execution + state; `engine/Age.Engine/Model/GfxState.cs`). `0x215` now returns distinct per-object slots. +2. **Missing system-boot state (a):** the CG handle array `G[0x62455..]` is set by boot script **INIT2** + (via entrypoint `SYSTEM4.BIN`), which a cold single-scene run skips → all CGs collapsed onto object 0. + Supplied via **`Age.Cli gfx --boot`** and **Godot `--boot`** (run `INITCONFIG/INIT2/INIT` through + `GameSession` first). So the drift needed BOTH — not story flags, and not native-ops-alone. + +**Result: with `--boot`, the opening event CGs render correctly** — screenshot-verified live in Godot +(`--path godot -- --boot`; the CGs that were entirely missing now fill the frame). **Residual (deferred, not +regressions):** `AE*` fade/flash effects draw opaque (alpha/blend deferred — a white "explosion" glow that +should fade stays); some object-slot CGs start with a zero anchor (cold gfx objects vs the real game's warm +ones — default object geometry is confirmed `(0,0)` in `gfx_object_init_default`, so not a missing default). +Next visual chunk = **alpha/blend + effect fading** (`0x202/0x203` already store the packed color) + per-frame +compositing. NOTE the two-boot gap: our Phase-B `--boot` runs *data* `*INIT` scripts; this added the *system* +boot — a "full boot" should run both. --- diff --git a/docs/tools-reference.md b/docs/tools-reference.md index 9f38a36..49d4c2e 100644 --- a/docs/tools-reference.md +++ b/docs/tools-reference.md @@ -81,7 +81,7 @@ subsystem oracles. Test scenes are **synthesized** via `Age.Engine/Sys4/ScriptAs | `run ` | Execute a script; print steps, show-text count, **call-script dispatch count**, the first 30 lines (each tagged with its source script), and the distinct source scripts. | `CaptureHost` (headless); **executes call-script**. | | `trace ` | Trace every SC/SP scene → offsets + halt + steps. **Provider-less** (call-script stubbed) = a base-ISA offset dump. | writes JSON. (Was the vm0 differential oracle; vm0 is retired from oracle duty — `TraceDiffTests` removed.) | | `audio [0xADDR=VAL…]` | Dump executed `play-bgm`/`play-voice` in order + resolved file. | optional seeds. provider-less (stub) for now. | -| `gfx [0xADDR=VAL…]` | Dump executed `set-texture`/`get-texture-size`/`draw-texture` with resolved file + computed geometry (the headless geometry oracle). | optional seeds. provider-less (stub) for now. | +| `gfx [--boot] [0xADDR=VAL…]` | Dump executed `set-texture`/`get-texture-size`/`draw-texture` (resolved file + computed geometry) **plus the per-object gfx slots** — the headless geometry oracle. **`--boot`** runs SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` first (so INIT2's gfx handle array is present) and runs the target with call-script on; without it, seeds-only + provider-less. | gfx ops now execute against `GfxState`. | | `play [--boot] [--state ] [--save-state ] [0xADDR=VAL…]` | ★ Cross-scene **state runner**: run a scene sequence carrying persistent globals. `--boot` first runs the 9 `*INIT` data scripts (real skill/item/unit/map/stage state). `--state`/`--save-state` load/persist a JSON snapshot. | `GameSession`; **executes call-script**. | | `sweep [--boot] [0xADDR=VAL…]` | Corpus-scale run. **With call-script execution on: 284/297 exit, 13 STEP-LIMIT** (input/state-gated ADV scenes spin headless once subroutine global-writes drive their loops — state divergence, not a bug; 0 depth-cap/unresolved). **With seeds = a story-state explorer**: reports which scenes' dialogue changes ±seed (e.g. form flag `0xa57=1` → 34/297 scenes). | | @@ -100,6 +100,7 @@ texture ops (no GPU context) — run windowed for real scenes. User args (after - `--scene ` — which scene to play (default `SC0000`), e.g. `--scene SC0240` (executes 29 nested subroutines). - `--selftest` — headless; runs a **synthesized** scene through the thread/suspend/`CallDeferred` plumbing and asserts it matches a live headless run (full handling; no vm0/frozen golden). Exits. - `--seed 0xADDR=VAL` (repeatable) — seed initial global state, e.g. `--seed 0xa57=1` unlocks Lily's form-A voiced dialogue. +- `--boot` — run SYSTEM4's state prefix (`INITCONFIG/INIT2/INIT`) via `GameSession` before the scene, so scene-assumed boot state (chiefly INIT2's gfx handle array) is present. **Needed for the gfx CGs to render** (without it the opening event CGs collapse/drift). e.g. `godot --path godot -- --boot`. - `--shot [--shot-page N]` — capture page N to a PNG then quit (dev screenshot). At scene end it also prints the call-scripts executed as nested frames. ## Asset resolution / graphics