docs(gfx): render drift RESOLVED (native ops + system boot); align living docs
engine-re.md: 0x1f7 erase correction + 'The render drift's SECOND half' (missing INIT2 boot state via SYSTEM4) + gfx-command-buffer backlog marked DONE + worker annotations noted. phase-a-slice-plan A2b: drift RESOLVED (both halves; CGs render screenshot-confirmed; residual = AE* alpha/blend + cold anchors). tools-reference: gfx --boot + godot --boot. (Status memory + MEMORY.md + CLAUDE.md updated on disk.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ subsystem oracles. Test scenes are **synthesized** via `Age.Engine/Sys4/ScriptAs
|
||||
| `run <file.BIN>` | 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 <out.json>` | 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 <SCENE.BIN> [0xADDR=VAL…]` | Dump executed `play-bgm`/`play-voice` in order + resolved file. | optional seeds. provider-less (stub) for now. |
|
||||
| `gfx <SCENE.BIN> [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] <SCENE.BIN> [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 <f>] [--save-state <f>] <SCENE.BIN…> [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 <NAME>` — 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 <png> [--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
|
||||
|
||||
Reference in New Issue
Block a user