From 501298de814ad87842f1a1437b025e5e25dbc1c1 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Tue, 25 Aug 2026 22:25:10 +0000 Subject: [PATCH] =?UTF-8?q?re:=20the=20trigger=20container's=20"push"=20is?= =?UTF-8?q?=20a=20clear=20=E2=80=94=20and=20the=20live=20test=20is=20block?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third independent line for yesterday's built-in 100 rename, from the callee this time. sub_8226E3B8 was labelled "push", which is what made built-in 100 look like push_trigger. It reads the element count, returns immediately when the container is EMPTY, and otherwise walks the node list splicing nodes out until it is empty. A push links one node; this unlinks all of them. It is clear(). The append is sub_8226E160, reached from built-ins 19 and 25. So the rename now rests on the handler, the usage (all 12 Stage 02 sites sit in the phase terminator next to timer_stop / clear_flag(-1) / MARK_LAST_PHASE), and the callee. The dynamic half did NOT run, and the write-up says so. phase_watch.py now samples [phase+272+20] (triggers queued) and [phase+216+8] (coroutines alive) so a phase terminator's effect on the VM is visible in one line — written here, never yet exercised against a live guest. Boot-nav could not reach the title in 381 s. Diagnosed rather than retried: skip_intro.sh only runs the title test on a static frame, gated at rmse <= 1500, and this run measured 1503 at 104 s and 1549 at 139 s — just above the cut — so is_title.py was never called and the one allowed press was never spent. Recorded in BACKLOG with the explicit instruction NOT to raise the constant: the first step is to log rmse and the glyph count through a whole boot and look at the two distributions, because tuning a threshold to make one run pass is fitting to a single sample. Also reaped a stale lock: a gdb orphaned 2h14m earlier was holding /tmp/xenia-canary.lock with an already-defunct emulator child. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 19 ++++++++++++++++++ docs/re/structures/isl-builtins.md | 31 ++++++++++++++++++++++++++---- tools/re-capture/phase_watch.py | 16 +++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 9fa5ff1..454ddec 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1033,6 +1033,25 @@ premise was wrong.** Candidates: the **7 `.embsec_` sections** (VAs 0x84D0000–0x86AC000, ~129 KB total, executable) or a hashed record in `hidden/MiscBin.pak`. **Finding it gives the actual per-phase clear condition for every stage.** +* 🚧 **(2026-08-25) BLOCKED — the live test of the `reset_phase_threads` rename + never got to run, because boot-nav cannot reach the title.** The experiment is + ready and worth doing: built-in 100 clears the trigger container and frees + every thread but the caller, so at a phase terminator **both** + `[phase+272+20]` (triggers queued) and `[phase+216+8]` (coroutines alive) + should collapse; if they climb straight through a phase boundary instead, the + reading is wrong. `tools/re-capture/phase_watch.py` now prints both counters — + **written this session but never yet exercised against a live guest.** + **The blocker, measured:** `skip_intro.sh` only tests for the title when the + frame is static, gated at `rmse <= 1500`. A 381 s run produced 18 "waiting it + out" samples, and two of them — **1503 at 104 s and 1549 at 139 s** — sat just + above the cut, so `is_title.py` was never even called and the single allowed + press was never spent. The attract loop then restarts and the wait never ends. + ▶️ **First step, and it is NOT "raise the constant":** log `rmse` *and* the + `is_title.py` glyph count every second through a boot, so the true-title and + attract-demo distributions can be seen; only then decide the gate. Tuning 1500 + to make one run pass would be fitting to a single sample. Note the surrounding + comments are emphatic that tapping through the movies is what breaks the + title, so the press budget really is one. * 🟡 **(2026-08-25) The legacy IDXD string-pool reader is wrong far more often than assumed** — now measurable for the first time, since the record table gives a ground truth. Verified by hand: `FCSRange` (the module doc's own canonical diff --git a/docs/re/structures/isl-builtins.md b/docs/re/structures/isl-builtins.md index f866df2..6de7f95 100644 --- a/docs/re/structures/isl-builtins.md +++ b/docs/re/structures/isl-builtins.md @@ -317,12 +317,35 @@ continuously — see [script-runtime-probe](../script-runtime-probe.md)). do with `[phase+272]`. * `[phase+272]` is **not a pointer to a queue** — it is an **embedded container**. `vt2` (`sub_82265DD0`) is literally `addi r3,r3,272 ; b 0x8226E3B8`, i.e. it - passes `phase+272` as `this` into the push. + passes `phase+272` as `this`. -### Container layout, from the push/pop pair +### ❌ `sub_8226E3B8` is a CLEAR, not a push -`sub_8226E3B8` (push, reached from built-in **100**) and `sub_8226E220` (pop, -called every frame from `sub_8226D740`): +It was labelled "push" here, which is what made built-in 100 look like +`push_trigger`. Read directly, it is the opposite: + +``` +lwz r11,20(r30) ; the element count +cntlzw / extrwi ; == 0 ? +bne -> 0x8226E450 ; count == 0 -> nothing to do, return +addi r31,r30,12 ; else walk the node list… + stw r11,0(r10) ; stw r10,4(r11) ; …unlinking each node +``` + +A push allocates and links **one** node; this runs only when the container is +**non-empty** and splices nodes **out** until it is empty. So it is +`clear()`. That is a third independent line for the rename above — the handler, +the usage (all 12 Stage 02 sites sit in the phase terminator), and now the +callee. + +The **append** is `sub_8226E160`, reached from built-ins 19 and 25: it takes the +record fields as arguments (including a `double` in `f1`, matching built-in 19's +`+24 dbl`) and is guarded on `[container+8]`. + +### Container layout, from the clear/pop pair + +`sub_8226E3B8` (clear) and `sub_8226E220` (pop, called every frame from +`sub_8226D740`): | offset in the container | meaning | |---|---| diff --git a/tools/re-capture/phase_watch.py b/tools/re-capture/phase_watch.py index d0c4bd8..c09b0c3 100644 --- a/tools/re-capture/phase_watch.py +++ b/tools/re-capture/phase_watch.py @@ -83,11 +83,23 @@ def main(): print(' [%6.1fs] *** GUEST FROZEN -- readings below are about ' 'a dead world ***' % now, flush=True) r = S.read_states(f, m, sym2, WATCH) - key = (r['phase_ordinal'], r['finished'], r['active_records'], + # Two script-VM counters, so a phase boundary and what it does to the + # VM are visible in the same sample: + # [phase+272+20] triggers queued (the container's element count) + # [phase+216+8] coroutines alive (the active-thread list's count) + # Built-in 100 is `reset_phase_threads`: it clears the container and + # frees every thread but the caller, so BOTH should collapse at a + # phase terminator. If they climb straight through one instead, that + # reading is wrong -- which is the point of printing them. + ph = S.u32(f, m + 4) + pending = S.u32(f, ph + 272 + 20) if ph else None + threads = S.u32(f, ph + 216 + 8) if ph else None + key = (r['phase_ordinal'], r['finished'], r['active_records'], pending, threads, tuple((n, (v or {}).get('state')) for n, v in r['units'].items())) if key != last: - print(' [%6.1fs] phase=%s finished=%s active=%3d %s' % ( + print(' [%6.1fs] phase=%s finished=%s active=%3d pending=%s threads=%s %s' % ( now, r['phase_ordinal'], r['finished'], r['active_records'], + pending, threads, ' '.join('%s:%s' % (n, (v or {}).get('state')) for n, v in r['units'].items())), flush=True) last = key