re: the trigger container's "push" is a clear — and the live test is blocked

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-25 22:25:10 +00:00
parent 9a9921b759
commit 501298de81
3 changed files with 60 additions and 6 deletions

View File

@@ -1033,6 +1033,25 @@ premise was wrong.**
Candidates: the **7 `.embsec_` sections** (VAs 0x84D00000x86AC000, ~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

View File

@@ -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 |
|---|---|

View File

@@ -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