Files
OpenMaidEngine/docs/phase-a-slice-plan.md
2026-07-20 11:29:12 -04:00

1980 lines
152 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase A — Vertical Slice Plan (the first build step)
Concrete execution plan for Phase A of `remake-architecture-and-roadmap.md`. Decided over the
alternative (fully decoding `SCJUMP.BIN`) after recon showed SCJUMP is not the gating unknown.
## Why the slice, and why headless-first
**SCJUMP recon (2026-07-06):** `SCJUMP.BIN` is a 29,796-instruction **progression state machine**,
not the `call-script` registry. Top level switches on `global 0x3234` (mode 19 → big blocks); each
block is nested `eq`/`ne`/`and`/`jcc` on flags, ending in `mov`s to output globals. Almost no
`call-script`. So it decides *what scene/branch comes next* via state, and does **not** resolve
`call-script id → code`. Consequence: the id→code registry stays engine-level (deferred), **but the
slice can stub `call-script`** — it is not gating for running one scene's dialogue.
**Correctness bootstrap (roadmap §5) drives the ordering:** the VM must be *validated-correct* before
it is trustworthy. Our strongest oracle is `build/text/dialogue.jsonl` (the `show-text` lines per
script). So the very first slice is **headless and text-only, validated by that oracle** — no Godot,
no AGF, no audio, no dispatch registry. Only once the VM reproduces dialogue do we add rendering.
Phase A therefore splits:
- **A0 — headless VM, dialogue-validated (Python prototype).** ← immediate, executable now.
- **A1 — port the validated model to C#** (the runtime's VM core).
- **A2 — Godot ADV backend** (render one scene with visuals + voice).
---
## A0 — Headless VM validated by the dialogue oracle
**Goal:** a Python interpreter that executes one ADV scene's bytecode and emits its `show-text`
sequence; that sequence is a coherent, in-order subsequence of the script's static `dialogue.jsonl`
lines. This proves the execution model — control flow, operand/pointer semantics, string handling,
and the no-op-marker assumptions — *before* any C#/Godot investment. Reuses `tools/sys4load.py` for
all parsing/decoding (no new parser).
### Execution model to implement
- **Memory:** one flat **global bank** = `dict[int,int]` (globals are raw offsets into one space;
`global-int A``G[A]`, default 0). Per-call **local frame** with typed banks sized by header
F0F5 (`local_int[F0]`, `local_float[F1]`, `local_string[F2]`, …).
- **PC / control flow:** build `offset→instruction-index` map from `sys4load` instructions (each has
`.offset` = dword index; jump targets are dword indices). `jmp t` → pc = map[t]. `jcc(cond, A, B)`
→ cond truthy ? goto A : goto B, where `0xffffffff` = fall through (confirmed model from RECOVER).
- **Operand resolution by type:** imm→value; global-int→`G[value]`; local-int→`frame.int[value]`;
string(2)→decoded string at dword offset; float/global-string/etc. analogous.
- **⚠ Pointer/lvalue semantics — the key modeling task.** RECOVER proves `-ptr` operands are
*lvalues*: `lookup-array(dst_ptr, base, idx)` yields a *reference* to `G[base+idx]`; `mov` through a
ptr writes to the referenced cell; reading a ptr rvalue dereferences it. Model a ptr slot as holding
an address into the global bank; nail this so the RECOVER array-copy produces correct results (unit
test it directly).
- **Opcode handlers (~52 named ops):**
- arithmetic/bit `add sub mul div mod and or sar shl``p1 = p2 ⊙ p3`.
- compares `eq ne lt lte gr gre` → 0/1.
- `mov` (incl. through ptr), `lookup-array` (`p1=mem[base+idx]`), `lookup-array-2d`
(`p1=mem[base + i*stride + col]`), `copy-to-global`, `set-array-to`, `bit-set/reset`, `check-bit`.
- control `jmp call jcc ret exit exit-script`.
- string `set-string concat strlen toString`.
- **ADV capture:** `show-text` → append (arg text) to the emitted list; `end-text-line`,
`wait-for-input`, `set-font`, `comment` → capture/skip (no visible state).
- **Markers → no-op (this TESTS the classification):** `0x1f4 0x1f5 0x1d5 0x1bc 0x1bf` skip;
tentative `0x21b 0x1d2 0x258` skip — if dialogue stays correct, the no-op assumption is validated.
- **`call-script` → STUB:** log `(id)`, return immediately. (Its dialogue belongs to other scripts;
stubbing keeps the emitted set = this script's own lines.)
- **Effectful (draw/texture/audio/ui/input) → STUB:** log and ignore.
- **Unknown/other opcodes → log + no-op**, so a rare op doesn't halt the run (record coverage).
### Oracle & scene choice
- **Oracle:** with calls stubbed and default state, every emitted `show-text` line must be a real
decoded string from the script's pool, and the sequence must be an **in-order subsequence** of that
script's `dialogue.jsonl` lines (≈ equality for a linear scene). Catches: garbage strings (bad
operand/ptr handling), impossible ordering (bad control flow), missing/extra lines.
- **Scene pick:** choose a **short, mostly-linear ADV scene** — high `show-text` count, low `jcc`
density, few `call-script`. Selection step: rank `SC####`/`SP####` by
`(show-text count) / (jcc + call-script count)`, small size. Known-good fallback: `SC0030.BIN`
(dialogue verified). Also run a **RECOVER unit test** to validate pointer/array semantics independent
of dialogue.
### Steps
1. `tools/vm0.py`: load a script via `sys4load`, build offset→index map, frame + global bank.
2. Implement operand resolution + the arithmetic/compare/mov/lookup/control handlers; unit-test on
`RECOVER.BIN` (array copy + both loops must produce correct global writes).
3. Add ADV capture + markers-as-noop + call/effectful stubs; add opcode-coverage logging.
4. Run on the chosen linear scene; diff emitted `show-text` vs `dialogue.jsonl` (subsequence check);
eyeball the first ~15 lines for coherence.
5. Iterate until several scenes pass; record which ops/markers were exercised and any surprises
(esp. whether the tentative-no-op markers hold).
### Success criteria (A0 done)
- RECOVER unit test passes (pointer/array model correct).
- ≥3 ADV scenes: emitted `show-text` is a coherent in-order subsequence of their `dialogue.jsonl`,
no garbage strings.
- Coverage report of which opcodes actually executed (drives A1/A2 priorities).
- The no-op-marker assumption is confirmed or corrected with evidence.
---
### A0 result (2026-07-06) — execution model VALIDATED
`tools/vm0.py` built (reuses `sys4load`; ~250 lines). Results:
- **RECOVER unit test PASSES** — all 7 checks (block-1 3-field copy, block-2 restore + flag, both
skip-guards). The pointer/lvalue model, 2D stride indexing, both loops, and two-way `jcc` all
execute correctly. **The core execution model is proven.**
- **Full SC/SP oracle sweep (`vm0.py --sweep`): 282 / 294 scenes DIALOGUE-VALID = 95.9%.** Every
emitted `show-text` line is checked (by string offset) as an in-order subsequence of the script's
static `dialogue.jsonl` lines. **Zero STRAY and zero ORDER violations across all 294 scenes** — the
model never emits a garbage string and never emits dialogue out of order. 279 CLEAN (valid + natural
`exit`); 3 OK/LOOP (valid subsequence, halted by the loop-guard); 12 EMPTY; 3 skipped (no static
show-text). SC0000 = 326 static / clean; SP0062 = 220/220 CLEAN.
**A0-remainder work done (2026-07-06, session 2):**
- **Loop-guard added** (`EMIT_CAP=2`): halt a run once any single line is re-emitted a 3rd time —
a semantic guard tied to the oracle (vs. a blind step limit), and it *classifies* the scene LOOPED
instead of spewing garbage. The 3 zero-state spinners (SC0010/SC0600/SC0200) now terminate cleanly
in <12k steps and their emitted lines are all valid.
- **SP0062 "stray" was a measurement artifact**, not a bug — the precise offset-based oracle shows it
CLEAN (220/220, natural exit). Offset-match ⟹ text-match (VM decodes each string at the same offset
the extractor did), so CLEAN is trustworthy.
- **`0x71` (label-def) folded into the no-op marker set** — structural, no runtime effect.
- **Sweep + single-scene diff harness** added to `vm0.py`: `--sweep [N]` (coverage table over all
SC/SP), `--scene NAME` (detailed diff for one script), plus `load_oracle`/`subsequence_status`.
**op 0x90 investigated in depth — it is input chrome, NOT a correctness hole** (full evidence:
`vm-map/opcodes.toml` op 0x90 `details`). Kelebek left it "ukn"; corpus analysis resolves it:
`0x90 x y w h tgt_a tgt_b tgt_c` (argc 7) is a **cursor/input hotspot hit-test** that branches per
interaction outcome and **falls through to pc+1 when nothing matches** (design-confirmed: enc.len 15
lands the next instr on the fall-through statement). It occurs ONLY in a shared ADV-chrome subroutine
that is byte-identical in all 301 ADV scripts — **exactly 8 sites each** (5 immediate-rect buttons at
`(684..772, 572)` toggling `G[0x6c9..0x6cd]` + 3 local-operand keyed forms), **zero scene-specific
use**. Headless (no cursor/input) ⇒ fall through ⇒ **vm0's stub is already correct**, proven safe by
all 279 CLEAN scenes (which contain these same 8 sites). `op 0x97` (argc 5, no targets) is its
companion register-hotspot call. **So 0x90 stays as fall-through in A1 with confidence; it is modelled
as a live hotspot test only in A2** (Godot input backend), confirming target→state mapping via Frida.
**The 12 EMPTY scenes — state-gated interactive screens, not a model failure.** Traced SC0830: it
exits early because `G[0xaba5c]==1` gates the content; past that gate the dialogue sits behind the ADV
input-wait loop (the hotspot-polling chrome above), so with no seeded state and no input the scene
exits or spins before reaching text. Unlocking them = seed per-scene state + supply input →
**Phase A2/B**, not an A0 model fix.
**⚠ Honest scope of the 95.9%:** the subsequence oracle proves **no-garbage / in-order**, not a
*complete* path — inherent to a subsequence oracle run headlessly (interactive/state-gated branches
take the no-input path by design). That anti-garbage guarantee is exactly what A0 set out to prove.
**Confirmed by this run:** the classified no-op markers (`0x1f4/0x1f5/0x1d5/0x1bc/0x1bf` + tentative
`0x21b/0x1d2/0x258`, now + `0x71`) are safe as no-ops for ADV flow; `call-script` is stubbable;
effectful ops (`draw-texture`/`create-texture`/`play-voice`/`0x1f7`/`0x202`/`0x203`/…) stub cleanly.
**✅ A0 COMPLETE.** Success criteria met: RECOVER unit test green (pointer/array/control-flow model
proven); 282 ADV scenes emit clean in-order subsequences with zero garbage; coverage number recorded;
no-op-marker assumption confirmed at scale; `op 0x90` (the last big control-flow unknown) resolved as
input chrome whose fall-through stub is correct headless. Next = **A1** — port the model to the C# VM
core, differential-test against `vm0.py`. 0x90/0x97 stay stubbed (correct headless); the interactive
input path + per-scene state seeding land in **A2** (Godot backend) alongside the real hotspot model.
## A1 — Port the validated model to C# ✅ DONE (2026-07-06)
Reimplement the A0 execution model as the runtime VM core in C# (the language decision from the
roadmap; GDScript is too slow for the loop). A0 is the reference: differential-test C# against the
Python prototype's traces on the same scenes. Port the container parser too (or load via a shared
spec). Deliverable: headless C# VM reproducing A0's results.
**Result:** `engine/` .NET 8 solution (`Age.Engine` classlib w/ `Model`/`Vm`/`Sys4`/`Hosting` seams +
`Age.Cli` + xUnit tests). RECOVER passes; the C# `trace` is **byte-identical to `vm0.py --trace` across
all 297 SC/SP scenes** (offsets+halt+steps). *(Historical: this parity held while call-script was
stubbed; once call-script execution landed [2026-07-07], vm0.py was retired from oracle duty and
`TraceDiffTests` removed — see the call-script EXECUTION section.)* Version-neutral `Script` contract enforced (VM core never
references `Sys4`). Spec/plan: `docs/superpowers/{specs,plans}/2026-07-06-a1-csharp-vm*.md`.
## A2 — Godot ADV backend (one scene, with visuals)
Wire the C# VM's effectful ops to Godot: `show-text`/message window (+ furigana via `display-furigana`),
`set-font`, `wait-for-input`, choices, `play-voice`/`play-bgm`, and `create-texture`/`set-texture`/
`draw-texture`/`draw-string` for the background + sprites. Convert the scene's AGF art with the
on-disk `AGF2BMP2AGF.exe`. Resolve just-enough `call-script`/state so the scene's setup runs (or
hand-set the preconditions). Deliverable: **the chosen scene playable in Godot** — bg + dialogue +
a choice + voice — matching A0's text.
### A2a — Interactive dialogue loop ✅ DONE (2026-07-06)
Godot 4.7 (.NET, `S:/Godot/Godot_v4.7-stable_mono_win64`) project in `godot/` referencing `Age.Engine`
in-process. VM gained one hook (`IHost.WaitForInput`, opcode 0x72); suspend/resume via a worker thread +
blocking `SemaphoreSlim` in `GodotAdvHost`, UI marshalled with `CallDeferred`. Plays SC0000 page-by-page,
pauses at wait-for-input, resumes on click/Enter. **Headless self-test** (`--headless -- --selftest`)
asserts the emitted 186-line offset sequence == `build/vm0-trace.json`; A1 engine tests stay 7/7.
*(Historical: the selftest was later rewritten [2026-07-07] to run a SYNTHESIZED scene through the
plumbing and match a live headless run — full call-script handling, no vm0/frozen golden — see the
call-script EXECUTION section.)*
Toolchain: `godot --headless --path godot --import``dotnet build godot/Himegari.csproj`
`godot --headless --path godot [-- --selftest]`. Spec/plan:
`docs/superpowers/{specs,plans}/2026-07-06-a2a-godot-dialogue*.md`.
**Next = A2b:** background via `AGF2BMP2AGF.exe`, `play-voice`/`play-bgm`, choices → VM globals,
just-enough `call-script`/state (unlocks richer scenes).
### A2b-Background — FIRST-PASS RENDER LANDED (2026-07-06)
Resolution solved (`docs/asset-resolution-re.md`: `resId → files[section_base(scene)+resId]`) and wired
into a live render. **Shipped:** `Age.Engine/Sys4/ResourceMap.cs` (loads `build/asset-index.json` +
`build/asset-sections.json`; `Resolve(scene,resId) → AssetEntry`; `TexturePath` → pre-converted BMP);
`GodotAdvHost` implements `create/set/draw-texture` (slot → `TextureRect` composited behind the dialogue
in a `_stage` layer); `IHost.DrawTexture` + VM dispatch extended to pass the destination x/y (draw-texture
args 7/8); `project.godot` window = 800×600; `convert_agf.py` searches all archives + `--scene` batch.
Engine 8/8, C# `--selftest` still byte-matches the vm0 trace (VM behaviour unchanged). **Works end-to-end:**
the VM executes `set-texture(resId)` → ResourceMap resolves across archives → BMP loads → composite; the
full-screen **event-CG layer (`EV052*`) renders correctly** as the opening plays.
**Known first-pass limitations (all one subsystem = graphics geometry/blend, the next chunk):**
1. **Only the full-screen layer is correct.** Sprites/effects and `BG*` backgrounds routed through the
CG-load subroutine (`label_12649`) derive width/height/position from native ops we still **stub**
`0x208` (get-texture-size) + the sprite position/registration/animation chain — so their `dst/size`
are garbage (backgrounds land off-center, e.g. `BG030A dst=(300,300)`; sizes come out `0x0`). Only the
*immediate* full-screen draws (`(0,0) 800×600`) render right.
2. **No alpha/blend.** `AE*` full-screen fade/flash effects draw **opaque and instant** (a static grey/white
sheet over the CG) instead of alpha-animating. No chromakey either (sprites would show green boxes —
moot until they position).
3. **Slot model is an approximation.** We use one `TextureRect` per slot, replace-on-draw. (⚠ Earlier this
line claimed "the game blits onto slot 0 as an immediate-mode canvas" — **DISPROVEN 2026-07-08**: the engine
is RETAINED — `draw-texture` binds a retained object by handle (`gfx_object_bind_draw`), objects have distinct
handles + per-object source slots, composited each frame. See engine-re.md "opening render path is RETAINED".)
4. AGF is **pre-converted to BMP offline** (`convert_agf.py --scene`); a runtime C# AGF decoder is deferred.
**Next chunk — graphics-geometry/blend subsystem:** implement `0x208` (host returns the slot's real image
dims) + the sprite position/registration ops so geometry is correct; add alpha/additive blend for fades +
green chromakey; likely move to a proper canvas/blit compositor. Fixes sprites, background placement, and
fades together. (Superseded: the id-specific plan in `docs/superpowers/plans/2026-07-06-a2b-background.md`.)
### A2b-Audio — WIRED, plays end-to-end (2026-07-06)
Audio wired, OGG plays natively in Godot (no Frida, no decode/geometry work). **KEY FINDING — the two audio
ops use DIFFERENT addressing (the initial "unified manifest" assumption was WRONG for BGM):**
- **`play-voice`** → per-scene manifest `files[base+id]`, **offset 0** (same as `set-texture`).
- **`play-bgm`** → **DIRECT LITERAL NAME** `id → BGM{id:03d}.OGG` (DATA3), NOT the manifest.
**Shipped:** `IHost.PlayBgm/PlayVoice`; VM dispatch routes `play-bgm`(0xbf)/`play-voice`(0xc4) (both argc 1);
the three non-Godot hosts (`CaptureHost`, test `RecHost`/`CountHost`) no-op them so `--selftest` + engine 8/8
stay byte-identical (audio ops still `pc+1`, step count unchanged); `ResourceMap.BgmPathById(id)` (direct
name) for BGM + `ResourceMap.AudioPath(AssetEntry)` (manifest `Resolve`) for voice; `Main` loads via
`AudioStreamOggVorbis.LoadFromBuffer` into two `AudioStreamPlayer` nodes (BGM `Loop=true`; voice `Loop=false`,
interrupt-on-new). Headless run: 0 OGG-load failures, selftest byte-parity OK.
**BY-EAR VALIDATED (2026-07-06, systematic-debugging).** User confirmed voices play on their lines
(`play-voice` med→HIGH). Two reports root-caused:
- **BGM off-by-one → FIXED (real root cause, resolver changed for BGM only).** Real game plays BGM005 for
`play-bgm 0x5` and BGM008 for `0x8`; we mis-played BGM006/009 because we resolved BGM via the manifest
(`files[5]=BGM006`). BGM is actually addressed by **direct name** `BGM{id:03d}.OGG`. **Proof:** `play-bgm
0x23 → BGM035.OGG`, a real standalone track (the BGM set skips 030-034) that the manifest mis-resolved to a
graphics entry (`files[35]=EV049AA.AGF`). Voices are NOT off-by-one — the manifest interleaves graphics/
voice (`files[35]=EV049AA`, `[36]=MAN999`, `[37]=EV052CA`, `[38]=SYL0001`), so `id-1` would land voices on
`.AGF` (silent) but they play ⇒ voice offset is exactly 0. So the fix is BGM-specific; voices/textures
unchanged. Corrects the earlier "Frida-confirmed play-bgm 5→BGM006" record (a mis-attribution).
- **Lily silent = correct, form-gated (NOT a bug).** Her lines use a 3-way dispatch on form flags
`G[0xa57]`(A)/`G[0xa58]`(B)/`G[0xa59]`(C): exactly one is 1 in the real game (her current form), else the
line `jmp`s past with no voice. Our harness seeds no globals → all zero → every Lily line skipped. Proven
by seeding: `audio SC0000.BIN 0xa57=1` → 35 LILA clips fire in order (form B→LILB, C→LILC). Left unseeded
by user choice (no dummy state); Lily stays silent until real cross-scene state flow (Phase B) exists.
**Diagnostic tool added:** `Age.Cli audio <SCENE.BIN> [0xADDR=VAL ...]` — runs a scene and dumps executed
`play-bgm`/`play-voice` ops in order (BGM direct-name, voice manifest), optional global seeding. Used for all
of the above. **Watch-items:** BGM looping is whole-file for now (Eushully OGGs may carry `LOOPSTART`/
`LOOPLENGTH` Vorbis comments — refine later); `play-sound-effect`(0xb4, argc 2) left stubbed (arg roles
unconfirmed).
### A2b-Geometry — `0x208` keystone + blit compositor (2026-07-06)
Spec/plan: `docs/superpowers/{specs,plans}/2026-07-06-a2b-graphics-geometry*.md`. **Shipped & verified:**
the CG-load subroutine (`SC0000.asm` `label_12649`) computes all sprite/background geometry **in
bytecode** (`add`/`sub`/`div`/`lookup-array`); the only missing native primitive was **`0x208 =
get-texture-size(slot) → (out_w, out_h)`**. Implemented as a real VM op (`IHost.GetTextureSize`, writes
the two output globals); non-Godot hosts return `(0,0)` so trace/selftest parity holds (engine 11/11,
`--selftest` byte-identical). Replaced the TextureRect-per-slot approximation with a faithful **800×600
immediate-mode blit compositor** (`Main.BlitSlot`: `_screen.BlitRect(src rect → dst)` in execution order,
one displayed `TextureRect`; source dims read from the pre-converted BMP header on the VM thread via
`BmpHeader.ReadDims`, so the bytecode's geometry math sees real sizes synchronously). New diagnostics:
`Age.Cli gfx <SCENE>` (headless numeric oracle — dumps per-draw resolved file + computed geometry) and
`godot … -- --shot <png> [--shot-page N]` (page-gated screenshot capture). **The opening event-CG sequence
renders correctly** — full-screen CG at `(0,0)` with dialogue over it (verified by screenshot, SC0000
pages 1/3).
**Slot-0 seed (bug found & fixed via the gfx oracle + user eyeball):** slot 0 is the **primary/screen
surface** (800×600), normally created by engine-boot init the single-scene harness skips. Cold, `0x208`
measured `0×0`, and the anchor-preserve math (`base' = center (w_new/2, h_new)`) then wrote a corrupted
`(400,600)` into the **persistent base globals** — so the first CG was grey and CG2 inherited the
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 — ✅ 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
with the rest of the screen grey). Root cause = the stubbed native op **`0x215`** collapsing every draw onto
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
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.
**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.
### A2b — Scene completeness gauge (opcode coverage tracker, 2026-07-07)
To stop guessing how "done" a rendered scene is, `tools/scene_opcode_coverage.py` histograms a scene's
static opcodes and classifies each against the C# VM: **impl** (VM has a handler arm — real or a deliberate
no-op like `set-font`), **safe-noop** (no arm, but `opcodes.toml` marks it `noop_headless` — a statement /
block marker, correct to skip), or **GAP** (no arm and effectful → the VM silently `pc+1`s past it). The
implemented set is parsed from `VirtualMachine.cs`'s `case` arms (single source of truth, no drift); output is
`build/scene-opcode-coverage/<SCENE>.md`. This makes a half-rendered scene legible: *"N ops still stubbed"*,
not *"something's wrong and we thought everything ran."*
**SC0000 baseline:** 129 distinct opcodes / 16257 instrs. Instruction-weighted the VM already covers **~94.8%**
(impl 12368 + safe-noop 3053); the holes are **68 GAP opcodes / 836 instrs (5.1%)**. The GAP list clusters into
concrete backlog buckets (drives the rendering roadmap below):
- **ADV on-screen text** — `draw-string`(0x204)×205 + `0x7a` text-param×205 (1:1 paired). Dialogue text is
currently surfaced via `IHost.ShowText` → Godot `Label`; the engine's *native* glyph/window draw path is
unmodeled (cosmetic for now, but it owns text layout/speed).
- **Unmodeled gfx-range cluster** — `0x21c0x243` + `0x2bd/0x2bf` (e.g. 0x220×66, 0x22f×34, 0x228×33, 0x21e×25):
siblings of the `0x2120x21a` command-buffer family we implemented, **not yet reversed** → the biggest single
rendering unknown (likely sprite/effect/blend geometry). RE these next before more compositor work.
- **Timing** — `sleep`(0xc8)×20: animation pacing; fades/effects can't *animate* (only snap) until this exists.
- **Audio/SFX** — `play-sound-effect`(0xb4)×24 + `0xb5/0xb6/0xc2/0xd9` (channel/volume/stop control) — stubbed.
- **Scene coroutine** — `0x7b`×6 / `0x7c`×2 / `0x140`×1: the scene-coroutine framework backlog (multi-object
scene setup routes through it; see the "SECOND latent gap" note in the status memory).
- **Misc VM-support ops** — a long tail (`0x75-0x77`, `0x85/0x88/0x8b`, `0x93/0x94`, `0x197-0x1a4`, `0x1c7-0x1cf`,
`0x1fd`, `0x20a/0x20c/0x20e`, …), 12 sites each; mixed markers vs effectful — triage per-op as the VM reaches them.
Re-run per scene (`scene_opcode_coverage.py SC0240 …`) to gauge any target. The tracker also cross-checks
`opcodes.toml` metadata against VM behavior — it already surfaced `0x259` (script-entry marker) missing its
`noop_headless` flag (now reconciled).
### A2b — Sprite transform/animation subsystem, opening slice (2026-07-07)
Spec `docs/superpowers/specs/2026-07-07-gfx-animation-subsystem-design.md`; plan
`docs/superpowers/plans/2026-07-07-gfx-animation-subsystem.md`; RE in `docs/engine-re.md`
("0x21c0x243 sprite transform / ANIMATION cluster"). First slice of the `0x21c0x243` cluster the
completeness gauge flagged.
**Implemented (VM records, engine 50/50, full parity — sweep 284 exit/13 STEP-LIMIT unchanged):** the four
opening-path anim ops — `0x21e`, `0x220`, `0x234`, and `0x238` — were first retained in
the port here. **Superseded by the 2026-07-10 matrix slice below:** the original shared-target interpretation was
wrong; `0x21e` is scale, `0x220` is translation, and `0x234` is cyclic rotation.
**Historical compositor:** this slice temporarily used transform Z as alpha. The 2026-07-10 follow-up removes
that approximation and applies the native separate scale/translation channels instead.
**Tracker delta (`scene_opcode_coverage.py SC0000`):** GAP 68→**64** ops (836→**741** instrs), impl 49→**53**,
correctly-handled 60→**65/129 (50.4%)**.
**⚠ Honest scope — the opening `AE*` explosion does NOT visibly animate from this chunk.** The opening's `AE*`
effect (`AE001D → AE002B → AE003B`) is a **`sleep`-paced sequence of RETAINED-object loads/draws** — verified
2026-07-08 from native code + the raw bytecode (NOT our `gfx` oracle, which mis-reported these as "slot 0"; see
engine-re.md "opening render path is RETAINED"). `draw-texture` binds a retained object by handle
(`gfx_object_bind_draw`@`0x47e870`); the SC0000 CG loader supplies `handle = CG_array[G[0x62450]]` (the INIT2
handle array `G[0x62455..]`) and a per-object working slot `G[0x62452]`, with `sleep` (`0x64`/`0x3e8`/`0x2ee`)
between steps. Our VM executes the whole load/draw/`sleep` burst **instantly** (no timing, no per-frame present),
so we only ever see the *final* retained state; the intermediate `AE*` frames never get a frame to display. This
retained-object channel was the correct architectural seam, but the opening explosion needs **frame-pacing**
modeling the scene-coroutine / `sleep 0xc8`
timing (`0x7b`/`0x140`/`0xc8`, still GAP) so the burst is *not* collapsed. That is the clearly-scoped next chunk
for the visible opening animation, and a genuinely different subsystem than the transform/alpha channel landed
here. (**Correction:** an earlier draft of this note called it "immediate-mode slot-0 blits" — wrong; the engine
is retained, per the native `draw-texture → gfx_object_bind_draw` bind.)
### A2b — Frame-paced `sleep` (2026-07-08) — ⚠ did NOT make the opening animate (corrected)
The chunk the animation-subsystem note above flagged as "the clearly-scoped next chunk." Spec/plan
`docs/superpowers/{specs,plans}/2026-07-08-frame-paced-sleep{-design,}.md`; RE `docs/engine-re.md` ("sleep (op
0xc8)").
> **⚠ CORRECTION (2026-07-08).** The original heading here ("the opening animates ✅") and the "Verified
> visually" claim below were **WRONG** — a misread. The `sleep` op is correctly decoded + implemented and the
> **one-shot dramatic pauses now work**, but the **rapid opening CG/AE\* burst is NOT sleep-paced** and did not
> start animating. Execution trace (via the new `--trace-histogram`) shows the back-to-back
> `set-texture→draw-texture` swaps run with **no** `sleep`/`wait`/`present`/coroutine between them; what actually
> paces them is still **unknown**. The `--shot-sequence` frames I read (arcane → Lily → maid → sky) were the game
> holding on key CGs via the **sparse one-shot sleeps** (slowed further by per-frame PNG-IO), which I mistook for
> the burst stepping. How the mistake happened: I inherited "the opening is sleep-paced" from this repo's own docs
> and treated it as verified instead of tracing execution first. What IS solid: the `sleep` seam, the one-shot
> pauses, the `GfxState` race fix, and the tooling. Related: the "493k sleeps" that confused me were a **headless
> artifact** (the name-entry poll loop), since fixed — see the "Headless divergence" note below.
> **Later resolution (2026-07-10):** the matching native presentation trace below established that ordinary
> mutations run burst-fast and first become visible at `0x20c`, `0x21c`, sleep, or stable input boundaries.
**What `sleep` actually is (RE-confirmed, correct):** the Godot compositor (`Main.Recomposite` in `_Process`)
presents live `GfxState` every frame; `sleep` (`0xc8`) was a GAP so the VM ran the whole burst in microseconds.
Implementing it makes the **explicit one-shot sleeps** (1000/750/200 ms) pause correctly — but those are the
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`
→ 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
it → **headless/CLI parity held** (sweep unchanged 284 exit/13 STEP-LIMIT, emitted offsets/steps identical,
selftest OK). `GodotAdvHost.Sleep` blocks the VM background thread `duration` ms (capped 10s) — the WaitForInput
suspend pattern, time-based — so the main-thread compositor presents each intermediate frame. Behaviorally
equivalent to the native non-blocking timer given our threading model.
**Race closed:** once the VM runs concurrently for seconds, the main-thread `SnapshotVisibleObjects` truly
overlaps VM-thread `_objects`/`_registry` writes. `GetOrCreate`/`Register`/`Release` were unlocked → serialized
them on the existing (re-entrant) `_lock`. New `GfxStateConcurrencyTests` (deterministic repro of the
"Destination array is not long enough" crash) + `SleepDispatchTests`; **engine 52/52**.
**New tool** `--shot-sequence <dir> [--frames N]` (one PNG per frame, auto-advancing past input waits — a
time-based effect can't be captured by a single `--shot`). The frames it produced showed the game holding on
distinct CGs (arcane `AE*` → Lily → maid → sky) — but per the correction above, those holds are the **sparse
one-shot sleeps**, not the rapid burst stepping. (Residual, unrelated: intermediate frames show the cold-object
anchor doubling — a geometry issue independent of timing.)
**Tracker delta (`scene_opcode_coverage.py SC0000`):** GAP 64→**62** ops (741→**714** instrs), impl 53→**54**
(`sleep`), safe-noop 12→**13** (`present-frame`), correctly-handled 65→**67/129 (51.9%)**.
**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 `0x39580x3973` loop + gfx-op sequence.
The scene-coroutine framework was deferred here; the bounded host model is completed below (2026-07-09).
### Diagnostics framework extended (2026-07-08)
Motivated by the misread above (a `--trace-steps` dump was 2.5M lines → grep/awk). Added, all observe-only
(parity preserved): **`HistogramTraceSink`** (op + call-site `script:pc` execution counts + sample operand),
**`TraceSinkBase`** (per-script step attribution across call-script frames), **`TextTraceSink`** op-filter
(`--trace-ops`), **`CompositeTraceSink`**, `OpcodeTable.ByLabel`; CLI `--trace-histogram`/`--trace-ops`; Godot
`--trace-histogram <file>` (profiles the REAL run) + `--sleep-scale`. See `docs/tools-reference.md`.
### Headless divergence FIXED — faithful halt-at-wait (2026-07-08)
The histogram pinned the goose-chase root cause: op `0x72 wait-for-input` was a **no-op headless**, so a run
plowed past all 166 of a scene's prompts into the name-entry poll loop (`INPUTNAME.BIN`) and spun `sleep 1`
**493,182×** to STEP-LIMIT — a path no real playthrough reaches. Fix = **`VmOptions.HaltAtWaitForInput`**: the VM
halts (reason `wait-for-input`) at `0x72`. **`run`/`play` faithful by default** (SC0000 → ~402 steps / 0 sleeps,
matching the real path to the first prompt; `--plow` = old walk-every-page); **`sweep` plow by default** (dialogue
oracle, 284/13 unchanged) with `--halt-at-wait` → all 297 scenes halt cleanly (0 STEP-LIMIT). Godot unaffected
(really blocks on input). The corpus's 13 STEP-LIMIT scenes were all this artifact, not VM bugs. `HaltAtWaitTests`.
---
## Risks / open questions for A0
- **Pointer/lvalue semantics** — the main modeling risk; RECOVER is the litmus test.
- **Initial global state** — a scene may assume preconditions from earlier flow (`SCJUMP`/prior
scenes). Mitigation: default-zero globals + set the few a scene reads early; the subsequence oracle
tolerates a shortened path.
- **Runtime vs static dialogue order** — static `dialogue.jsonl` is file-order (all lines); runtime is
execution-order (branch taken). Hence *subsequence*, not equality; pick linear scenes to tighten it.
- **Hidden effect in a "stub"** — a stubbed effectful op that actually gates control flow could skew
output. Watch for divergence; promote a stub to a real handler if a scene needs it.
## Immediate next action
Build `tools/vm0.py` and get the **RECOVER unit test** green (pointer/array/control-flow correctness),
then run the first linear ADV scene against the dialogue oracle. That single result tells us whether
the whole VM approach executes correctly — the load-bearing question behind option 3.
---
## ✅ call-script EXECUTION (2026-07-07) — subroutines now run in the C# VM
Spec `docs/superpowers/specs/2026-07-07-callscript-vm-execution-design.md`; plan
`docs/superpowers/plans/2026-07-07-callscript-vm-execution.md`. Enabled by the native-RE finding that
`call-script <id>` is a raw SYS4INI file index (`docs/engine-re.md`).
**Shipped (engine 25/25 green):**
- **`IScriptProvider`** (Hosting) + **`Sys4ScriptProvider`** (Sys4): `id → build/callscript-names.json →
name → Paths.Scripts() → Sys4Loader.Load`, cached. Injected into the VM so `Vm` never references `Sys4`.
- **`ExecFrame` refactor** of `VirtualMachine`: per-script state (script, pc, locals, intra-call stack,
per-frame emit-guard) moved into `ExecFrame`, run by a recursive `RunFrame`. Globals/Emitted/Steps stay
VM-level (shared). Emitted lines now carry their source script name.
- **`call-script` executes:** loads the child, runs it as a nested frame sharing globals, returns to the
caller at `pc+1`. `exit`/`exit-script` and empty-stack `ret` return from the frame (top frame → HALT).
Depth-capped (`VmOptions.CallDepthCap=64`; native limit 38). Shared globals are the return channel;
per-call locals are discarded on return.
- **Product paths** (`Age.Cli run/play/sweep`, `GameSession.RunScene`) inject the provider. `trace` +
the `audio`/`gfx` diagnostics stay provider-less (base-ISA oracle / built against stub behavior).
**Design decision (refines the spec):** a VM with **no provider** falls back to the prior stub
(`host.CallScript(id); pc+1`), not a halt. This keeps every existing base-ISA test byte-identical
(they construct provider-less VMs) and needs no golden-fixture regeneration; **vm0.py retires from
oracle duty gracefully** — `trace`/`TraceDiffTests`/`WaitForInputTests` stay on the stub path as the
base-ISA guard, with zero lockstep maintenance.
**Validation:** 6 new tests (fake-provider unit tests: return-to-caller, callee `exit` returns not
halts, shared-global visibility, per-frame local isolation, unresolved-id halt, no-provider stub;
integration: ADDILL executes ADDILLSUB+CALCREVISE and reaches its own exit; BUNKI's top-level `ret`
returns cleanly). **Corpus sweep (execution on): 284/297 exit clean, 13 STEP-LIMIT, 0 depth-cap, 0
unresolved, 0 crashes.** The 13 STEP-LIMITs are input/state-gated ADV scenes (SC0000 etc.): executing
subroutines makes their global-writes drive caller loops that headless can't break (no input;
`WaitForInput` is a no-op) — the known state-divergence, not a call-script bug (loops hit STEP-LIMIT,
not the depth cap → recursion is bounded correctly).
**✅ Godot wired + testing approach corrected (2026-07-07, follow-up).** The provider is now injected
**everywhere** — no path runs with call-script disabled:
- **Godot play path** gets `Sys4ScriptProvider`, so subroutines execute live on screen (and the
headless input-wait loops break on real player input, which is why headless-only scenes STEP-LIMIT).
- **Testing principle (user-directed): synthesize test data; never disable a feature to keep a real
scene matching a frozen number.** New `Age.Engine/Sys4/ScriptAssembler` (code+strings → `Script`;
also Phase-D modding-assembler groundwork). `SyntheticSceneTests` runs an assembled scene (show-text
+ wait-for-input + real nested call-script + shared-global return) with full handling and asserts its
exact output. `WaitForInputTests` reworked onto a synthesized two-page scene. `RecoverTests` keeps its
ISA-litmus role with call-script handling ON via a no-op subroutine **double** (isolates the ISA from
the real subroutines' state deps). **`TraceDiffTests` retired** (it matched the C# VM to vm0.py's
stubbed trace; vm0 is off oracle duty and we don't gate handling to keep it matching).
- **Godot `--selftest` rewritten:** was "run SC0000 stubbed, match vm0-trace(186)"; now runs a
synthesized scene through the Godot thread/semaphore/CallDeferred plumbing and asserts it matches a
**live headless run** of the same scene — full handling, no frozen golden, no vm0 dependency. Verified:
`godot --headless -- --selftest` → "threaded host matches headless (3 lines, full handling)".
**Verified live in Godot (2026-07-07):** added `--scene <NAME>` to the frontend and a scene-end report
of the call-scripts executed as nested frames (collected thread-safely — Godot drops `GD.Print` from the
VM background thread). **SC0240 executes 29 call-scripts** (RESETLAND, SETEN, ADDEN, RENDERMAP, SETOBJ,
DRAWOBJ, CALCREVISE, LOOK) live in the real runtime; SC0000 renders the opening event CG (windowed).
**Remaining follow-ups:** optionally give the `audio`/`gfx` CLI diagnostics a provider (they still run
provider-less); **engine-level diagnostics** (the next pivot — the engine, not the frontend, should
surface script/scene execution + call-script dispatch); `decision→scene` (scene chaining) rides this
same loader once the SCJUMP decision→scene-id native hop is reversed.
## A2b — opening speed-through: the pacing lead is ENGINE CADENCE (2026-07-08)
> ⛔ **Correction.** An earlier draft of this section claimed "there is no missing pacer — all opening
> pacing primitives are already shipped." That was **retracted**: it was inferred from *headless op-counts*
> (which cannot render) and it contradicts the direct eyes-on observation that the opening **visibly speeds
> through**. Ground truth = it speeds through; the pacer is real and the cause is still open. What survives
> below is only the mechanically-verified part plus the corrected hypothesis.
**Verified (mechanical).**
1. The "rapid burst" seen in *plow* traces is a headless fiction (input-plow). The real back-to-back
`set-texture→draw-texture` swaps are the **per-page compositor** `label_1235a` (instruction indices
`0x39580x3973` = dword `0x123e80x12497`): a loop over ≤8 gfx object slots that per slot queries state
(`0x215`), erases/releases (`0x1f7`/`0x1fa`), and draws with alpha (`0x203`).
2. Op `0xcd get-input-type` is **unmodeled** in the VM (no `case`; only `0x72 wait-for-input` is handled —
two input mechanisms, one modeled). `INPUTNAME.BIN:0x1c1` name-entry is a `get-input-type→jcc→sleep 1→jmp`
poll that spins unfed. This is a **real but separate** gap (interactive blocker), **not** the speed-through.
**The corrected hypothesis (engine cadence, not bytecode).** Many opening draws are **back-to-back with no
`sleep` between them in the bytecode** (e.g. resId `0x29` twice at `0xc45`/`0xc52`; `0x34`→`0x35` at
`0x1467`/`0x1479`), yet the real game paces them. So the pace comes from the **engine's execution cadence**,
not a script primitive. Corroboration: holding **Ctrl fast-forwards ADV** in the real game (faster, not
instant) — a global engine speed governor over interpreter advancement. **Suspected cause in our port:** the
Godot VM runs on a **free-running background thread** that is not synced to the 60fps compositor, so it blasts
an entire page's draws in microseconds and the main-thread `Recomposite` only ever samples the final gfx-state
→ the intermediate CGs collapse. The native engine is a cooperatively-scheduled main loop where the interpreter
yields per frame under vsync (consistent with `sleep` already being RE'd as a *non-blocking, main-loop-polled*
timer — which only makes sense if the interpreter yields back to that loop).
**Next (open).** (1) Determine what actually landed re 60fps/vsync and the VM threading model in `godot/`.
(2) RE the native engine main loop in Ghidra: how it ticks the interpreter per frame, the frame cap/vsync, and
the Ctrl speed governor. (3) Frame-lock our VM to the render loop (a per-frame step budget or a per-frame
yield/sync point) instead of the free-running thread. Confirm any fix against **pixels** (windowed
`--shot-sequence` → real PNGs), not op-counts.
### A2b — Frame-stepped VM ✅ DONE & MERGED (2026-07-08)
**Historical implementation, superseded on 2026-07-10 by the native retained-presentation trace below.**
The validation recorded here was accurate for that slice, but the inferred native per-op cadence was not:
service waits had been folded into an operand-fetch average. Commit `85fc07d` removed the production opcode
throttle; ordinary work now runs burst-fast to proven publication/pacing boundaries.
Executed the "Next (open)" list above (option 3: a per-frame yield/sync point). Spec
`docs/superpowers/specs/2026-07-08-frame-stepped-vm-design.md`, plan
`docs/superpowers/plans/2026-07-08-frame-stepped-vm.md`; merged to `main` (`74a4221`).
**What landed (3 TDD tasks):**
1. `engine/Age.Engine/Hosting/FrameClock.cs` — pure, threadless virtual clock: `NowMs`, `Speed` (field,
1.0), `OpsPerFrame` (30), `Advance(realΔseconds)`, `EffectiveBudget` (= `OpsPerFrame*Speed`, min 1).
2. `IHost.FrameYield()` — the VM (`VirtualMachine.RunFrame`) calls it **once per executed opcode** (right
after `Step`). **No-op in all 9 headless hosts** (`CaptureHost`, the CLI trace hosts, every test host) →
headless output byte-identical. This is the parity guarantee.
3. `godot/GodotAdvHost.cs` + `godot/Main.cs` — the throttle. `Main` owns a `FrameClock`; each `_Process`
it `Advance(delta)`s the clock and `PulseFrame()`s. `GodotAdvHost.FrameYield()` counts ops and, once
`EffectiveBudget` is reached, **blocks the VM background thread** on an `AutoResetEvent` until the next
`_Process` advances the clock — throttling the interpreter to ≈`OpsPerFrame` ops per rendered frame
(≈30 → ≈1800 ops/sec, the native rate-limited cadence). `Sleep` now waits on the **same** clock (not
`Thread.Sleep`), and the anim tween reads `_lastDelta * _clock.Speed`, so the single `Speed` factor
scales throttle + sleep + tween coherently. `Speed` is the future **Ctrl fast-forward** hook, left
**unwired at 1.0** (wiring it needs ADV-mode-scope RE; the seam is ready).
**Verified.** Engine 62/62; `sweep` exit=284/STEP-LIMIT=13 (parity); Godot `SELFTEST OK`; windowed
`--boot --shot-sequence` = **16 distinct paced visual states across 120 frames** (event-CG → fade/transition
→ onward) instead of an instant jump to the final frame — the throttle demonstrably steps the opening over
real time.
**⚠ Residual (NOT this slice — the graphics geometry/blend subsystem).** By-eye the opening is still hard to
judge because the **graphics are still wrong** (AE* fades draw opaque with no alpha/blend; cold-object
anchors double; multi-surface compositing approximate). Pacing is fixed and mechanically confirmed against
pixels, but visual correctness is a **separate open chunk** the user has deferred. Do not conflate "pacing
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 (**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)
First of three graphics-fidelity slices (A blend/transparency, B geometry/anchors, C render-targets).
Spec `docs/superpowers/specs/2026-07-08-blend-transparency-design.md`, plan
`docs/superpowers/plans/2026-07-08-blend-transparency.md`, branch `feat/blend-transparency` (engine 69/69,
sweep exit=284/STEP-LIMIT=13 parity, `SELFTEST OK`).
**Landed (hybrid: engine resolves, host blits):**
- `Age.Engine/Model/BlendMath.cs` — pure colorkey match + ARGB unpack (colorkey format reversed:
op arg 3 `<0` = none, else `0xRRGGBB` exact-match, `0` = key black; baked at surface-load, see
engine-re.md §Blend).
- `RenderObject` gains `Alpha`/`Tint`/`Blend` (`BlendKind` Opaque|Alpha|Additive); `GfxObject.HasColor`;
`GfxState.SetObjectColor`; `SnapshotVisibleObjects` resolves them. `0x202`/`0x203` route through
`SetObjectColor` (the stale "alpha deferred" trace stub is gone — alpha is now consumed).
- Godot compositor: colorkey-baked image cache (keyed by `(path, colorKey)`), `BlitLayer` applies
colorkey + object alpha + RGB-tint modulate, and **surfaceless colored objects fill a tint×alpha quad**
(the fades) instead of being skipped.
**Result (pixels):** page-1 event-CG composites cleanly (dialogue + prompt, no opaque boxes); the opening's
mid-fade frames now alpha-blend (dark bg + light-ray burst, then a blended dark transition) instead of the
pre-change **full-screen opaque grey wall** that ate the CG. Verified via `--shot`/`--shot-sequence` on
`SC0000 --boot`.
**Deferred (documented, NOT built — confirmed by a stalled interp RE pass, engine-re.md §Blend):**
smooth color-animation *interpolation* (the fade ramps snap to the correct end-state rather than gliding —
the `0x202` color channel's blit consumer + clock coupling is a dedicated dig) and **additive/glow blend**
(`local_2c` mode 2/3; its object field isn't pinned). `BlendKind.Additive` is an unused seam. Next graphics
slices unchanged: B (geometry/anchors — sprite *placement*) and C (render-targets).
### A2b — SC0000 anim/transform/spritesheet cluster (partial) ✅ DONE (2026-07-08)
The `0x21c``0x243` gfx cluster, scoped to a **tractable subset** after Task-1 RE revealed it's heterogeneous
(setters + queries + matrix/scale + a movie op). Spec `docs/superpowers/specs/2026-07-08-sc0000-anim-transform-cluster-design.md`,
plan `.../plans/2026-07-08-sc0000-anim-transform-cluster.md`, branch `feat/anim-transform-cluster`
(engine 79/79, sweep exit=284/STEP-LIMIT=13 parity, `SELFTEST OK`, coverage SC0000 67→74/129 handled).
**Key RE unlock:** the dispatch table `handler(op)=ctx[0x26c93+op]` recovered statically from `FUN_00413860`
(the `opcodes.toml` `u004xxxx` labels are Kelebek drift). Full op→field map in `engine-re.md` §SC0000 anim cluster.
**Built (hybrid: engine resolves, host blits):**
- `GfxObject` gains src-rect (spritesheet) + animated-color channels; `GfxState.SetSrcRect` / `SetColorAnim`;
`BlendMath.PingPong`; `SnapshotVisibleObjects(long nowMs)` = a port of `gfx_object_anim_interpolate`
(looping row-major spritesheet cells + ping-pong color/glow), driven by the **`FrameClock`** (the "mach 5" pacing fix).
- Wired 7 ops: `0x22f`/`0x229` position (direct V24 set), `0x239`/`0x231` spritesheet (static cell / animate),
`0x232` color glow (ping-pong), `0x228`/`0x23f` queries (geometry back to script vars).
**Deferred (own follow-ups, per scope decision):** `0x21f`/`0x223` matrix/scale (need affine rendering),
`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 host supplies service-boundary suspension and retained presentation, 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.
### A2b — native scale/translation channel split ✅ IMPLEMENTED; exact visual math provisional (2026-07-10)
Replaced the legacy shared `AnimTarget` / transform-Z-as-opacity approximation with the native channel
split proven in Ghidra. Op `0x21e` now owns normalized scale (100 = identity; current `obj+0x6c`,
target `obj+0xac`, delay/duration `obj+0x3c/+0x50`). Op `0x220` owns absolute translation
(current `obj+0x16c`, target `obj+0x1ac`, delay/duration `obj+0x44/+0x58`). Consumer
`gfx_object_apply_transform_channels` (`0x472f00`) establishes a shared first-frame start
timestamp, independent delayed linear interpolation, and target commit on completion.
`GfxState` samples both channels from `FrameClock`; Godot applies scale around V18 plus independent
translation, including nearest-neighbor scaling/flips in the software blitter. Op `0x234` no longer
overwrites either matrix: the completed consumer RE corrected it to a separate cyclic rotation period+axis
channel (retained now; affine rendering deferred). No transform Z value contributes to opacity.
**Focused validation:** six matrix/rotation tests cover channel independence, VM dispatch, shared-start
delay/duration sampling, target commit, and non-opacity Z values. Engine **86/86** and Godot build pass.
Booted SC0000 with `--shot-sequence` + `--gfx-log`: 180/180 PNGs, 152 log lines, and zero
unresolved/error/NaN/Infinity outcomes. The visible circle expands around its anchor through sampled scales
`1.00 → 1.22 → 1.44 → 1.66` while remaining `op=1.00`, then the retained object is logged
`GONE`. This validates removal of the transform-Z opacity shortcut and shows the scale channel affecting
geometry in the capture. It does **not** prove the port's exact matrix calculation, anchor interpretation,
multiplication order, or 2D projection: normal playback still races past these sections too quickly for a
reliable visual judgment. Treat that math as provisional until the pacing slice enables slow normal playback
and a native-versus-port frame comparison.
### A2b — animation pacing + matrix validation ✅ (2026-07-10; pacing model superseded)
This slice initially interpreted roughly 1,788 `vm_operand_fetch` calls/s as evidence for a
refresh-independent 200 completed-opcode/s allowance. That scheduler interpretation is **superseded**:
the probe counted operand reads and mixed burst execution with time parked in native services. The matching
presentation trace below proves ordinary work is burst-fast; `--speed` now scales sleeps and retained
presentation clocks, not opcode throughput. Input remains ignored unless the VM is actually at
`wait-for-input`.
Live native capture recorded the complete `0xcbc0` scale ramp (1→5 over 1,890 ms), including the exact
composed matrices. The 215/s and 200/s port replays were useful historical diagnostics showing that object
lifetime had been coupled incorrectly to script progress, but those rates are not native scheduler constants.
Native matrix terms and the port's focused tests agree on row-vector
`anchor + (point-anchor)*scale + translation`; for base `(0,600)`, anchor `(400,1000)`,
scale 5, both project to `(-1600,-1000)`. Exact axis-aligned anchor/order/projection is validated;
cyclic-rotation rasterization remains the next affine-rendering slice.
Verification: engine **92/92** after the opcode-clock reset test, corpus sweep unchanged at
**284 exit / 13 STEP-LIMIT**, Godot build and threaded `SELFTEST OK`. The transform capture tool and
transform-aware `--gfx-log` are documented in `docs/tools-reference.md`.
### A2b — ADV transition/lifecycle diagnosis plan ✅ COMPLETED BELOW (2026-07-10)
This is the next SC0000 correctness slice. It is driven by live A/B observations, not by static opcode
coverage alone. The direct SC0000 histogram is currently **80/129 distinct ops handled (62.0%)** and
**96.2% instruction-weighted**, but a rare query, scheduler op, or render worker can still control an
entire visible section. Coverage also excludes called scripts; this slice follows only callees actually
entered on the failing path rather than expanding into blanket subscript completion.
**Observed native ADV contract (client behavior):** foreground presentation changes are transitions even
when they are only in-place fades. SC0000 begins black and fades into the first CG; the message window then
fades in before text reveals. During a CG swap, the window transitions out, one or more CG transitions run,
then the window transitions back in. A click during an active foreground transition completes it immediately
and lets the next presentation step start; a click at a stable `wait-for-input` advances the script. Ambient
retained animation is a separate class and must not all be completed by that click.
**Current port failures (2026-07-10):**
- The initial black state holds and then the first CG pops in instead of fading. This is consistent with the
port applying op `0x202`'s endpoint as a static tint while its native animated-color consumer remains
unmodeled.
- The retained textbox artwork appears to begin fading in, then disappears. The Godot `Label` shortcut stays
visible; it is deliberately out of scope for this slice because native `draw-string`/text presentation will
replace it before SC0000 is called complete. Diagnose the **box object's** lifetime, not the shortcut text.
- The late animation burst before the first music change is badly wrong, then the screen becomes white and
interactive play does not proceed to the music change.
**Concrete failing boundary.** `BGM005` begins at SC0000 offset `0x7fa`; the expected first change to
`BGM008` is `play-bgm 0x8` at **`0x1728`**. The immediately preceding block `0x133f..0x1725` exercises
repeated `0x202/0x203` color operations and the implemented geometry/scale family, but also directly executes
three still-stubbed gfx ops: **`0x236`** at `0x13c8`, **`0x1fd`** at `0x14f3`, and **`0x21f`** at `0x159a`.
It calls the shared animation finalizer `label_1235a` several times, including at `0x1725`; that finalizer
sets op `0x238`'s duration, then reads still-unimplemented **`0x1c7 get-message-skip`** and
**`0x1cc get-adv-service-state`** to choose its present/yield path. Therefore the white stall could be an
object/compositor error, an unmodeled foreground-transition gate, or wrong control flow caused by a stubbed
output—not safely assumed to be “just interpolation.”
#### Investigation order
1. **Make the failure boundary deterministic before changing semantics.** Reproduce from `--boot` with
auto-input and a long enough `--shot-sequence`, plus `--gfx-log`. Add a single synchronized diagnostic
timeline if the existing logs cannot answer the boundary: frame/virtual time; active script + PC/opcode;
VM state (`running`, sleep, transition, input wait, halt); BGM event; and every changed visible object's
handle, surface slot/resId, tint/alpha, transform, and lifecycle event. Use `play-bgm 0x8 @ 0x1728` as the
reachability sentinel. Do not judge progress from the white pixels alone.
2. **Classify before fixing.** If the VM reaches/passes `0x1728` while the frame stays white, identify the
topmost white/fill object and whether its handle remains visible, loses/rebinds its live surface, or has a
stuck color endpoint. If the VM never reaches `0x1728`, record the last PC and whether it is sleeping,
input-waiting, transition-waiting, polling, halted, or still executing. If the executed path itself is
suspect, capture the same native passage with `trace_engine_ops.py` and use `diff_optrace.py` to find the
first engine/port offset divergence.
3. **Track the textbox artwork as an AGE object.** From its first visible frame, identify its retained handle
and follow bind, color/animation, present, erase, release, and surface-rebind events through the first CG
swap. The key result is one of: `GONE` (premature erase), still present but covered (z/lifecycle input),
still present but transparent/tinted (color channel), or bound to a replaced slot (surface lifetime).
Compare only those corresponding native object events; do not spend this slice synchronizing the Godot
text overlay.
4. **Recover the foreground ADV transition contract.** Reverse/capture the producer behind
`get-adv-service-state` (`0x1cc`), implement the already-known `get-message-skip` output (`0x1c7`), and
observe what a click changes during the initial fade and the pre-`0x1728` burst. Establish an explicit
host-level foreground transition with `start → per-frame progress → natural/forced completion → resume`.
Click completes and consumes the active foreground transition; only a stable input wait advances content.
The wall clock remains the progress source and opcode pacing remains a guard within runnable bursts, not
the mechanism that decides how long a presentation state lives.
5. **Reverse only the executed missing gfx dependency that remains causal.** Triage the three direct gaps in
failing-order: `0x236` (timed/animated-surface worker), `0x1fd` (scaled vector/animation setter), and
`0x21f` (affine/matrix channel). For each, capture native inputs, retained fields, and sampled output at the
exact SC0000 site; implement it with a focused VM/state/compositor test. Do not declare a stub harmless
merely because it is rare, and do not implement the whole remaining opcode list without evidence.
6. **Validate as presentation checkpoints.** Native/manual observation remains the final visual oracle, but
each check should first have machine evidence (PC reached, object identity/lifetime, transition progress,
and final state). Required checkpoints: black visibly ramps into the first CG; the textbox artwork survives
until its intended transition-out; a CG swap orders window-out → CG transition(s) → window-in; clicking an
active transition snaps to its endpoint without also advancing a stable page; the late burst has no stuck
white owner; and execution reaches `BGM008 @ 0x1728`. Re-run engine tests, corpus sweep, Godot threaded
self-test, and the SC0000 coverage report after each landed opcode or scheduler change.
**Stop conditions / scope guard:** this slice is complete when the port reaches `0x1728` interactively and
the above foreground transitions have correct lifecycle/click behavior. Native glyph rendering, configurable
text reveal speed, and unrelated subscript opcode completeness remain separate work. Any called script proven
to own the first divergence becomes an explicit dependency of this slice; otherwise it stays out of scope.
#### Investigation 1 result — deterministic boundary classification (2026-07-10)
Added the observe-only Godot `--timeline-log <jsonl>` diagnostic so VM steps (real byte offsets), virtual
time/frame, host state, BGM events, and changed visible-object outcomes share one ordered stream. The
reproduction was `SC0000 --boot`, stable-wait auto-input via a long `--shot-sequence`, `--gfx-log`, and a
uniform diagnostic `--speed 8`; speed scales VM, sleeps, and animation clocks together and does not inject
input outside `wait-for-input`.
**Classification: not a VM/control-flow stall on the deterministic path.** The run executed all three direct
gaps (`0x236 @ 0x13c8`, `0x1fd @ 0x14f3`, `0x21f @ 0x159a`), called the finalizer at `0x1725`, then executed
`play-bgm 0x8 @ 0x1728` in `running` state at frame 818 / virtual `70,262 ms`. The BGM event resolved to
`BGM008.OGG` in the same synchronized event and execution continued through `0x172b` and beyond; the full
1,800-frame run reached page 80 and five BGM events. Therefore a native offset-path diff is not warranted for
this boundary unless a separately reproducible manual-input path fails to reach the sentinel.
The full-screen fill owner is retained handle **`0xcf08`**, but it is not stuck at this boundary. It was a
transparent white `800x600` fill (`a=0.00`) when `0x1728` executed. Later, `0x203 @ 0x1337f` made it solid
white for one sampled diagnostic frame (frame 911); the following `label_1235a` path executed `0x1c7`,
`0x1cc`, and `0x21c`, and the compositor sampled the same handle back at `a=0.00` on frame 912. This is
evidence of a likely incorrect flash/color presentation contract, not evidence for the reported pre-BGM
infinite stall. No opcode, scheduler, or compositor semantic fix was made in this investigation step.
Validation after adding the diagnostic: engine **92/92**, Godot build clean, threaded `SELFTEST OK`, and
`git diff --check` clean. The headless `--shot-sequence` PNG capture path emits dummy-renderer `GetImage`
errors, but the CPU compositor/timeline completed and the same path already had this limitation; use a
windowed sequence when pixel files rather than object-state evidence are required.
### A2b — cyclic rotation and affine rasterization ✅ DONE (2026-07-10)
This bounded slice followed the non-reproduced white-stall classification above; it did not resume that
investigation and does not claim the interactive symptom is fixed.
**Native contracts.** Op `0x21f` is a delayed one-shot axis-angle rotation, not a generic matrix row:
`(handle,delay,duration,axisX,axisY,axisZ,angleDegrees)`. It shares `obj+0x34`'s start with scale/translation,
uses delay/duration `+0x40/+0x54`, and linearly samples current axis/angle `+0x1ec/+0x204` to target
`+0x1f8/+0x208`. Op `0x234` is separately anchored cyclic rotation with integer-degree phase
`floor(((now-start)%period)*360/period)`. The native call order reduces to
`T(-anchor)*scale*oneShotRotation*translation*cyclicRotation*T(anchor)`, so the cycle rotates translation.
Op `0x223` was also closed out but deliberately not implemented here: it inserts a type-0 timed-alpha record
in the surface command map, containing a target surface slot and two object ranges. SC0000's shared site
`0x129e7` passes `(handle+2, slot, handle+1,1,handle,1,delay,duration)`. This belongs to render-target/
foreground-transition presentation, not affine object state, and remains a visible GAP rather than receiving
an uncertain approximation.
**Native matrix oracle.** The retained trace's handle `0xcb8e` sample (anchor `(700,600)`, scale from 0.9,
axis `(0,0,1)`, 30° target, sampled 11 ms into a 390 ms ramp after 500 ms delay) is
`[0.9055,0.0134;-0.0134,0.9055]`, translation `(74.1449,47.3127)`; the focused port test matches it.
The two executed SC0000 cycle sites use periods 9000/13000 ms and axes `+Z/-Z`. A windowed port capture
advanced 563 ms from their first sample to phase angles `22°/15°`, exactly the native integer formula.
**Port result.** `GfxState` now retains/samples the one-shot rotation and cyclic start/phase. `Transform2DMath`
composes a row-vector 4×4 matrix and projects it to an invertible 2D affine transform. The Godot compositor
uses a pure inverse-mapped nearest-neighbour RGBA8 rasterizer for both textures and solid fills, preserving
the existing colorkey, tint-strength, opacity, clipping, flipping, and z-order paths. Native D3D9 filtering
can still differ at subpixels; the matrix/order is oracle-backed rather than approximated.
The windowed `--shot-sequence` run wrote 454 PNGs with 102 pixel-state transitions; the first cyclic passage
produced distinct affine frames as `0xcb8e/0xcb98` advanced. There is no corresponding native PNG sequence in
the workspace, so validation is matrix/phase exact plus port-pixel coverage—not a false claim of pixel-perfect
native frame equality.
**Ghidra.** Renamed/commented `gfx_object_set_rotation_channel` (`0x47eb70`),
`gfx_queue_surface_alpha_transition` (`0x47f440`), the surface-command map helpers, and
`matrix4_make_axis_angle` (`0x48b215`); corrected comments on the one-shot consumer, cyclic interpolator,
and composite call order; named useful parameters; saved `/v2`.
**Validation:** engine **97/97**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; opcode tooling and focused Python
tests clean; transform tool compiles; SC0000 coverage **81/129 handled (62.8%)**, 48 GAP ops / 602 GAP
instructions; windowed affine capture clean. Final whitespace/diff validation is recorded with the handoff.
### A2b — ADV foreground transition/click lifecycle ✅ DONE (2026-07-10)
**Native recovery and control-flow correction.** `0x1c7` is the message-skip run-state query.
`0x1cc` reads `ctx+0x6dbd4`, now `adv_read_skip_state`; `adv_refresh_read_skip_state@0x406cd0` plus the
text/label/wait handlers maintain it from `message_ReadTextSkip` and the current-PC read-history lookup.
It is not surface progress. The earlier reading of `label_1235a`'s branch was reversed: zero OR-state
(normal playback) reaches `0x21c` and sets scheduler bit `0x400`; nonzero skip/read state reaches
`0x243 + 0x20c` to present the endpoint. Synchronized execution caught and corrected this before validation.
The executed missing producer for `0x223` range A was `0x21d`: `gfx_object_clone@0x47e4f0` copies the full
`0x2d4`-byte retained-object record from the current CG handle to `handle+1`. SC0000 then rebinds the source
handle to the new CG and queues `0x223` with old clone range A, new source range B, and target presenter
`handle+2`. Op `0x203` mode 2 is the companion transition-source alpha/identity path; its `0xffffffff`
is opaque identity, not generic solid-white tint, and negative alpha/RGB preserve current static bytes.
**Port lifecycle.** `GfxState` owns explicit queue/start/wall-clock progress/natural-or-forced completion.
Normal `0x21c` parks only the VM thread until the foreground command completes while Godot continues
per-frame compositing. A click during that wait forces only the foreground transition to progress 1, is
consumed, and resumes the VM; stable `wait-for-input` remains a separate gate. Tests verify that forcing a
transition leaves an independent cyclic retained rotation active. The `0x20c` skip/read path starts and
snaps a pending foreground transition to its endpoint without the normal wait.
**Synchronized window evidence.** At normal speed, the second-to-third CG transition produced seven
successive distinct PNG hashes while progress advanced `0.133, 0.297, 0.442, 0.592, 0.742, 0.891, 1.000`;
the user independently observed that fade working in the live window. A timed click landed at progress
`0.157`, the next frame was the endpoint, and the stable page count did not advance. The accelerated
windowed run reached `BGM008 @ 0x1728` on frame 137 with non-white changing pixels (2,469 sampled colors at
the sentinel) and continued; its earlier white interval was finite and released before that boundary.
This is windowed pixel/object/timeline evidence, not the earlier headless reachability inference.
The retained dialogue-text surface `0xe678` still has no pixels because native `draw-string 0x204` remains
a separate GAP; the Godot `Label` shortcut remains outside this slice. Its create/erase events are explicit
in the object timeline and are not being mistaken for foreground CG transition correctness.
**Ghidra.** Renamed/commented `adv_refresh_read_skip_state` (`0x406cd0`),
`op_0x1cc_get_adv_read_skip_state` (`0x427330`), `op_0x21d_clone_gfx_object` (`0x423310`), and
`gfx_object_clone` (`0x47e4f0`); added `EngineCtx.adv_read_skip_state`; saved `/v2`.
**Validation:** engine **102/102**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all six Python test scripts,
opcode/ctx lint, 481-script decode validation, RECOVER, and Windows CR-aware `git diff --check` clean.
The plain check reports only the generated reference's CRLF on its newly added row. SC0000 coverage is
**85/129 handled (65.9%)**, 44 GAP ops / 598 GAP instructions. This slice was later committed as `c9be9c5`.
### A2b — op 0x202 one-shot color/presentation ✅ DONE (2026-07-10)
**Native contract.** `gfx_op_0x202_worker_set_color_anim@0x47ea00` writes target packed ARGB at
`obj+0x64`, delay/duration at `+0x38/+0x4c`, and resets the one-shot family's shared start `+0x34`.
The missing consumer is the bit-1 path in `gfx_object_apply_transform_channels@0x472f00`: it seeds start
from frame time `ctx+0xb550`, performs an integer bytewise current-`+0x60` to target-`+0x64` LERP, then
commits target, clears timing, writes target `-1`, and clears the active bit once all sibling one-shot
channels finish. This is ordinary presentation-clock coupling, not op `0x238`'s separate service clock.
`ctx+0xb55c == 1` forces completion unless the object-local `+0x2d0` override bit is set. `/v2` comments
were corrected and saved.
**Port and endpoint diagnosis.** `GfxState` now retains current/target/delay/duration separately, shares
the start timestamp with scale/rotation/translation, and exposes synchronized `color=current->target` plus
progress in the compositor/timeline log. This fixes the initial black-to-EV049AA cut: windowed frames sample
the full-screen `0xcf08` black owner through 11 monotonically decreasing states from `1.00` to `0.00` instead
of installing the transparent endpoint immediately. The ADV textbox backing `0xd2f0` is a real 800×227 black
fill at `(0,373)`; repeated show/hide passages now produce intermediate samples (for example
`0.00, 0.26, 0.54, 0.82, 1.00` and the reverse), synchronized to the executing `0x202` sites around
`0x12283..0x12342`.
**Predecessor diagnosis, resolved by the next subsection.** The remaining full-white runs were not a stuck
color clock or d2f0 endpoint: pixel windows place them after
d2f0 is transparent, while object timelines show `AE*` handles first becoming visible in mode 0 with default
`0xffffffff` before their later mode-1 initializer. Native call-site disassembly proves op `0x203`'s
`obj+0x30` is passed directly as the D3D blend selector and mode 1 uses `SRCALPHA/INVSRCALPHA`; the port now
uses ARGB alpha as opacity and RGB as multiplicative modulation for that mode. The long white intervals did
not disappear, which localizes the residual to when intermediate retained state is presented/batched, not to
0x202 interpolation. Altering that scheduler boundary without a matching native present trace would exceed
this bounded slice. `draw-string 0x204`/`0x7a` was not pulled in.
Windowed evidence: 340 PNGs, 107 distinct pixel states; black-to-first-CG and d2f0 ramps agree with synchronized
object progress. Focused tests cover exact integer midpoint math, delay, natural completion field cleanup,
negative sentinel resolution, the 0x202-then-0x203 current/target pattern, shared color/matrix clock start,
mode-1 opacity/modulation, and the software rasterizer.
**Validation:** engine **108/108**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build clean
apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all seven Python test scripts,
opcode/ctx lint, 481-script decode validation, and RECOVER clean. SC0000 coverage remains **85/129 handled
(65.9%)**, 44 GAP ops / 598 GAP instructions. The differential oracle retains its prior branch-state
divergence after `0x12031`; it agrees through the executed `0x202`/`0x203` sequence and does not implicate
this slice. Windows CR-aware whitespace validation is clean.
### A2b — retained presentation batching / native scheduler boundary ✅ DONE (2026-07-10)
The synchronized native trace resolves the white-hold residual. At the AE001D passage, the native engine
binds `0xcb8e/0xcb98`, applies mode 1 at `0xd5a/0xd63`, and arms `0x202` at `0xd73/0xd8a` within one
roughly 5 ms opcode burst. No `gfx_render_frame` occurs between those mutations; the first composition is
the following `0x21c` service loop. The preceding explicit `0x20c`/`0x125a6` mode-0 white frame lasts only
about 10 ms. The port's 200-completed-op/s throttle had stretched that between-present burst across multiple
window frames, making retained intermediate state look like a long white stall.
Godot now leaves ordinary opcode `FrameYield` unthrottled and publishes retained state only at proven
presentation-capable boundaries: `0x20c`, `0x21c`, sleep, and input wait. `0x21c` renders while visible finite
one-shot channels or `0x223` commands remain active; hidden stale records and ambient cyclic channels cannot
hold it open. The first implementation incorrectly included hidden records and parked at the first CG; the
visible-only correction was re-run through 14 pages and the full AE burst.
Windowed before/after evidence at the same SC0000 sites: the old capture exposed AE001D in mode 0 for six
compositor frames and held one identical white PNG state for 25 frames. The corrected capture executes bind,
mode 1, and color targets in frame 64 and first publishes AE001D already in mode 1 at `0x21c`; the old mode-0
AE object state is absent from the object/pixel timeline. Remaining short white flashes are explicit native
present/color effects, not the prior between-op hold. Draw-string `0x204/0x7a`, movie `0x236`, and SFX remain
separate slices.
**Validation:** engine **109/109**; full sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build and
threaded `SELFTEST OK`; all seven Python suites, opcode/ctx lint, 481-script decode, and RECOVER clean.
Normal-speed windowed capture wrote 220 PNGs and progressed through the complete AE sequence; at the target
frame, bind + mode-1 + `0x202` setup share one VM frame and the first published object state is mode 1.
Ghidra `/v2` comments were updated and saved. This slice was committed as `85fc07d`.
### A2b — native ADV retained text (`0x7a` / `0x204`) ✅ DONE (2026-07-10)
The prerequisite input check passed before text work: the unmodified port reached page-1
`wait-for-input@0x83c` in about 2.0 seconds and a manual run released 14 consecutive waits through page 15.
Clicks were independently healthy, so no scheduler/input workaround was folded into this slice.
Native `/v2` dispatch resolves `0x7a` to `op_0x7a_handler@0x41eba0` and `0x204` to
`op_0x204_handler@0x422a60`. Op `0x7a` selects layout slot 1 and writes the last 20-byte record's cursor;
SC0000 voiced pages compute `(75,47)`, while narration resets to `(100,47)`. Layout origin is `(0,430)`,
bounds are `(720,147)`, retained glyph handles begin at `0xd6d8`, and glyph source surface is 21.
Op `0x204` locks a numbered D3D surface and GDI-rasterizes its CP932 string with retained font/color/effect
state. SC0000 `0x9b2` draws `"魔王"` to 400x30 surface 13 at `(1,1)`; `0x1fb@0x9bb` binds that surface to
object `0xe678` at `(74,444)`. Show-text builds all glyph records, then publishes one every 50 ms; a click
completes and consumes an active reveal before the VM can reach the following stable wait.
The port now handles both opcodes and string-pointer types 8/14. Blank-surface text is retained by slot and
drawn through the bound object's transform (the SC0000 speaker-name path), while ADV text retains cursor,
origin, 50 ms/glyph progress, and forced completion. The prior top-left dev Label/status presentation is
gone. Page-1 window pixels show the black 800x227 textbox with text at native `(100,477)`; voiced page 7
has separate name/dialogue bands at y=447468, 478500, and 507530. Final manual validation progressed
14 pages, with 11 completion clicks consumed during reveal and 14 later clicks releasing 14 waits.
Ghidra `/v2` was renamed/commented throughout the handler, layout, raster, surface-lock, and publication
chain and saved. The new low-frequency `capture_adv_text_trace.py` records matching native offsets/state;
an initial experimental version combining a D3D device scan with hot per-glyph/render hooks crashed in
`frida-agent.dll` at teardown, so those hooks were removed and the safe narrowed rerun left AGE alive.
**Validation:** engine **110/110**; corpus sweep unchanged at **284 exit / 13 STEP-LIMIT**; Godot build
clean apart from the pre-existing nullable warning and threaded `SELFTEST OK`; all seven Python suites,
opcode/ctx lint, 481-script decode, RECOVER, and `git diff --check` clean. SC0000 coverage rises from
**85/129 to 87/129 handled (67.4%)**, with 42 GAP ops / 188 GAP instructions. Movie `0x236` and SFX remain
separate slices. No implementation commit was made.
### Foundation track — native asset VFS + ALF/AAI/AGF readers (VFS-A/B/C DONE)
This is an optional high-leverage detour before movie `0x236` or SFX. It replaces the Phase-A
pre-extracted/pre-converted asset bootstrap with the native loose-override/archive-fallback model and removes
the runtime dependency on `extracted/` plus `build/textures/`. Canonical format/architecture detail and source
references live in `docs/asset-resolution-re.md` §“Runtime asset-VFS track”; this section defines
slice boundaries only.
Land it as three bounded slices, not one archive/codec rewrite:
VFS-A is the common prerequisite. VFS-B and VFS-C may be swapped afterward: choose A→C→B for the fastest
SC0000 textbox/chrome payoff, or A→B→C when complete base+append archive mounting is the priority.
1. **VFS-A — base SYS4 catalog + ALF byte reads.** Runtime-parse SYS4INI into raw-id, scene-local, and name
lookup views; preserve placeholders; expose bounded asset streams. Apply native precedence
`loose root/<record.name> → indexed ALF range`. Convert `Sys4Loader`/`Sys4ScriptProvider` to consume bytes
from this seam first, proving the existing 52 patched root BINs still shadow archive copies. Gate: sampled
and corpus-size/range validation against `extracted/`, plus synthetic loose-override tests.
2. **VFS-B — APPEND01 AAI mount.** Parse `S4AC422` and read `APPEND01.ALF`. Use Ghidra/native observation
only as needed to settle pack selection/mount precedence; compare the entire parsed directory and sampled
payloads with `BinExtractALF.exe`. Gate: no guessed name override or high-byte-id behavior reaches runtime.
3. **VFS-C — in-process AGF → RGBA8.** Port the MIT GARbro decode algorithm into platform-neutral engine
code with a focused LZSS primitive, palette/truecolor expansion, stride/orientation handling, and optional
ACIF alpha. Replace BMP-path texture loading with decoded pixel surfaces. Gate: codec sample matrix plus
exact SO001 dimensions/alpha, then windowed SC0000 textbox/button pixels with `extracted/` and
`build/textures/` unavailable to the runtime.
After those gates, move OGG/WAV consumers onto the same byte store as a small follow-up. That prepares SFX
without conflating its channel/timing semantics with archive access. Movie `0x236` may likewise consume raw
AGF/MPEG bytes later, but video decoding remains explicitly outside this track.
Expected authored seams (names provisional): `Sys4AssetCatalog`, `IAssetStore`/`Sys4AssetStore`, bounded
archive stream/reader, `LzssDecoder`, and `AgfDecoder`; focused tests belong in `Age.Engine.Tests`. Runtime
caches should key decoded assets by catalog identity plus loose-file timestamp, while tests use synthetic
fixtures and installed-game integration checks rather than committing proprietary assets.
**Decision point:** this track is worthwhile before broadening beyond SC0000 because it establishes the
modding contract and benefits scripts, UI chrome, SFX, and movies. It is not required to continue opcode
coverage immediately, so choosing movie/SFX next remains valid.
### VFS-A — base SYS4 catalog + ALF byte reads DONE (2026-07-11)
`Sys4AssetCatalog` now runtime-parses `SYS4INI.BIN` into all 13208 raw slots (including two `@`
placeholders), 13206 real/name records, bounded scene sections, and universal raw-id lookup.
`Sys4AssetStore` applies native `loose exact-basename -> ALF offset/size` precedence with traversal rejection,
per-open file handles, and a seek/read boundary. `Sys4ScriptProvider` and all CLI/Godot root-script paths use
store bytes; `ResourceMap` consumes the live catalog instead of generated asset JSON.
Validation: 116 engine tests; every field against `build/asset-index.json`; all 136 generated scene views;
all 13206 archive ranges; representative bytes across DATA1..5 against `extracted/`; synthetic precedence,
range, traversal, and concurrency tests; CLI SC0000 run; Godot build/selftest. The installed root has 49
archive-backed script overrides and two root-only BIN engine files, not the historically reported 52
shadowing scripts; all 49 were byte-proven to win and differ from their archive payload. VFS-B APPEND01/AAI,
VFS-C AGF, audio migration, and movie `0x236` remain separate.
### VFS-C — in-process AGF to RGBA8 + SC0000 system chrome DONE (2026-07-11)
`AgfDecoder` now consumes `IAssetStore` bytes and emits a platform-neutral, tightly packed top-down RGBA8
surface. Its shared 4 KiB-ring `LzssDecoder` covers raw/compressed information, pixel, and optional ACIF
alpha sections; expansion covers 4/8-bit palettes, 24/32-bit truecolor, DIB row padding, and bottom-up
orientation. Synthetic fixtures exercise the whole format matrix. Installed `AE000A`, `AE001A`,
`BG030A`, `EV052CA`, and `SO001` match the existing converter's pixels exactly; SO001 is 800×300
with non-binary per-pixel alpha.
`ResourceMap` now resolves texture ids through the scene manifest with a universal raw-id fallback,
then decodes through the loose-first VFS. Godot and the CLI no longer use `BmpHeader`, `TexturePath`, or
pre-converted BMPs. SYSTEM4's inherited SO001 surface is seeded into slot `0x11` before SC0000, allowing
the existing callback-window crops to render the textbox and control icons. Windowed page-1 validation
passed with `build/textures/` temporarily unavailable; source alpha produced the translucent upper edge
and the bottom-right control row was visible.
Validation: 124/124 engine tests, CLI/Godot builds, Godot threaded selftest, pixel parity for five installed
assets, and the windowed no-BMP capture. VFS-B APPEND01/AAI, OGG/WAV migration, movie `0x236`, and unrelated
opcode work remain separate.
### VFS-B — APPEND01 AAI mount DONE (2026-07-11)
`Sys4AssetCatalog.Load` now discovers installed `*.AAI` files beside `SYS4INI.BIN`, parses the `S4AC`
directory at its native offsets, and mounts each catalog by the selector stored at header offset `0x108`.
For Himegari, `APPEND01.AAI` mounts selector 1, names one `APPEND01.ALF`, and contains 81 records whose
literal names all begin `$1$`. `AssetEntry` retains the pack id, `ResolvePacked` applies the evidenced
`high byte -> mounted catalog; low 24 bits -> record` rule, and `Sys4ScriptProvider` routes append call ids
through the existing `IAssetStore`. Base direct-name lookup remains base-only: append records are not guessed
name replacements. The selected catalog still probes the record's exact loose basename before its ALF range.
Native `/v2` now names/comments the AAI constructor/load/table/open/get chain and
`asset_mount_append_catalogs@0x44f120`; `asset_open_indexed_entry@0x44f390` documents the packed-id split.
The native scan stores each successful catalog directly into its selector slot, so a later enumerated AAI
with the same selector replaces the earlier pointer. Its `SAR 24` selector extraction makes sign-bit high
bytes negative table indexes, so the port rejects them rather than inventing `0x80..0xff` behavior. Only
selector 1 is installed and exercised here.
Validation: 127/127 engine tests; stable complete-directory digest; all 81 ranges fit; a disposable
`BinExtractALF.exe APPEND01.AAI` extraction matches all 81 names, sizes, and payload bytes; real
`$1$SC1260.BIN` loads through its packed id; CLI/Godot builds and threaded `SELFTEST OK`. Audio migration,
movie `0x236`, AGF work, and unrelated opcodes were untouched.
### Phase A — native SC0000 SFX family (`0xb4`/`0xb5`/`0xb6`/`0xc2`/`0xd9`) DONE (2026-07-11)
Native RE and the matching trace resolve the bounded family. `0xb4(resource,channel)` synchronously loads
and retains a scene-manifest sound; `0xb5(channel)` starts that loaded sound once; `0xb6(channel)` destroys
and clears it. The manager supports 13 native slots, while SC0000 uses a fixed `0..9` pool. Native playback
is a notification-fed DirectSound ring: `Play(...,DSBPLAY_LOOPING)` keeps the ring alive, but logical mode 0
stops at decoder EOF. Mode 1 belongs to adjacent op `0xba` and remains outside this slice. These five ops do
not set pan or SFX volume; SC0000 inherits centered pan and configured SFX volume. The captured loads both
apply DirectSound attenuation `-2377`; because native audio-preference import is outside this slice, the
bounded extracted-WAV port uses unity gain and centered pan instead of treating that user setting as opcode
semantics.
At the requested first site, `0xb4@0xc29` resolves `0x28` to `E0808.WAV` and loads channel 0;
`0xb5@0xc2e` publishes playback in the same native millisecond. `0xb4@0xc31` preloads the same WAV into
engine-owned secondary channel 4. `G[0x6242d]` is native/profile-owned rather than script-written, so the
port exposes value 4 through the new external-global seam for SC0000. The matching port timeline now records
the same `(load ch0, start ch0, preload ch4)` sequence in one frame. Startup `0x62b..0x646` and the later
`0x120d..0x1228` both release channels 0..9 in order.
`0xc2(target,duration)` is the adjacent blocking BGM fade: 100 linear steps for durations at least 1000 ms,
10 steps below that, with target zero releasing the source. `0xd9` only clears native service bit `0x1000`;
the isolated VM recognizes it with no host-visible effect. The Godot backend retains ten `AudioStreamPlayer`
channels, loads catalog-resolved WAV bytes, separates load from start, releases buffers deterministically,
and parks BGM fades on the unified virtual clock. ALF/AAI/AGF VFS work and movie `0x236` remain separate.
Ghidra `/v2` now names/comments all five handlers plus the asset-open, decode, DirectSound start/refill/stop/
volume/release, and BGM fade workers; the program is saved. Evidence is
`build/native-sfx-trace.jsonl` and `godot/build/sfx-port-timeline4.jsonl` (the latter is the final rebuilt
channel-4 trace; earlier diagnostic reruns captured the missing external-state mismatch).
**Automated validation:** engine **111/111**; corpus sweep unchanged at **284 exit / 13 STEP-LIMIT**;
Godot build and threaded `SELFTEST OK`; all seven Python suites, opcode/ctx lint, 481-script decode, RECOVER,
and tracer bytecode compilation clean. SC0000 coverage rises from **87/129 to 92/129 handled (71.3%)**,
leaving 37 GAP ops / 101 GAP instructions. Headless sequence capture still emits the known dummy-renderer
`GetImage` diagnostics while completing successfully; it is not an audio failure.
**Manual validation:** the normal-speed windowed port advanced through `wait-for-input@0x1a58` at 45.6 s,
well past the first effects and dialogue pages, with no audio-related stall. The user confirmed the effects
were audible and sounded good.
### Audio-byte migration onto the asset VFS DONE (2026-07-11)
`ResourceMap.BgmPathById` and `ResourceMap.AudioPath` are removed. BGM direct-name resolution now returns a
catalog entry, while voice and SFX retain their scene-local resolution; all three flow through
`ResourceMap.ReadAudio` and the injected `IAssetStore`. `GodotAdvHost` passes owned byte arrays across the
existing deferred main-thread boundary, and `Main` decodes them with
`AudioStreamOggVorbis.LoadFromBuffer` / `AudioStreamWav.LoadFromBuffer`. The existing BGM loop and fade,
voice interruption, ten-channel SFX load/start/release, clock pacing, and gain behavior are unchanged.
Validation: engine **128/128** and both .NET/Godot builds pass. An archive-only SC0000 regression verifies
the Ogg signatures for `BGM005.OGG` and `MAN999.OGG` plus the RIFF/WAVE signature for `E0808.WAV`. With
`extracted/` physically moved aside, the CLI completed SC0000's 127-op audio trace and a windowed Godot run
crossed voice at `0xa0a`/`0xbf6`, BGM005 at `0x7fa`, and the established SFX load/start/preload sequence at
`0xc29`/`0xc2e`/`0xc31` on channels 0/0/4. Godot reported no OGG/WAV read, decode, or deferred-call errors;
`extracted/` was restored afterward. Movie `0x236`, AGF work, opcode semantics, and unrelated VM behavior
were untouched.
### Phase A — SC0000 movie opcode `0x236` DONE (2026-07-11)
Reversed and implemented only SC0000's existing `0x236@0x13c8`. Native ABI is
`(resource_id, surface_slot, movie_flags, sync_mask)`; the native site evaluates
`(0x33, 0, 2, 0)`. Native graph construction and asset open are synchronous, playback is asynchronous,
and the opcode itself does not park the interpreter: execution resumes at bytecode `0x13d1`. SC0000's
later `0x21c` presentation boundary sets run-state bit `0x400` and services the retained movie until EOF;
only the following cleanup stops and detaches playback.
Resource `0x33` resolves to the 8,194,052-byte `DATA1.ALF:CHAPTER.AGF` MPEG-1 program stream through the
catalog and `IAssetStore`. `ResourceMap.ReadMovie` owns that VFS read. The Godot Windows backend uses the
system DirectShow MPEG splitter/decoder, converts its bottom-up RGB32 samples to retained RGBA frames, and
presents them on the native slot-0 movie layer. The payload contains an audio stream, but its pin remains unrendered;
audio, AGF still-image work, generalized video APIs, other opcodes, and unrelated VM behavior remain out
of scope.
Ghidra `/v2` names/comments the `0x423ee0` handler and the movie ctor, graph-interface/open/play,
sound-route/volume, texture-renderer sample/media-type, stop/detach/release/destructor chain; the program is
saved. Validation covers the exact real-SC0000 operands and `0x13c8 -> 0x13d1` boundary, archive-only VFS
payload identity, actual 800x600 RGBA DirectShow decode, .NET/Godot builds, and the real Godot site opening
the same VFS byte count. Manual testing exposed and corrected an initial same-frame teardown: pre-yield
static loads no longer destroy the movie, `0x21c` waits for DirectShow completion, and a real-render run
records first frame 98 / stop frame 181. A follow-up compositor-only investigation found that the movie
object was present at the correct retained z-position, but the static AGF image cache keyed by
`(assetId,colorKey)` froze its first sample. A duplicate unconditional backbuffer copy of the current sample
was then covered by that same retained object. Dynamic movie surfaces now bypass the static-image cache and
publish only through their retained object. A windowed auto-play run recorded first frame 101 / stop frame
190, and the user manually confirmed that the video visibly played. The known lower white/textbox panel is
an unrelated retained-object issue. Movie audio remains deliberately unrendered and out of scope.
The decoder test
asserts two delivered MPEG frames differ. The two archive/decode tests
also pass with all of `extracted/` physically moved aside. The real-scene trace is intentionally separate:
the existing test bootstrap still finds root script fixtures through `Paths.Scripts()` under `extracted/`,
and changing that unrelated bootstrap was outside this movie slice. Final validation: engine **132/132**,
Godot build with zero warnings, threaded `SELFTEST OK`, and opcode-map lint clean.
### Phase A — SC0000 textbox/control-strip one-shot blend correction DONE (2026-07-11)
The lower white panel noted after movie publication and the white-outline controls had one shared cause,
not a movie or AGF-alpha fault. SYSTEM4's inherited SO001 surface is cropped by ADV backing object `0xd2f0`
and control-strip object `0xd2f1`; both use a mode-0 `0x202` one-shot color transition. The port discarded
that one-shot provenance after sampling and interpreted the packed alpha as static tint strength. As a
result, full-strength white replaced the controls' yellow pixels, while zero strength exposed the raw
mostly-white backing crop instead of hiding it.
`GfxState` now distinguishes static mode-0 `0x203` state from mode-0 color state which has passed through
`0x202`. The latter retains ARGB-opacity plus multiplicative-RGB semantics after target commit. This keeps
the established `0x00ffffff` static CG initializer opaque, preserves yellow under white identity modulation,
and makes a committed alpha-zero ADV crop transparent. Focused model/raster tests cover the committed
endpoint, identity modulation, and zero-opacity output. Validation: engine **134/134**, Godot build with
zero warnings, matching windowed capture, and user manual confirmation that the box disappears fully and
the controls retain their normal color while visible.
### Godot window jitter / compositor performance investigation (2026-07-11)
The visible window jitter is main-thread compositor pressure, not `FrameClock.Speed`, coroutine pacing,
or GPU throughput. A bounded windowed run on the Vulkan backend (RTX 4080 SUPER, `--max-fps 60
--print-fps`) sustained only **2325 FPS / 4043 ms per frame** while SC0000 approached its first ADV
input wait. The retained frontend currently makes `ShouldRecomposite()` true throughout input waits,
explicit sleeps, foreground waits, and text reveal. Each recomposite clears the 800x600 canvas, then for
every visible layer calls `Image.GetData()`, runs the C# per-pixel inverse-affine rasterizer, calls
`Image.SetData()`, and finally uploads the full canvas with `ImageTexture.Update()`. An unchanged input-wait
screen therefore consumes the same expensive path every rendered frame; dragging the OS window stutters
because this work runs on Godot's main thread.
Continuous presentation itself is partly load-bearing: one-shot transitions, movies, and ambient cyclic
channels must continue sampling `FrameClock` while active. The *unconditional* redraw implied by
`IsWaiting`/`IsTextRevealing` is not. The existing gfx-change log showed real one-shot color changes through
render frame 121, then no retained-object changes, while the separate steady-state FPS probe remained near
23 FPS. A safe correction should preserve time-based presentation but distinguish dirty/static waits from
active visual channels, and should avoid copying the full Godot `Image` out and back once per layer. Likely
implementation boundaries are: (1) explicit compositor dirty generation plus an active-animation/movie
query, and (2) one CPU backbuffer acquisition/update per recomposite or migration of ordinary layers to
Godot/GPU-native drawing. Merely changing `--speed`, sleep/coroutine behavior, or the 60 FPS cap will not
remove the underlying frame cost.
**Quick win 1 implemented (2026-07-11).** `GfxState.HasActiveVisualPresentation(nowMs)` now reports
only retained pixels that can change without another VM mutation: finite surface/one-shot channels plus
visible spritesheet, color, and cyclic-rotation channels with positive periods. Godot consumes explicit
presentation dirtiness once and otherwise recomposites only while that query is true. Entering input wait
or `sleep` requests one publish so preceding retained writes remain visible; the wait/sleep state itself and
ADV text reveal no longer rebuild the background. Movie frames retain their existing per-sample dirty
publication. This preserves the clock, VM suspension model, active transitions, ambient animation, and
movie lifecycle.
Validation: the focused static-vs-ambient query regression brings the engine suite to **135/135**; Godot
builds with zero warnings and threaded `SELFTEST OK`. A matching hidden-window Vulkan run capped at 60 FPS,
using `--speed 8` only to reach the static state quickly, held **60 FPS / 16.66 ms per frame** for all eight
reported samples, versus the pre-change 23-25 FPS. Buffer batching/source-pixel caching and rasterizer fast
paths remain independent follow-ups.
**Quick win 2 implemented (2026-07-11).** The Godot compositor now owns one reusable 800x600 RGBA8
managed backbuffer. `Recomposite()` clears it once, every image/fill/transition layer mutates that same
array in z-order through the unchanged `SoftwareAffineRasterizer`, and only the completed frame crosses the
Godot boundary via one `Image.SetData()` plus one `ImageTexture.Update()`. The former per-layer screen
`Image.GetData()` / `Image.SetData()` round trip is gone. Static source images still call `GetData()` per
draw; source-pixel caching remains the next independent optimization.
Visual validation used the same SC0000 page-1 command before and after the refactor (`--speed 8 --shot-page
1 --shot-settle 3`). The two PNG files are byte-identical, both SHA-256
`E669355772D4D9118BE80AC93595F78BB467B088659DEA4CC2D2B7D0F05B62BE`. A 12-second normal-speed windowed
Vulkan probe now reported 53, 26, and 39 FPS through the initial heavier active intervals, then 59-60 FPS
through lighter/static intervals; the old compositor had remained around 23-25 FPS. Validation also holds
at engine **135/135**, zero-warning Godot build, and threaded `SELFTEST OK`. The general per-pixel affine
loop is now the clearest remaining active-frame cost.
The byte-identical page-1 capture is a **differential compositor oracle only**, not a claim that the
automated `--shot` path has the correct interactive presentation state. Both the pre- and post-refactor
captures show fallback `SO013A` rather than the expected first event CG. The matching gfx log proves this is
not resource-load failure: `EV049AA.AGF` resolves and loads into slot 3, but at capture its handle `0x0` is
positioned at `(-400,-600)` and the full-screen `0xcf08` copy has completed its fade to alpha zero. The user
confirmed that ordinary manual play displays the correct CGs. Keep this shot-path state discrepancy open
and do not use page 1 as an absolute scene-fidelity oracle; it predates and is pixel-identical across quick
win 2.
**Quick win 3 implemented (2026-07-11).** Static compositor sources no longer round-trip through Godot
`Image.CreateFromData()` / `Image.GetData()`. The cache now retains `(width,height,RGBA)` directly under
`(assetId,colorKey)`: unkeyed assets reuse their decoded `RgbaImage.Pixels`, while keyed variants clone and
bake transparency once so the canonical decoded asset remains reusable under other keys. Dynamic movie
samples never enter this cache; they use the decoder-owned newest-frame bytes directly, cloning only if a
color key must be applied.
The differential page-1 PNG remains byte-identical at SHA-256
`E669355772D4D9118BE80AC93595F78BB467B088659DEA4CC2D2B7D0F05B62BE` (subject to the shot-path oracle
caveat above). A bounded 220-frame sequence reached VFS movie publication at render frame 141 and captured
multiple distinct movie-frame hashes, confirming dynamic samples still change rather than freezing in the
static cache. Engine **135/135**, zero-warning Godot build, and threaded `SELFTEST OK` remain clean.
The normal-speed FPS samples were effectively unchanged from quick win 2 (54/26/38, then 58-60, versus
53/26/39, then 59-60). Source copying was therefore no longer a material bottleneck after the shared
backbuffer landed. The cleanup removes needless Godot objects/copies and helps future layer-heavy scenes,
but the measured next target is the general inverse-affine per-pixel path—especially identity-transform,
opaque-copy, and axis-aligned fill fast paths.
**Quick win 4 initial raster fast path implemented (2026-07-11).** `SoftwareAffineRasterizer` now detects
exact identity matrices with integral translation and bypasses inverse-matrix construction, floating-point
coordinate application, `Floor`, and per-pixel geometric rejection. Both `BlitRgba` and `FillRgba` use
direct clipped integer indexing for that case while retaining the exact existing tint, multiplicative tint,
source alpha, object opacity, destination blend, and alpha-accumulation arithmetic. Scale, rotation,
fractional translation, singular transforms, and all other geometry retain the original affine fallback.
Zero-opacity draws also return before scanning pixels.
Six new differential cases compare the fast paths byte-for-byte against a retained copy of the pre-fast-path
algorithm across positive/negative clipping, existing destination pixels, ordinary and multiplicative tint,
partial source/object alpha, and solid fills. The full engine suite is **141/141**; Godot builds with zero
warnings and threaded `SELFTEST OK`. The differential SC0000 page image retains SHA-256
`E669355772D4D9118BE80AC93595F78BB467B088659DEA4CC2D2B7D0F05B62BE`, subject to the automated-shot
presentation-state caveat above.
Because the console SC0000 state does not match the user's ordinary interactive CG presentation, no
whole-scene FPS claim is made for this slice. An isolated Release-mode 800x600 tinted/alpha identity-blit
kernel comparison measured **2.737 ms** for the new path versus **4.217 ms** for the retained general loop,
or **1.54x** for that layer type. Manual-path fade responsiveness is validated below. A later bounded step
may add a fully opaque/unmodulated row-copy path as optional incremental work.
**Manual validation:** the user tested the ordinary interactive path—the path whose CG state and fade
cadence differ from the console capture—and reported a **massive improvement**. This confirms the identity
blit/fill fast path materially improves real fade responsiveness, not merely the isolated kernel benchmark.
### SC0000 third-CG white screen and AE001D glow fixed (2026-07-11)
The page-14 screenshot path reproduced the user's all-white EV052DA page exactly. Synchronized compositor
and opcode evidence excluded the animated glow assets as the white owner: both AE001D layers load
successfully, retain their AGF alpha, rotate, and render in mode 1 at 6-8% opacity. Base handle `0xcb2a`
was correct through the
`0x223` EV052CA→EV052DA transition (`mode=2`, identity), then `0x203@0x12478` restores it to textured mode 0
with preserved `0xffffffff`. The old port resolved that endpoint as `tintStr=1.00`, whitening every EV052DA
pixel. Textured mode 0 now uses opaque multiplicative RGB modulation, while surfaceless mode-0 fills retain
their existing strength behavior. The transition cleanup regression covers mode 2 → mode 0 `(-1,-1)`.
The resulting non-white capture then proved the glow itself still contributed no visible pixels. Native RE
closed the causal gap: `0x1fd(handle,x%,y%,z%)` immediately writes the current scale matrix at `obj+0x6c`,
distinct from `0x21e`'s one-shot target. SC0000 scales the two AE001D circles to 210% and 240%; the stub left
them at 100%, almost entirely below the viewport. `GfxState.SetCurrentScale` and VM dispatch now reproduce
the native matrices, with focused geometry/dispatch tests. Validation: engine **145/145**, opcode build/lint,
Godot build/capture, Ghidra annotations saved, and user confirmation that the CG and animated glow are both
visible. This AE001D path is a retained scaled/rotating texture; the later `0x231` spritesheet-cell issue
was separately resolved by preserving draw-texture's 200x200 cell and interpreting `(8,4)` as total
frames/columns rather than a grid divisor.
### SC0000 ADV wait indicator implemented (2026-07-11)
The missing post-reveal bat is `SO000.AGF`, a 390x27 strip of thirteen 30x27 frames. SYSTEM4 loads raw
asset `0x337c` into surface slot 12 and configures layout 1 through op `0x73` as a marker at layout-relative
`(385,140)`, screen `(385,570)`, with terminal frame 12 and a 48 ms period. Native op `0x72` activates that
layout descriptor after reveal completion. The Phase-A bootstrap now injects SO000 into surface 12 and
forwards the exact op-`0x73` configuration through `IHost`; `WaitForInput` carries the selected layout and
starts the marker clock. Godot presents it as a separate 30x27 atlas overlay, advancing inclusive frames
0..12 every 48 ms only during the stable wait and hiding on release. This avoids recompositing the 800x600
software backbuffer during otherwise static waits. Validation: engine **147/147**, zero-warning Godot build,
threaded `SELFTEST OK`, and a real page-1 capture showing the transparent bat at `(385,570)`.
### A2b -- SC0000 post-movie AE001H spritesheet correction ✅ DONE (2026-07-11)
The reported missing cave spirit was the `0x231` path, independent of the earlier AE001D retained glow.
Native interpolation proved `(period, 8, 4)` means milliseconds per frame, total frames, and columns;
the existing 200x200 draw-texture rectangle is one cell and must not be divided again. The retained model
now cycles the eight AE001H cells row-major and wraps. Focused tests cover the exact 800x400/4x2 layout.
Validation: engine **146/146**, opcode sources regenerated, Godot zero-warning build and threaded selftest,
real-scene compositor trace across all eight cells, Ghidra `/v2` annotations saved, and user confirmation
on the normal client path. The automated-shot wrong-CG state remains the pre-existing separate gotcha.
### A2b -- SC0000 post-movie AE001H travel-path correction ✅ DONE (2026-07-11)
The remaining off-screen motion is a separate `0x228` query-model bug, not part of the spritesheet crop.
Native `0x228` decomposes and returns the retained object's target translation (`obj+0x1ac..+0x1b4`), while
the C# VM currently returns its static draw/base position `V24`. For AE001H this changes the first `0x220`
target from `(40,-20)` to `(400,0)`; because the compositor also retains the `(360,20)` draw base, the
sprite was displaced to approximately x=760 before cyclic rotation. `GfxState.TryQueryTranslationTarget`
now exposes the retained target independently of `V24`; VM dispatch matches the native 0/1 status convention
and preserves output operands on a miss. Bytecode-level tests cover the exact three targets `(40,-20)`,
`(50,-80)`, `(130,-100)` plus missing-object behavior. Validation: engine **149/149**, zero-warning Godot
build, and threaded `SELFTEST OK`. Current static SC0000 coverage is **94/129 distinct ops handled
(72.9%)**: 82 implemented + 12 safe no-ops, leaving 35 GAP ops / 88 GAP instructions. By instruction
frequency, 16,169/16,257 are handled (**99.5%**); this gauge does not replace manual end-to-end fidelity.
### A2b -- SC0000 AE001H white-pulse correction ✅ DONE (2026-07-11)
Manual movement confirmation exposed a separate color artifact. The sheet itself has no white frames.
SC0000 later calls `0x232(handle,1200,224,-1)`; native treats RGB `-1` as "preserve current static RGB,"
whereas the port masks it to white and drives mode-0 tint strength from the animated alpha. The compositor
log correspondingly cycles `tintStr` from 0 to about 0.82 and back. This identifies the white wash as a port
bug, not intended artwork. Native dataflow confirms fresh static color is `0xffffffff`; `0x232` samples
`0xffffffff ↔ 0xe0ffffff` and passes it through unchanged blend mode 0, whose alpha is inert and whose
white RGB is identity modulation. The port now resolves native default/static color and negative sentinels,
samples packed ARGB, and applies it through normal mode-specific blending rather than a separate tint-strength
convention. Focused tests cover exact AE001H invariance, mode-0 RGB modulation, and mode-1 alpha opacity.
Validation: engine **152/152**, zero-warning Godot build, and threaded `SELFTEST OK`; manual confirmation is
the remaining visual gate.
### ADV page-to-script locator implemented (2026-07-11)
Godot now turns each stable `wait-for-input` into a shared human/tool coordinate. A normal run recreates
`build/page-map-<SCENE>.jsonl` and records the run-relative page number, page-start location, canonical wait
script/offset, last show-text instruction and inline-string offsets, dialogue text, and nested call stack.
The optional HUD uses the compact form `SC0000 P014 · wait SC0000@0x… · text SC0000@0x`; F2 toggles it
and F3 copies it. `tools/locate_page.py SC0000 14` resolves that record and prints authoritative disassembly
around the wait. Page number is deliberately only the friendly coordinate because state and branches can
shift ordinals; the script/offset remains authoritative. A live SC0000 run verified page 1 as
`show-text@0x834`, string `0x14963`, and `wait-for-input@0x83c`. Validation: focused Python tests,
engine **152/152**, zero-warning Godot build, threaded `SELFTEST OK`, and the live lookup all pass.
### SC0000 page-58 EV050EA background restored (2026-07-11)
The first user-supplied locator, `SC0000 P058 / wait@0x6545 / text@0x653d`, led directly to the missing
full-screen CG. Resource `0x7a` resolves to `EV050EA.AGF` and was loaded into slot 5, but retained handle
`0xcb2a` inherited an older `(-100,0)` translation, `-90 degree` rotation, and alpha-zero endpoint. Native
RE showed why cleanup diverged: `gfx_object_init_default` zeroes source slot `obj+4`, so op `0x215` returns
0 for a created-but-unbound object; only a missing handle returns -1. The port's `SourceSlot=-1` default made
the sign-tested cleanup skip that object. The model now uses native slot 0, allowing erase/recreate to restore
identity state. A lifecycle regression reproduces the transform-create, query-cleanup, and CG-reuse sequence.
Validation: engine **153/153**, zero-warning Godot build, and a synchronized real-scene compositor trace at
page 58 showing `EV050EA.AGF` at `(0,0)`, scale 1, translation 0, rotation 0, and opacity 1.
### SC0000 page-89 BG004D background restored (2026-07-11)
Locator `SC0000 P089 / wait@0x89e9 / text@0x89e1` identified `BG004D.AGF` as loaded but offscreen at
`(400,650)`. The prior BG001A animation intentionally leaves translation y=550; the erroneous extra
`(400,600)` came from `GodotAdvHost` retaining slot 0's seeded 800x600 dimensions after op `0x1fa` released
the surface. Native release frees and nulls the slot. Static releases now remove the host resource and size
state (while the existing in-flight movie retention remains unchanged), and the CLI graphics trace mirrors
that lifecycle. The loader consequently builds base `(0,-500)`, so the retained translation places BG004D
at `(0,50)`. Op `0x1ff` was also completed as the native immediate current-translation setter at obj+0x16c.
Validation: engine **155/155**, zero-warning Godot and CLI builds, and a full synchronized run reaching the
exact page-89 wait with `BG004D.AGF` drawn at `(0,50)` alongside the character sprite.
### ADV control-strip button investigation complete (2026-07-18)
Native RE and the shared SC0000 routine now establish the implementation contract for the five visible
lower-right controls: op `0x90` registers enter/leave/activation callbacks, SO001 supplies the base icons,
tooltips, generic alpha-0x80 hover overlay, and persistent state overlays, and the activation callbacks map
left-to-right to History, Auto message, all-message Skip, read-message Skip, and Hide window. Manual
correction confirms the controls are silent on hover, matching the absence of any audio call in the path.
Relevant input/state opcodes and `G[0x6c9..0x6cd]` are named in their canonical registries; the `/v2`
Ghidra image is annotated and saved. Full native evidence is in
[`engine-re.md`](engine-re.md#adv-control-strip-buttons-and-native-hotspot-callbacks-2026-07-18).
### ADV control-strip pointer callbacks + Auto bridge implemented (2026-07-18)
The first bounded runtime slice is now live. `ExecFrame` owns the native-style hotspot registry populated by
ops `0x90`, `0x94`, and `0x97`; Godot routes 800x600 virtual pointer motion into it and consumes an armed
hotspot click before the ordinary ADV page-advance path. Enter, leave, and activation targets execute as
temporary local callbacks on the VM thread while the enclosing `wait-for-input` remains parked. A dedicated
host wake channel services those callbacks without releasing the page semaphore. Inclusive bounds, direct
enter/leave dispatch, activation/re-arm, nested local calls, click consumption, and registry lifetime have
focused regressions. Callback completion requests one retained-frame publication, so static waits show the
new tooltip/glow immediately without continuously recompositing the 800x600 canvas.
This does not introduce duplicate Godot widgets or hover assets: the callbacks already present in every ADV
script redraw the retained SO001 tooltip/glow/state layers. Ops `0x1b6`/`0x1b7` now retain Auto-message state,
so the x=706 action toggles and redraws its active overlay. Auto's timed page advance is not part of this
slice; nor are keyboard/pad activation and the remaining four action services. Validation: engine 157/157,
zero-warning Godot build, threaded `SELFTEST OK`, and SC0000 static coverage 100/129 handled (77.5%;
88 implemented + 12 safe no-ops, 29 GAP ops / 70 instructions).
**Next:** manually validate hover and repeated Auto clicks in a normal SC0000 run, then add Auto timing and
the History/all-skip/read-skip/Hide service behavior one bounded action at a time. No hover-audio work is
required.
**Manual-validation correction (2026-07-18).** The first build displayed the SO001 strip but did not hover.
Its cold profile state took `jcc@0x8d` with `G[0x6c1]==0`, skipping the five visible registrations at
`0x94..0xd0`; later op `0x93@0x622` also canceled the one early registration pass before the first dialogue
wait. Native SYSTEM4 supplies `G[0x6c1]=1`, and its ADV coroutine republishes the shared definitions at the
stable wait. The single-scene bootstrap now supplies that exact inherited flag, retains canceled definitions
as inactive coroutine templates, and re-arms them at `wait-for-input`. Called-script frame exit also restores
the parent's armed registry. A real-SC0000 regression reaches the first wait, moves to History `(684,572)`,
executes `label_1a0`, observes `G[0x6c9]=1`, and verifies callback-frame publication. Validation is now engine
159/159, zero-warning Godot build, and threaded `SELFTEST OK`; manual recheck remains the visual gate.
### ADV Auto timing implemented (2026-07-18)
The follow-up manual check confirms hover and the script-owned toggles work; Auto previously changed only its
SO001 active overlay because timed release had not yet been implemented. Native RE closes that gap without a
scene-specific shortcut. Auto now uses the engine's two configuration channels: unvoiced pages wait
`AutoMessageTime1` (CONFIG default 2000 ms), while voiced pages wait for real voice completion and then apply
`AutoMessageTime0` (default 500 ms). Ops `0x1b8`/`0x1b9` read/write those delays, op `0xc4` marks a queued
voice, and op `0x1bc` resets the per-message voice state. A configured zero retains the native 100-ms
fallback.
The implementation keeps VM-owned settings separate from host-owned clock/audio state. The Godot wait loop
polls the existing monotonic frame clock and an interrupt-safe voice generation counter; it does not enqueue
a fake click or sleep the VM thread for a hard-coded duration. Focused regressions cover unvoiced and voiced
deadlines, live Auto disable/re-enable, zero fallback, the time-setting ABI, and voice-state reset. Validation:
engine 165/165, opcode and EngineCtx generators/tests/lints clean, Godot build has no compiler/analyzer
warnings, and threaded `SELFTEST OK`.
**Next:** manually validate Auto advancing both an unvoiced page and a voiced page, then take the remaining
History/all-message-skip/read-message-skip/Hide actions as separate bounded services.
### ADV all-message Skip implemented (2026-07-18)
The x=728 action now enables the native persistent Skip service through op `0x88`. The VM distinguishes
persistent button/display state (op `0x19a`) from the live skip query (op `0x1c7`, persistent state OR the
existing host fast-forward channel), and op `0x101` resets only transient input state. Godot completes text
reveal and stable waits while Skip is active; the existing skip-aware transition branches publish their final
endpoints. Voice calls follow the native latest-only queue: requests replace one deferred voice while Skip is
active, and the most recent request starts when op `0x88(0)` clears the service.
Native `set:CancelMesSkipOnClick` defaults to zero, so this slice deliberately does not turn arbitrary clicks
into a port-only cancel gesture. A future settings-profile implementation can expose the native nonzero
press/release policy without changing the Skip state seam. Regressions exercise the opcode lifecycle and the
real SC0000 x=728 callback. Validation: engine 167/167, zero-warning Godot build, opcode and EngineCtx
generators/tests/lints clean, and threaded `SELFTEST OK`.
**Next:** manually validate that x=728 fast-forwards text and transitions and that a later `0x88(0)` boundary
returns to normal pacing. Then implement Read-message Skip on the same service seam, adding the required
per-script-offset read-history model rather than treating it as another global all-skip toggle.
**Manual-validation correction (2026-07-18).** The initial Skip build collapsed each formerly blocking
message span into a free-running VM burst, so playback teleported to explicit sleeps and then teleported
again. Native still executes one opcode per interpreter tick during Skip. Godot now waits for one rendered
frame pulse at each `FrameYield` only while persistent message Skip is enabled, retaining normal free-running
bursts outside Skip. Validation: engine 168/168, zero-warning Godot build, threaded `SELFTEST OK`; repeat the
x=728 pacing check before starting Read-message Skip.
### ADV Read-message Skip persistence investigation (2026-07-18)
Native RE confirms that this service cannot be implemented as a second all-message toggle. AGE resolves the
current script code offset through its per-frame message-offset table, then tests a dword flag in an
engine-owned `ReadTextDB` keyed by the raw packed script resource id and per-script message index. Click,
Auto, and active-Skip completion queue `{script_resource_id,index,count}`; opcode `0x71`, whose layout-reset
sites are also the structural T1 targets,
commits those pending records and therefore is not a pure runtime no-op.
The database is profile-wide rather than slot-local. AGE loads and writes `RT.DAT` beside shared
`SAVE.DAT`, using `$$RT.DAT` and `RT.BAK` for replacement/backup; numbered game slots remain
`SAVE%2.2d.DAT`. Successful slot/context saves and normal shutdown persist the shared database. The full
native chain and file layout are recorded in
[`engine-re.md`](engine-re.md#adv-read-message-skip-and-shared-rtdat-history-2026-07-18), and `/v2` is
renamed/commented/saved.
**Next:** implement a profile-level read-history service behind the VM/host boundary, make click/Auto/Skip
completion queue the native tuple, make op `0x71` commit it, and use the same resolver for op `0x1cc`.
Persist it in a port-owned shared format first; raw `RT.DAT` import/export can remain a separate compatibility
slice.
### ADV remaining-button difficulty inventory (2026-07-18)
The remaining action buttons beyond Read-message Skip are History (`x=684`) and Hide Window (`x=772`).
They are not additional toggle services. `HIDEWIN.BIN` is 29/37 distinct opcodes covered (251/286
instructions) and mainly needs the real op-`0x199` coroutine transition plus AGE's per-frame mouse/joy
callback tables, button-state polling, and cursor-resource operations. Existing retained-object translation
and presentation support covers its scene-view/pan behavior, making this a medium-sized infrastructure slice.
`HISTORY.BIN` is 41/78 distinct opcodes covered (761/854 instructions) and needs a separate session-level
retained text-record model, navigation/render/metadata ops `0x1d0/0x1d1/0x1d3/0x1d4`, stored-voice replay,
the same generic input-callback layer, literal local-array initialization, and additional menu/text-surface
operations. The shared ReadTextDB is not a backlog: it stores only per-message read flags. History is
therefore the largest remaining control-strip action.
**Recommended next slice:** implement Hide Window first as the smallest clean remaining action and use it to
land the generic coroutine/input-callback service that History will later reuse. Read-message Skip remains
the next bounded persistence slice; History should follow after both foundations exist. Full native evidence
and the difficulty table live in
[`engine-re.md`](engine-re.md#remaining-adv-control-strip-actions-and-implementation-cost-2026-07-18).
### ADV Hide Window implemented (2026-07-18)
The real x=772 callback now yields through op `0x199`, runs the shipped `HIDEWIN.BIN`, re-enters the second
ADV coroutine handler, and resumes after the original yield via op `0x7c`. The VM owns frame-local timed
mouse and 32-entry input callback registrations; Godot supplies virtual coordinates, distinct left/right
button bits, directional/action indices, and release callbacks. Cursor ids `0x3318..0x331f` resolve from the
original asset catalog, decode from Windows CUR XOR/AND masks with native hotspots, and install as Godot
custom cursors.
`G[0x62425]` is now named `adv_hide_window_enabled`. It is not a temporary test seed or save-slot value: all
ordinary ADV scripts read it, none writes it, and the full VM-write capture misses it because AGE's native
scheduler owns the inherited state. The bounded Godot scene bootstrap mirrors the original enabled value 1.
Focused tests cover real SC0000 activation and HIDEWIN return, the two-handler coroutine sequence, timed
mouse callbacks, held input callbacks, cursor opcode forwarding, and decoding a real CUR asset.
Validation: engine 175/175, opcode/global generator tests and lints clean, zero-warning Godot build, and
threaded `SELFTEST OK`.
**Next:** manually validate x=772 hide/restore and edge cursors in a normal SC0000 run. The next development
slice should be Read-message Skip's profile-wide ReadTextDB model; History can then reuse both that
message-completion seam and the generic callback/input layer landed here.
**Manual-validation correction (2026-07-18).** The textbox faded through the shipped retained-graphics path,
but the port's separate ADV text Label and wait-indicator TextureRect stayed visible. Op `0x199` now suspends
those two page overlays for the lifetime of the yielded ADV coroutine, and op `0x7c` restores them with the
saved page. The failed restore interaction was a shared opcode error: `bit-set`/`bit-reset` take a bit index,
not a literal mask, so HIDEWIN's set-index-1/test-`0x2` raw right-button release never matched. Both the C# VM
and the Python oracle now use indexed-bit semantics. Follow-up original-game validation corrected an
over-narrow input conclusion: AGE separately maps physical left/right mouse buttons into logical callback
indices 4/5, and HIDEWIN registers those actions to restore as well. Godot now queues the primary action for
a left click while the ADV page is suspended and keeps that click from advancing the enclosing dialogue
wait. The real-script regression restores through the left-action callback without hitting the step limit
and records the overlay suspend/restore sequence.
Validation: engine 177/177, opcode tests/lints and vm0 RECOVER clean, zero-warning Godot build, and threaded
`SELFTEST OK`; `/v2` bit handlers are annotated and saved.
**Next:** manually recheck x=772 with the original left-click restore gesture, then continue with the
profile-wide ReadTextDB slice if the visual lifecycle now matches.
### ADV Read-message Skip deferred pending persistence architecture (2026-07-18)
The native behavior and data model established above remain the implementation contract: read state is
profile-wide, keyed by packed script resource id and per-script message index, with message completion
queued and committed through the native `0x71` boundary. Implementation is deliberately deferred, however,
until the port has a fuller picture of numbered saves, shared `SAVE.DAT` state, `RT.DAT`, configuration, and
other engine-owned profile/global data.
In particular, the earlier suggestion to persist ReadTextDB in a port-owned format first is not an adopted
storage decision. The port should not choose native `RT.DAT`, a port-owned replacement, or an import/export
split in isolation from the broader save/profile backend, lifecycle, migration, and interoperability design.
When that work resumes, the already-reversed semantic model can sit behind whichever backend that wider
architecture selects.
With Auto, all-message Skip, and Hide Window implemented and Read-message Skip consciously deferred,
History is the sole remaining unimplemented ADV control-strip action to investigate as a current slice.
### ADV History retained-record investigation (2026-07-18)
Native RE now bounds History as an in-memory engine service rather than an unknown menu subsystem. The text
manager retains 0x48-byte text/metadata/voice records plus 8-byte `{layout_slot,first_record_index}` logical
entries. Ops `0x70/0x71` establish group boundaries, `0x6e` writes styled text, `0xc4` writes voice pairs,
and op `0x1d2` writes typed metadata. That last opcode was incorrectly classified as a safe marker despite
17,323 corpus uses and is now corrected. Op `0x1bb` suppresses recursive recording while HISTORY runs; op
`0x85` clears both vectors at ordinary ADV block boundaries.
The other large-looking gaps are generic: `0x64` copies encoded inline literal arrays; `0xa1/0xa2/0xa3`
implement switch/case dispatch; `0x12e` performs rectangle-array hover tests. The existing Hide Window input
callbacks already supply HISTORY's actions. Font/color/layout/surface/presentation opcodes are individually
bounded, while `0xd3/0xd4/0xd5` remains the smooth scrollbar callback/interpolation detail.
Native context saves can serialize/deserialize the live backlog, but that path is distinct from `RT.DAT`.
The first implementation should deliberately stay in memory: add an engine/session-owned history service,
wire the native writers and `0x1d0..0x1d4` readers, then let the host render requested groups. Save/load
restoration remains a named integration seam for the later unified storage architecture rather than a new
backend decision in this slice. Full layout, lifecycle, and opcode evidence lives in
[`engine-re.md`](engine-re.md#retained-history-record-model-and-lifetime-2026-07-18).
**Recommended next implementation slice:** land the history record/index model and its ordinary ADV write
path first, with focused synthetic tests for grouping, metadata, voice pairs, suppression, and clear. Then
add generic literal-array/switch support and the HISTORY read/render path. This orders the work around the
data that must exist before clicking History can display anything.
### ADV History retained writer model implemented (2026-07-18)
The recommended first slice is complete. `AdvTextHistory` is an engine model rather than a Godot widget:
standalone VMs own it for their complete nested-script lifetime, and `GameSession` supplies one shared
instance to successive scene VMs. It stores logical `{layout_slot,first_record_index}` entries and semantic
text, typed-metadata, and voice-pair records with native flags, layout/cursor snapshots, and the active
font/color/effect style.
The ordinary native write path is live. Ops `0x70/0x71` configure/reset layouts and establish group
boundaries; `0x6e`, `0xc4`, and `0x1d2` append text, voice, and metadata records; `0x1bb` suppresses all of
those writers while HISTORY renders itself; and `0x85` clears the retained vectors at ordinary ADV block
boundaries. The already-reversed font, color, mode, effect, bold, and cursor setters update the snapshots
future history rendering will consume.
Five focused regressions cover grouping and first-record flags, metadata and voice fields, style/layout
snapshots, suppression/re-enable, clear lifetime, session sharing, and the real SC0000 first page reaching
its wait with string `0x14963` retained. Validation: engine 182/182, zero-warning Godot build, threaded
`SELFTEST OK`. Static HISTORY coverage is now 56/78 distinct opcodes and 798/854 instructions handled or
safe-noop.
**Next:** implement generic inline integer arrays (`0x64`) and value-switch dispatch (`0xa1/0xa2/0xa3`),
then wire History navigation/query ops `0x1d0`, `0x1d3`, and `0x1d4` against this model. Rendering op `0x1d1`
and its presentation operations can follow once those data-side script paths execute correctly.
### ADV History data-side navigation implemented (2026-07-18)
The second bounded slice is complete. `Script.BodyDwords` retains the original SYS4 body for runtime data
operands, and op `0x64` copies its plain count-prefixed footer values into consecutive global or local VM
cells. The native rotate/XOR sequence reverses AGE's loader-created in-memory representation; applying it
again to the file would corrupt HISTORY's visibly plain rectangle and coordinate arrays. Generic
`0xa1/0xa2/0xa3` value-switch sequences now use formatted keys and branch to their registered or default PC.
The retained model now exposes native navigation and query behavior. Layout boundaries update a latest-entry
anchor; op `0x1d0` resolves cumulative deltas from that anchor without mutating it, skipping duplicate first-
record offsets and navigation-filtered records. Ops `0x1d3/0x1d4` scan from a supplied record through one
group and return typed metadata or the voice pair with native defaults. Ghidra confirms both opcodes' third
operands reach unused helper parameters despite HISTORY passing 1.
Eight new focused cases cover signed inline values, real HISTORY footer retention, matched/default value
switches, cumulative group navigation, duplicate/filter skipping, metadata/voice queries, and missing-value
defaults. Validation: engine 190/190, opcode generator tests/lint clean, zero-warning Godot build, threaded
`SELFTEST OK`. HISTORY is now 63/78 distinct opcodes and 833/854 instructions handled or safe-noop; its 15
remaining effectful gaps total 21 instructions.
**Next:** implement History's display slice around `0x1d1`, layout origin `0x198`, message-window alpha
`0x131`, surface fill `0x20b`, and retained-object presentation `0x222`. After visible backlog rows render,
finish rectangle hover/interaction, voice replay, and the `0xd3/0xd4/0xd5` smooth-scroll scheduler details.
### ADV History visible display implemented (2026-07-18)
The third bounded slice is complete. Op `0x1d1` now selects one retained group and produces an engine-owned
render batch for the requested target text layout. `0x198` updates that layout's origin, `0x71` clears stale
host presentation, and the existing cursor/style setters provide the target baseline and retained font,
color, outline, and effect state. The live `HISTORY.BIN` call passes flags/colors as zero, so the port renders
its ordinary bound-text path without inventing behavior for unused native special modes.
Godot maintains per-layout History labels while the engine remains the canonical backlog owner. The surface-
text path was generalized from one string per surface to clipped lists, allowing all five speaker-name rows
drawn into surface `0xc1` to coexist. Op `0x20b` clears each reused 600x30 name region, op `0x131` reads a
host configuration seam with a backend-neutral default, and op `0x222` explicitly publishes the retained
rebuild. No persistence, RT.DAT, or port-owned profile format was introduced.
Four focused regressions cover target origin/cursor/style, alpha/fill/present operand shaping, layout reset,
and an end-to-end real-script path: real SC0000 page-one records are shared into unmodified `HISTORY.BIN`,
which produces a non-empty visible row and reaches `(0,60000)` presentation without a step-cap halt.
Validation: engine 194/194, zero-warning Godot build, threaded `SELFTEST OK`. HISTORY is now 68/78 distinct
opcodes and 840/854 instructions handled or safe-noop; its ten remaining effectful gaps total 14 instructions.
**Next:** implement the basic History interaction slice around rectangle hit/hover (`0x12e`) and clean exit
using the existing callback layer. Then add stored voice replay (`0x1bd`); leave `0xd3/0xd4/0xd5` smooth
scroll interpolation as a separate fidelity slice.
### ADV History basic interaction implemented (2026-07-19)
The fourth bounded slice is complete. Op `0x12e` now performs its native forward scan over addressable VM
arrays, including HISTORY's local rectangle and offset tables. It begins after the incoming index, applies
per-candidate offsets, and inclusively intersects `[left,right,top,bottom]` candidates with the supplied
reference rectangle. The shipped arrays resolve to scrollbar/control regions, bottom-right close candidate
8, and five 650x130 dialogue rows at candidates 9..13; the script's existing redraw owns hover visuals.
Raw input ownership is now frame-scoped. Registering a timed mouse callback through `0xcc` marks that script
frame as the input owner until return. Godot continues updating raw mouse bits and input callback indices,
but does not release the enclosing ADV page semaphore while that modal loop is active. This closes a real
scheduler hole without a HISTORY filename, scene offset, or synthetic button path.
Two regressions cover inclusive edges/forward scan and the complete live route. The latter reaches SC0000's
first wait, activates the real x=684 History hotspot, lets unmodified `HISTORY.BIN` render retained text,
presses/releases its real close region, and proves the script returns with recording restored while the same
single ADV wait remains parked. Validation: engine 196/196, zero-warning Godot build, threaded `SELFTEST OK`.
HISTORY is now 69/78 distinct opcodes and 841/854 instructions handled or safe-noop; its nine remaining
effectful gaps total 13 instructions.
**Next:** implement stored History voice replay (`0x1bd`) against the existing voice host path. Then take
`0xd3/0xd4/0xd5` smooth-scroll interpolation as a separate scheduler/fidelity slice.
### ADV History manual layout corrections (2026-07-19)
Manual original/port comparison found that History's fixed rail was correct, but the lookup-driven buttons
were clustered at the top-left, the hovered-row artwork disagreed with its hit region, and rendered text
lost its x origin. The common VM cause was op `0x61`: `HISTORY.BIN` uses local-int operands as the bases of
its copied coordinate arrays, while the port treated the values in those cells as global addresses. Local
pointers now retain their local/global address domain, with matching dereference and write-through behavior
in both the C# VM and Python oracle. A real-script regression fixes the seven button coordinates at
x=768/y=121..549 and confirms every visible row retains layout origin x=65.
Godot's dynamically created History/surface labels also no longer request full-rect anchors before they have
a parent. They use top-left absolute placement, matching the batch's origin+cursor coordinates and removing
the associated parent/layout diagnostic.
The next manual run showed that the sole bottom line was actually the enclosing ADV dialogue overlay, while
the genuine retained batches were clipped to one pixel. The backlog itself is healthy: a new real SC0000
six-message regression produces multiple non-empty History rows. The missing state was the exact
`SYSTEM4.BIN` prefix that defines nine shared text layouts before dispatching any scene; HISTORY only resets
and repositions slots 2..6. The single-scene Godot bootstrap now carries forward all nine definitions, with
History rows sized 650x150 at x=65 and y increments of 150, until full SYSTEM4 replay replaces this existing
class of inherited-state bridges.
Godot also suppresses the parked page's ordinary dialogue Label and wait indicator while any nested timed
raw-input frame owns the modal screen, then restores them on return. Root Control anchor presets are now
applied after parenting, removing the remaining occurrence of the associated runtime layout diagnostic.
A follow-up exit check found that the host retained the last History render batches after the script had
erased its objects and re-enabled recording. Op `0x1bb(1)` at `HISTORY.BIN@0x1100` is now the presentation
end boundary: it clears only the host's transient layout bindings, leaving the engine-owned backlog intact
for the next opening. The real interaction regression requires zero active History batches after return.
Validation: engine 198/198, zero-warning Godot build, threaded `SELFTEST OK`, and vm0 RECOVER clean.
**Next:** manually recheck the five History rows and overlay cleanup against the original. If they match, proceed
with stored History voice replay (`0x1bd`), followed by `0xd3/0xd4/0xd5` smooth-scroll fidelity.
### ADV History stored voice replay implemented (2026-07-19)
The manual row/cleanup recheck passed, and stored History voice replay now uses the existing voice resolver,
message-Skip replacement queue, and Auto voice-pending service. Native RE exposed a meaningful second value
that the earlier one-id host API erased: ordinary `0xc4` calls the indexed voice service and records
`{voice_id,0}`, while `0x1bd` uses and records `{voice_id,1}`. `IHost.PlayVoice` now carries this playback
variant explicitly; Godot preserves it in queued requests and diagnostics while continuing to play the same
resolved OGG until the native variant's audible interpretation is proven.
The generic opcode still appends `{voice_id,1}` when history capture is enabled, matching the native handler.
During the real History menu, the existing `0x1bb(0)` suppression boundary prevents that replay from becoming
a new backlog record. Focused tests cover the variant, Auto state, normal recording, and suppressed replay.
HISTORY is now 70/78 distinct opcodes and 842/854 instructions handled or safe-noop; its eight remaining
effectful gaps total 12 instructions.
Validation: 199 non-DirectShow engine cases pass, the isolated DirectShow movie case passes, opcode tests and
lint are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded `SELFTEST OK`. The full
suite's DirectShow case still times out only when run among the complete suite; this pre-existing multimedia
test interaction is unrelated to the History voice path. The `/v2` Ghidra image names/comments the shared
indexed voice service and is saved.
**Next:** investigate and implement the `0xd3/0xd4/0xd5` smooth-scroll callback/interpolation family as a
separate fidelity slice, including the remaining scheduler support it depends upon.
### ADV History live voice gate correction (2026-07-19)
Manual replay remained silent, and the synchronized live trace localized the failure before the audio host:
the clicked row reached `HISTORY.BIN@0x9c7`, found retained voice `{0x24,0}`, then failed its type-2 metadata
query at `0x9e3/0x9ee` and returned without executing `0xc4` or `0x1bd`. The retained writer had reversed
op `0x1d2`'s operands. Native handler/helper dataflow and SC0000's concrete `0x1d2(2,0x11)` site prove the
ABI is `(metadata_type,value)`; the helper stores value at record `+0x14` and type at `+0x18`.
The VM now records operand 2 as the value and operand 1 as the type. The real-script regression clears the
voice request emitted by the original page, clicks the actual visible voiced History row, and requires a new
request for SC0000 `0x24`/`MAN999.OGG`; this prevents a pre-History voice from producing a false positive.
The `/v2` handler and helper comments are corrected and saved. Manual replay of that same retained row now
plays audibly, confirming the full click-to-Godot path.
### ADV History smooth-scroll scheduler implemented (2026-07-19)
Native RE resolves `0xd3/0xd4/0xd5` as a generic frame-local timed local-callback sequence. Op `0xd3`
initializes it, op `0xd4` appends repeated relative deadlines with separate primary and catch-up targets,
and op `0xd5` starts the elapsed timer and services callbacks through the penultimate entry. The final entry
is a look-ahead sentinel. The scheduler chooses the catch-up target when the next deadline is already late,
which lets HISTORY advance interpolation state without performing every intermediate redraw.
HISTORY's concrete schedule is 10/20/30/40/50/51/52 ms: five scrollbar interpolation steps, a no-op
terminal callback, and the look-ahead sentinel. The VM now stores this state on the active script frame,
uses the host's existing clock/sleep boundary, redirects into the shipped local callbacks, and resumes the
same `0xd5` after `ret`. Focused regressions cover on-time execution and forced catch-up behavior.
The canonical opcode and `EngineCtx` sources are updated and regenerated; the `/v2` handlers, vector append
helper, timer helper, and main-loop scheduler are named/commented, the expanded 56-field `EngineCtx` is
reapplied, and the program is saved. HISTORY is now 73/78 distinct opcodes and 847/854 instructions handled
or safe-noop. Its five remaining effectful gaps total seven instructions: `0x10d` and `0x8b` twice each,
plus one each of `0x1ce`, `0x1cb`, and `0x20a`.
**Next:** investigate the five remaining HISTORY support opcodes together, then select the smallest coherent
implementation slice from their actual responsibilities rather than treating them as one feature by count.
Validation: all 203 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
Godot build has zero warnings, and threaded `SELFTEST OK`.
### ADV History mouse-wheel navigation implemented (2026-07-19)
The five-gap investigation separated independent responsibilities instead of grouping them by proximity.
Native `age_main_window_proc@0x486320` handles `WM_MOUSEWHEEL` by sign-extending its high-word delta and
accumulating it at `EngineCtx+0x1c34`; op `0x10d` returns that signed total and clears it. HISTORY first
uses the opcode to discard stale motion, then polls it from its timed mouse callback and feeds the sign into
the already implemented smooth-scroll route.
Godot now translates wheel-up/down into signed 120-unit deltas, and the VM atomically accumulates and
consumes them. This extends the existing generic raw-input callback seam and introduces no script-specific
offsets, seeded state, or persistence choice. A focused regression proves accumulation/read-clear behavior;
a real SC0000-to-HISTORY regression opens after eight messages and proves wheel-up changes the retained row
selection without releasing the enclosing ADV wait.
The canonical opcode and EngineCtx sources are regenerated, and `/v2` now names/comments the recovered main
window procedure and the specific `0x10d` handler. HISTORY is 74/78 distinct opcodes and 849/854 instructions
handled or safe-noop. Remaining: `0x8b` twice, paired sprite-animation service ops `0x1ce`/`0x20a`, and the
deferred Read-message Skip setting getter `0x1cb`.
**Next:** reverse the common text-style block around `0x8b`; it is the smallest remaining non-persistence
gap. Keep `0x1cb` deferred, and treat `0x1ce`/`0x20a` together as their own sprite-animation slice.
Validation: all 205 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
Godot build has zero warnings, and threaded `SELFTEST OK`.
### ADV History text line spacing implemented (2026-07-19)
Native RE resolves the two remaining `0x8b` calls as text-manager line leading. The handler writes its
single pixel count to manager offset `+0x560` (`EngineCtx+0x14ea0`), whose initializer default is 6.
Horizontal newline and History rendering add that value to the primary font's positive pixel height; the
script's concrete style blocks use 8 pixels with their 22/24-pixel Mincho fonts and 9 with a 16-pixel
Gothic font.
The VM now retains that current style property, Godot applies it as the Label `line_spacing` theme
constant, and History rendering matches native ownership: line spacing comes from the text manager's
current state rather than from the retained record. A focused regression distinguishes a record retained
with spacing 9 from the current History spacing 8, and the real HISTORY route requires all non-empty rows
to render with its setup value of 8.
The canonical opcode and EngineCtx sources are regenerated. The `/v2` handler and all supporting
initializer/line-advance functions are named and commented, the expanded 58-field `EngineCtx` is reapplied,
and the program is saved. HISTORY is now 75/78 distinct opcodes and 851/854 instructions handled or
safe-noop. The remaining instructions are paired sprite-animation service ops `0x1ce`/`0x20a` and the
deferred Read-message Skip setting getter `0x1cb`.
**Next:** investigate and implement `0x1ce`/`0x20a` together as the final non-persistence History support
slice. Keep `0x1cb` deferred until the global save/profile storage boundary is chosen.
Validation: all 205 engine tests pass, including four focused History presentation cases; opcode and
EngineCtx tests/lints are clean, vm0 RECOVER passes, the Godot build has zero warnings, and threaded
`SELFTEST OK`.
### ADV History text publication / wait-indicator service implemented (2026-07-20)
Native RE corrects the provisional "sprite-animation service" description: `0x1ce` and `0x20a` are the
explicit control points for ADV text publication and its animated input-wait indicator. `0x1ce(enabled)`
starts or stops the indicator's run-state/timer/frame service; Himegari only calls it with zero when entering
modal scripts, including HISTORY. `0x20a(layout_slot)` republishes the selected retained text layout and,
when the service is active, binds its current indicator frame; HISTORY's shared redraw callback uses slot 1.
Normal ADV wait opcode `0x72` implicitly arms the same service.
The VM now forwards both operations through generic host methods. Godot gates its existing animated marker
on the explicit enabled state and requests recomposition for layout publication. Because the port keeps the
parent wait blocked while a nested raw-input callback executes, it restores a previously enabled parent
marker when that callback returns, matching the native path that re-enters the shared redraw/wait routine.
No script offsets, boot seeds, or persistence assumptions were introduced.
A focused synthetic regression verifies the two service calls, and the real SC0000-to-HISTORY regression
requires marker disable plus layout-slot-1 publication while preserving the same enclosing ADV wait and
cleaning the modal rows. The canonical opcode and EngineCtx sources are regenerated; the `/v2` handlers and
workers are named/commented, the expanded 63-field `EngineCtx` is applied, and the image is saved. HISTORY
is now 77/78 distinct opcodes and 853/854 instructions handled or safe-noop. The only remaining instruction
is the deliberately deferred `0x1cb` Read-message Skip setting getter, pending a global profile/save backend
decision.
Validation: all 206 engine tests pass, opcode and EngineCtx tests/lints are clean, vm0 RECOVER passes, the
Godot build has zero warnings, and threaded `SELFTEST OK`.
### SC0000 deferred SFX start and voice/BGM duck control implemented (2026-07-20)
Native RE and the saved SFX trace resolve two high-frequency audio gaps. Opcode `0x2bf` is the sound
facade's `SetDelay(channel,start_mode,delay_ms)`: SC0000's first `(4,0,100)` call starts the preloaded
secondary E0808 channel about 109 ms later through the same worker used by `0xb5`. The port now schedules
that start against the presentation clock and attaches it to a per-channel generation, so a later load or
release cancels an obsolete callback instead of starting replacement audio.
Opcode `0x1cf` replaces a transient voice/BGM-duck control mask; bit 0 suppresses automatic ducking.
The native engine registers ducking enabled with a 50-percent target, and its voice-start helper saves the
current BGM level before applying that target. Godot now follows that contract and restores the saved level
when voice playback completes or message Skip stops it. This is runtime audio state only: no profile/save
field, boot seed, or storage-backend decision was introduced.
The canonical opcode and EngineCtx sources are regenerated, the expanded 65-field `EngineCtx` is applied,
and the `/v2` handlers/workers are named, commented, and saved. SC0000 is now 119/129 distinct opcodes and
16,238/16,257 instructions handled or safe-noop (92.2% distinct; 99.9% instruction-weighted); its ten
remaining effectful gaps total 19 instructions.
**Next:** investigate the six-call `0x1ad` cluster first. It is now the largest remaining SC0000 gap and
should be classified before choosing between it and the smaller two-call support pairs.
Validation: all 206 engine tests pass; opcode and EngineCtx lints, vm0 RECOVER, the zero-warning Godot
build, and threaded `SELFTEST OK` are clean.