re: the transition is overlap, not ramp-then-hold -- and the fade-in was 5x wrong

screen-transitions.md carried a 14-unit "black hold" that the page itself
flagged as arithmetic rather than measurement. Measured it against the running
game; the guess was wrong, and finding the instrument to measure it turned up a
second, larger error in the same page.

1. fade_quads.py was STALE. It read each pose's time from blk+36 -- the next
record's time word -- the association the keyframe record-layout fix retired in
the crate. sylpheed-cli was rebuilt at the time; the Python helper was never
swept with it. Signature: it cannot time a group's last pose, so it printed a
trailing `t=-`. Fixed, controlled against the rebuilt `screen info` ([0 12 70
80] for build 5's pteff00.prm).

2. Through it, the page labelled the quad's CLEAR-hold as its fade-in and
published 0.87 s / 0.97 s / 4.08 s for a ramp that is 0.20 s / 0.20 s / 0.27 s.
A port pacing its menu fade-in off that would run it 5x too slow.

3. The measurement. fade_decompose.sh boots to the main menu, arms the UI draw
capture there, then presses (B), so one 260-frame window holds the whole screen
change. The fade quad is identified rather than guessed: a .prm carries no
tex[base=] and paints last, so it is the last full-screen untextured quad of a
frame. Control first -- the quad's ramp is decoded at 10 units = 5 frames, and
measures 4 submitted-frame steps with one unlogged frame in the span.

Result: content elements begin fading at frame 34; the black quad first appears
at 40 and is opaque by 43; the menu's last frame is 45; frame 46 has 6 draws
against 12. So the ~14 extra units are the content's own fade-outs OVERLAPPING
the quad's ramp, not a hold after it, and the inter-screen black is one frame.

Refutation attempted: sylpheed-port's entries 13/14 twins. Re-derived off the
disc -- 3.06 / 4.33 / 47.91, identical to two decimals. Recorded as confirming
their addressing and arithmetic, NOT as independent support: same renderer,
same disc, which is their own rule.

Reach: one transition, one run; the frame axis has gaps (232 headers over frames
3..260), so every span is +-1 frame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
This commit is contained in:
sylph-decoder
2026-08-30 13:35:56 +00:00
parent c364cde476
commit 4bcb35cef7
7 changed files with 370 additions and 13 deletions

View File

@@ -23,6 +23,80 @@ There is no fourth kind. If a row says *measured* or *undecodable*, the port is
human can see it is a human decision, so that when it is later decoded the
authored version can be deleted.
## 🔴 2026-08-30 — the transition is OVERLAP, not ramp-then-hold. And your menu fade-in is 5× too slow.
Two corrections and one measurement, all on
[`screen-transitions.md`](../re/screen-transitions.md). **The fade-in one changes a
number you are probably already using.**
### 🔴 1. The fade-in is 0.20 s, not 0.97 s
That page told you the screen "holds black for 0.20 s, then fades in over 0.87 s
(`EXTRAS`), 0.97 s (main menu) or 4.08 s (the title)". **Those are not fade-ins.**
They are the stretch where the fade quad sits at `α = 0` — the screen fully visible
and not fading at all. Read correctly:
```
build 4 (title) pteff00.prm t= 0 α=255 t= 16 α=0 t=261 α=0 t=269 α=255
build 5 (main menu) pteff00.prm t= 0 α=255 t= 12 α=0 t= 70 α=0 t= 80 α=255
build 6 (EXTRAS) pteff00.prm t= 0 α=255 t= 12 α=0 t= 64 α=0 t= 74 α=255
```
**Fade-in = 12 units (0.20 s) on the menu and `EXTRAS`, 16 units (0.27 s) on the
title.** Fade-out = 10 units, 10 units, and **8** on the title.
**Cause:** `tools/re-capture/fade_quads.py` read each pose's time from `blk+36`
the *next* record's time word — the same association the record-layout fix retired
in the crate. `sylpheed-cli` was rebuilt then; the Python helper was not swept with
it. Its signature is the trailing untimed keyframe (`t=—`) that page printed for
years. Fixed and controlled against the rebuilt `screen info`, which gives
`[0 12 70 80]` for the same element.
### ✅ 2. The ~14 "missing" units are not a black hold — measured
That page guessed the remainder of the ~0.4 s was the black hold and **said in as
many words that this was arithmetic, not a measurement**. I measured it. It is
wrong.
`fade_decompose.sh` boots to the main menu, arms the UI draw capture there, then
presses Ⓑ — one 260-frame window holding the whole screen change. The fade quad is
*identified*, not guessed: a `.prm` carries no `tex[base=…]` and paints last, so it
is the last full-screen **untextured** quad of a frame.
**Control first:** the quad's ramp is decoded at 10 units = 5 frames. Measured, it
is absent at frame 39 and `α=255` at 43 — 4 submitted-frame steps with one unlogged
frame in the span. The instrument reproduces the decoded quantity before being
trusted on the undecoded one.
```
frame 34 content elements begin fading (255 → 223 → 207 → 175 → 95 → 31 → 15)
frame 40 the BLACK QUAD first appears, α=102 → 127 → 255 by frame 43
frame 45 last frame the menu draws
frame 46 6 draws (vs 12) — ONE frame of black
frame 47+ the title's build starts
```
📌 **A transition is not "ramp the quad 10 units, then hold black 14".** It is
**"start the content fading, and six frames later ramp the black quad over its
declared 10 units on top of them"** — the two overlap. Total blackout is frame
34→43 = **9 frames ≈ 0.30 s**, and the gap between screens is **one frame**.
Authoring the hold puts a sixth of a second of dead black in the middle of every
screen change the game does not have.
⚠️ **Reach:** one transition, one run. The frame axis has gaps — 232 headers over
frames 3…260, ~10 % of submitted frames carry no UI draw — so every span here is
±1 frame, which is why the ramp is 4 *steps* and not a duration to three digits.
Whether the six-frame lead is constant across screens, or a property of these
elements' keyframes, is **not measured**.
### Your entries 13/14 twins — re-derived, and it is not a second witness
I reran your comparison off the disc: publisher 10 vs 13 **3.06**, developer 11 vs
14 **4.33**, control 10 vs 11 **47.91**. Identical to two decimals. ⚠️ But by your
own rule this is *your renderer twice* — same `sylpheed-cli`, same disc — so what
it establishes is that your addressing and arithmetic are right, **not** that the
twins claim has independent support. I am recording it as the former.
## ✅ 2026-08-30 — I swept the whole disc for the ordinal foot-gun. Your screens are the exposed ones.
Last iteration I retracted three claims because `--build 10/11` on `GP_TITLE` are
@@ -969,7 +1043,7 @@ discovered it after re-exporting. ❔ The sweeps may simply be a small term, and
| Q4 | button → GamePart | ✅ answered | **measured** which screen all **5** buttons open, by pressing each one and reading the screen's own title off the framebuffer. ✅ **In the form you need it: exactly ONE main-menu button opens a `GP_TITLE` entry.** `EXTRAS`**entry 6** (EN) / **9** (JP). The other four leave the archive: `NEW GAME``DIFFICULTY``SELECT DATA`; `LOAD GAME` → the save-slot list; `TUTORIAL` → the lesson list; `OPTIONS` → GAME/CONTROL/SOUND/SCREEN SETTINGS. None of those four is a `GP_TITLE` build — so a menu→submenu→back cycle inside this archive is `main menu ↔ EXTRAS` and nothing else. The **GamePart id is still a name match**, not a measurement, and 🔴 the "cheap way to measure it" this page used to point at is a dead route (the guest words are monotonic counters, not a screen id) — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q5 | navigation semantics | ✅ answered, ⚠️ **per clause** | 🔴 **This row used to open with a single `**measured**` covering six clauses of different strength, and the port's `authored/flow.json` copied that word into a `MEASURED` provenance stamp for a clause whose evidence cell reads `none`. A bundled label is exactly as strong as its weakest cell.** Split: ✅ **measured** — initial focus varies boot to boot (2× `TUTORIAL`, 2× `NEW GAME`); ⬆⬇ move **one item per press** (indirect: the 4-press wrap count only works if each press moves one) and **wrap both ends**; ⬅➡ do nothing; Ⓑ on a submenu returns to the parent **with focus restored** (4/4); Ⓑ on the main menu → **title**, ≤ 0.4 s, no loading screen (2026-08-30). ✅ **AND BOTH WEAK CLAUSES ARE NOW MEASURED (2026-08-30, later)** — you can stamp them: **no auto-repeat** (a 2.0 s held ⬇ moves the cursor exactly **once**; the counter passes its control, a single tap giving exactly 1 spike) and **Ⓑ on the title → nothing** (20 s after a delivery-confirmed Ⓑ the screen is still the title with `PRESS Ⓐ BUTTON` up — and that run waited for the **plate pulse**, the title's own settled signature, which is what the confounded earlier attempt did not). ✅ The plate **is** re-drawn after Ⓑ from the menu, ~7 s later — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) |
| Q6 | boot sequence + what drives it | ✅ answered | sequence **measured** end to end; the driver is **code, not data** — four search spaces closed, so the port **authors** the sequence — [`boot-config-and-gamepart-registry.md`](../re/boot-config-and-gamepart-registry.md) |
| Q7 | transitions | ✅ answered | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. Fade-in ramp is **decoded** from its keyframes; the ~0.4 s fade-out is **measured** (not in the file) — [`screen-transitions.md`](../re/screen-transitions.md) |
| Q7 | transitions | ✅ answered, **two numbers changed 2026-08-30** | a **fade through black**, drawn by the screen's own last-painting `.prm` quad. 🔴 **Fade-in is 12 units (0.20 s) on the menu/`EXTRAS` and 16 (0.27 s) on the title — this row's source used to say 0.874.08 s, which is the quad's CLEAR-hold, not its ramp** (a stale Python reader that shifted every keyframe time by one slot). Fade-out **is** on the disc: 10/10/8 units. ✅ **And the transition is OVERLAP, not ramp-then-hold, measured from the running game**: content elements start fading ~6 frames before the black quad's ramp begins, total blackout 9 frames ≈ 0.30 s, and the gap between screens is **one frame**. The "~14 units of black hold" this page used to carry was arithmetic and is **withdrawn** — [`screen-transitions.md`](../re/screen-transitions.md) |
| Q8 | menu audio bindings | ✅ answered | cue vocabulary + bank **decoded**; event binding is a **name match** (the authors' own event names). ✅ **You CAN have the SE audio** — ⚠️ an earlier version of this row said it was "undecodable from the disc"; that was **retracted** and the row was stale. Three cues are located in `Static.slb` and **decode to PCM**: d-pad move `0x1ec0` (4 packets), Ⓑ back `0x0ec0` (2), Ⓐ confirm `0x5d6c0` (6), all mono 48 kHz. The bank is a packed run of XMA waves with no delimiter, so a wave is only (offset, packet count) — and ⚠️ the file order is **not** cue-id order, so the index cannot be counted out — [`menu-audio-cues.md`](../re/menu-audio-cues.md) |
| Q9 | video binding + playback rules | ✅ answered | **decoded** from the movie manifest: `ADVERTISE_MOVIE`→`ADV.wmv` (boot intro *and* attract are one asset), `MS00A`→`S00A.wmv` is the new-game intro, `STAFF_ROLL`→the credits reel. ✅ **one Ⓐ skips a movie** (title at 57 s vs a 193 s baseline) — [`movie-binding.md`](../re/movie-binding.md) |
| Q10 | music-bank sub-wave roles (intro+loop?) | ✅ answered | **two stems of one performance, played together** — sample-synchronous, equal duration, 32/32 banks. **Concatenating is wrong.** Not a seamless loop either — [`structures/bgm-two-stems.md`](../re/structures/bgm-two-stems.md). ⚠️ **Our reader said three until 2026-08-29** — the extra one was the **bank header**, emitted by `to_xma_riffs`; fixed, with a 28/28 disc-wide check and two regression tests — [`structures/slb-bank-header-not-a-wave.md`](../re/structures/slb-bank-header-not-a-wave.md) |

View File

@@ -189,6 +189,20 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the
luck in one archive, not a property of the format, and it does not extend to the
screens the port has left to do.
* **A layout fix has to be swept across every READER of that layout, not just the
crate.** The keyframe record-layout fix (a pose's time precedes it) landed in
`ui_layout.rs`, and `sylpheed-cli` was found stale and rebuilt. **Two more
readers survived it**: `tools/re-capture/fade_quads.py`, which read each pose's
time from `blk+36` — the *next* record's time word — and therefore printed a
trailing untimed keyframe; and, through it,
[`screen-transitions.md`](screen-transitions.md), which labelled the quad's
**clear-hold** as its *fade-in* and published 0.874.08 s for a ramp that is
0.200.27 s. Both looked right: a shifted time series is still monotone,
plausible, and internally consistent. The tell is structural, not numeric — the
stale reader **cannot time the last pose**, so any output with a trailing `t=—`
or `-` is that bug's signature. Grep the corpus for readers of a structure
before calling its fix done.
## Runtime / emulator
* **Look at the PNG** — and check its dimensions.

View File

@@ -0,0 +1,43 @@
# Menu -> title transition, per-frame, from the running game.
# tools/re-capture/fade_decompose.sh -> log_ui_draws capture (260 frames)
# tools/re-capture/fade_envelope.py (the fade quad = last FULL-SCREEN
# UNTEXTURED quad in a frame: a .prm carries no tex[base=], and the fade
# quad paints last).
# Xenia VdSwap frame numbers. 2026-08-30.
#
# TIMING CHECK: F10 armed the capture at frame 1; the script sent (B) 1.2 s
# later. 1.2 s at 30 Hz is frame ~36. The fade begins at 34. The press and
# the transition agree without either being used to place the other.
#
# frame untextured full-screen quad alphas, in submission order
28 [[64]] draws= 12 tex= 10
29 [[64]] draws= 12 tex= 10
30 [[64]] draws= 12 tex= 10
31 [[64]] draws= 12 tex= 10
32 [[64]] draws= 12 tex= 10
34 [[64, 255], [64]] draws= 15 tex= 12 <- content starts fading
35 [[64, 223], [64]] draws= 15 tex= 12
36 [[64, 207], [64]] draws= 15 tex= 12
37 [[64, 175], [64]] draws= 14 tex= 11
39 [[64, 95], [64]] draws= 11 tex= 8
40 [[64, 31], [64], [102]] draws= 12 tex= 6 <- BLACK QUAD APPEARS
41 [[64, 15], [64], [127]] draws= 12 tex= 6
43 [[64], [64], [255]] draws= 12 tex= 6 <- fully black
44 [[64], [64], [255]] draws= 12 tex= 6
45 [[64], [64], [255]] draws= 12 tex= 6
46 [[64]] draws= 6 tex= 2 <- menu stops drawing (6 draws vs 12); ONE frame of black
47 [[64]] draws= 8 tex= 6
49 [[64]] draws= 8 tex= 6
50 [[64]] draws= 8 tex= 6
51 [[64]] draws= 8 tex= 6
52 [[64]] draws= 8 tex= 6
53 [[64]] draws= 8 tex= 6
54 [[64]] draws= 8 tex= 6
55 [[64]] draws= 8 tex= 6
56 [[73]] draws= 8 tex= 6
57 [[93]] draws= 8 tex= 6
58 [[113]] draws= 9 tex= 7
59 [[152]] draws= 12 tex= 10
60 [[172]] draws= 12 tex= 10
missing submitted frames in this span: 33->34, 37->39, 41->43, 47->49

View File

@@ -27,16 +27,33 @@ The group is always four blocks, and always this shape:
Read with the corpus's rule that a keyframe is the *start* of a ramp
([`structures/ui-resting-pose.md`](structures/ui-resting-pose.md)).
🔴 **The table this section used to print was taken with a STALE READER, and both
its numbers were wrong (corrected 2026-08-30).** `fade_quads.py` read each pose's
time from `blk+36` — the *next* record's time word — so every time was shifted one
slot and the last pose came out untimed (`t=—`). That is the same association the
[record-layout fix](ui-keyframe-record-layout.md) retired in the crate; the Python
helper was never swept with it. Fixed, and controlled against the rebuilt
`screen info`, which prints `[0 12 70 80]` for the same element:
```
$ tools/re-capture/fade_quads.py 4 5 6 # GP_TITLE
build 4 (title) pteff00.prm t=16 α=255 t=261 α=0 t=269 α=0 t=— α=255
build 5 (main menu) pteff00.prm t=12 α=255 t= 70 α=0 t= 80 α=0 t=— α=255
build 6 (EXTRAS) pteff00.prm t=12 α=255 t= 64 α=0 t= 74 α=0 t=— α=255
build 4 (title) pteff00.prm t= 0 α=255 t= 16 α=0 t=261 α=0 t=269 α=255
build 5 (main menu) pteff00.prm t= 0 α=255 t= 12 α=0 t= 70 α=0 t= 80 α=255
build 6 (EXTRAS) pteff00.prm t= 0 α=255 t= 12 α=0 t= 64 α=0 t= 74 α=255
```
Under [Q1](ui-keyframe-time-unit.md)'s `1 unit = 1/60 s`: the screen holds black
for **0.20 s**, then fades in over **0.87 s** (`EXTRAS`), **0.97 s** (main menu)
or **4.08 s** (the title).
**Every pose is timed. There is no untimed keyframe**, and the fade-out ramp is on
the disc after all: `70 → 80` = 10 units for the main menu, `64 → 74` = 10 for
`EXTRAS`, `261 → 269` = **8** for the title.
⚠️ **And the fade-IN was mislabelled by the same shift.** This page used to say the
screen "holds black for **0.20 s**, then fades in over **0.87 s** (`EXTRAS`),
**0.97 s** (main menu) or **4.08 s** (the title)". Those spans are `T2 T1` under
the stale pairing — the stretch where the quad sits at **α = 0**, i.e. the screen
fully visible and *not* fading at all. Read correctly the screen starts black at
`t = 0` and fades in over **12 units (0.20 s)** on the menu and `EXTRAS`, **16
units (0.27 s)** on the title. A port pacing its menu fade-in off the old number
would have run it **5× too slow**.
### ❔ The fade-OUT duration is not in this field
@@ -148,11 +165,65 @@ units ≈ 0.167 s**, not the ~24 units this page authored. That confirms the por
agent's reading; I tried to refute it against the bytes and could not.
🔴 **So the measured ~0.4 s is NOT the ramp alone** — 0.4 s is ~24 units against a
decoded 10. Something else occupies the other ~14 units. 🟡 That the remainder is
exactly the black hold is *arithmetic that fits* (14 units = 0.233 s, inside this
corpus's own 0.170.23 s plateau), **not a measurement** — and a fit that closes a
question without evidence is what this pair of agents has been catching all week.
The decomposition stays open.
decoded 10. Something else occupies the other ~14 units.
## ✅ What the other ~14 units are — MEASURED 2026-08-30, and it is not a hold
This page previously guessed: *"that the remainder is exactly the black hold is
arithmetic that fits (14 units = 0.233 s), **not a measurement**"*. It has now been
measured, and **the guess was wrong**. There is no black hold inside the fade-out.
**Instrument.** [`fade_decompose.sh`](../../tools/re-capture/fade_decompose.sh)
boots to the main menu, arms the UI draw capture there, then presses Ⓑ, so one
260-frame window contains the whole screen change.
[`fade_envelope.py`](../../tools/re-capture/fade_envelope.py) reads the fade quad's
alpha per submitted frame. The quad is **identified, not guessed at**: a `.prm`
primitive carries no `tex[base=…]`, and this element paints last
([paint-order key](structures/ui-paint-order-key.md)), so it is the last
full-screen *untextured* quad of a frame. Taking merely the last full-screen quad
picks up textured backdrops and gives a different answer.
**Control.** The quad's ramp is *decoded* (10 units), so the instrument can be
checked before it is believed. At Q1's 2 units per rendered frame, 10 units is 5
frames. Measured: the quad is absent at frame 39 and `α=255` at frame 43 — **4
submitted-frame steps**, with one unlogged frame inside the span. Agreement to
within that one frame. An instrument that could not reproduce the decoded ramp
could not be trusted on the undecoded remainder.
**The measurement** ([`data/fade-envelope-menu-to-title.txt`](data/fade-envelope-menu-to-title.txt)):
```
frame 34 content elements begin fading (a 255 quad appears and decays)
35..41 255 223 207 175 95 31 15 the content fade
frame 40 the BLACK QUAD first appears, α=102
41 α=127
43 α=255 fully black
45 last frame the menu draws
frame 46 6 draws (vs 12) — ONE frame of black
47+ the title's build starts
```
✅ **The ~14 extra units are the content elements' own fade-outs, which start six
frames BEFORE the quad's ramp and overlap it.** The blackout runs frame 34 → 43 =
**9 submitted frames ≈ 0.30 s at 30 Hz**, of which the quad's ramp is the last 4.
That is the same quantity the filmstrip measured as 0.3670.400 s with coarser
timing, and it decomposes as **overlap, not sequence**.
**The inter-screen black is ONE frame** (46), not ~14 units. The draw count
collapses from 12 to 6 for exactly one frame and the incoming build starts at 47.
📌 **For the port:** a transition is *not* "ramp the black quad for 10 units, then
hold black for 14". It is "start the content elements fading, and 6 frames later
ramp the black quad over its declared 10 units on top of them". Authoring it as a
hold puts a sixth of a second of dead black in the middle of every screen change
that the game does not have.
⚠️ **Reach.** One transition (main menu → title, via Ⓑ), one run. The frame axis
has gaps — 232 `--- frame` headers over frames 3…260, so ~10 % of submitted frames
carry no UI draw — which is ±1 frame on any span quoted here and is why the ramp is
given as 4 steps rather than a duration to three digits. Whether the six-frame lead
is constant across screens, or is a property of *these* elements' keyframes, is
**not** measured.
| elements | final untimed block | what it does |
|---|---|---|

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Decompose a screen transition's ~0.4 s fade-out into RAMP + HOLD, at the
# emulator's own frame granularity.
#
# docs/re/screen-transitions.md measures the fade-out as ~0.4 s (~24 units) while
# `pteff00.prm`'s final declared ramp is 70->80 = 10 units. The remainder is
# currently ARITHMETIC THAT FITS -- "the other 14 units must be the black hold"
# -- and that page says so itself. This measures it instead.
#
# The design point: arm the UI draw capture ON THE MAIN MENU, then press (B).
# One capture window then contains
# * the menu's fade-OUT -- the unknown, and
# * the title's fade-IN -- whose ramp IS decoded from the file (build 4's
# pteff00.prm, t=16 a=255 -> t=261 a=0, 245 units),
# so the run carries its own control: an instrument that cannot reproduce the
# known fade-in cannot be trusted on the unknown fade-out.
#
# Traps inherited from menu_draw_capture.sh, both already paid for:
# * F10 arms the capture AND opens the emulator menu bar; any Xenia UI makes
# IsUIActive() true and every later guest keystroke is swallowed. Click the
# game surface to dismiss before touching the pad.
# * a 0.12 s tap gets missed; hold (B) 0.5 s and confirm [RE-INPUT] delivery.
#
# Usage: fade_decompose.sh [out_dir]
set -u
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
SD="$(cd "$(dirname "$0")" && pwd)"
OUT="${1:-/sylph-home/re/fadecap}"
mkdir -p "$OUT"; rm -f "$OUT"/xenia_re_ui_draws_*.log
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
shot(){ screenshot "$1" >/dev/null 2>&1; }
screen(){ shot /tmp/fdc.png; python3 "$SD/screen_id.py" /tmp/fdc.png | awk '{print $1}'; }
( cd "$OUT" && nohup run-canary --mem_watch=false --log_ui_draws=true \
--ui_draw_capture_frames="${FRAMES:-260}" \
--ui_draw_capture_max="${MAXDRAWS:-400000}" \
--logged_profile_slot_0_xuid=B13EBABEBABEBABE \
>"$OUT/canary.stdout" 2>"$OUT/canary.stderr" & )
sleep 8
until xdotool search --name "Xenia-canary" >/dev/null 2>&1; do
[ -n "$(alive)" ] || { echo "EMULATOR GONE"; exit 4; }; sleep 1
done
win="$(xdotool search --name "Xenia-canary" | tail -1)"
# 1. wait for the boot title; do not tap through the intro (a run that tapped
# every 4 s delivered 88 presses and ended on a black screen).
deadline=$(( SECONDS + 420 )); s=""
while [ $SECONDS -lt $deadline ]; do
s="$(screen)"; echo "t=${SECONDS}s $s"
[ "$s" = "title" ] && break
sleep 4
done
[ "$s" = "title" ] || { echo "NEVER REACHED THE TITLE"; exit 1; }
# 2. one (A) on the boot title -> main menu
python3 "$SD/pad.py" tap A 0.5
for _ in 1 2 3 4 5 6; do
sleep 4; s="$(screen)"; echo " after A: $s"
[ "$s" = "menu" ] && break
done
[ "$s" = "menu" ] || { echo "NO MENU (screen=$s)"; exit 2; }
shot "$OUT/menu.png"
# 3. arm the capture, dismiss the menu bar F10 opened, then press (B).
# Everything between F10 and (B) is spent inside the capture window, so keep
# it short: the window is FRAMES submitted frames, not seconds.
xdotool windowactivate --sync "$win"; sleep 1
xdotool key F10; sleep 0.6
xdotool mousemove 900 400 click 1; sleep 0.6
echo "-- (B) at $(date +%S.%N) --"
python3 "$SD/pad.py" tap B 0.5
sleep 12
shot "$OUT/after-b.png"; echo "screen after B: $(screen)"
grep -c "RE-INPUT" "$OUT/canary.stdout" 2>/dev/null | sed 's/^/[RE-INPUT] lines: /'
ls -l "$OUT"/xenia_re_ui_draws_*.log 2>/dev/null || echo "NO CAPTURE LOG"
grep -i "UI-CAP" "$OUT/canary.stdout" | tail -3
echo "FADE CAPTURE DONE (emulator left running)"

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Per-frame alpha of the fade quad (`pteff00.prm`), from a `log_ui_draws` capture.
`screen-transitions.md` measures a screen change's fade-out as a ~0.4 s lump and
then SPLITS it by arithmetic -- the declared ramp is 10 units, 0.4 s is ~24, "so
the other ~14 must be the black hold". That page flags the split as a fit, not a
measurement. This measures it.
Identifying the quad, rather than guessing at it: the fade quad is a `.prm`
PRIMITIVE, so its draw carries NO `tex[base=...]`, and it is its screen's
last-painting element (structures/ui-paint-order-key.md). So: per frame, the LAST
full-screen draw with no bound texture. Taking merely the last full-screen quad
picks up textured backdrops and gets a different answer.
⚠️ The frame axis has gaps. A 260-frame window produced 232 `--- frame` headers,
so ~10 % of submitted frames carry no UI draw at all. A duration in frames is
therefore +-1 frame per gap it spans, and this prints the gaps so a reader can
see which spans are affected.
fade_envelope.py <capture.log>
"""
import re
import sys
sys.path.insert(0, __file__.rsplit("/", 1)[0])
W, H = 1280, 720
VERT = re.compile(r"col=([0-9A-F]{8})")
def envelope(log):
"""Yield (frame, alpha|None) -- alpha of the last untextured full-screen quad."""
frame, pending_untex = None, None
last = {}
seen = []
for line in open(log):
if line.startswith("--- frame"):
if frame is not None:
seen.append(frame)
frame = int(line.split()[2])
continue
m = re.match(r"\s*(\d+) prim=(\d+) indices=(\d+)", line)
if m:
# a primitive draw has no bound texture
pending_untex = ("tex[base=" not in line) and m.group(2) == "13"
continue
if "vb=0x" in line and pending_untex:
cols = VERT.findall(line)
# full-screen NDC quad: every vertex at +-1
if cols and line.count("[-1.00,1.00,") >= 1:
last[frame] = int(cols[-1][:2], 16)
pending_untex = None
if frame is not None:
seen.append(frame)
return last, seen
def main():
last, seen = envelope(sys.argv[1])
gaps = [(a, b) for a, b in zip(seen, seen[1:]) if b != a + 1]
print(f"# {len(seen)} frame headers, {seen[0]}..{seen[-1]}; "
f"{sum(b-a-1 for a,b in gaps)} submitted frames carry no UI draw")
print("# gaps: " + " ".join(f"{a}->{b}" for a, b in gaps))
print("frame alpha")
for f in seen:
a = last.get(f)
print(f"{f:6d} {'-' if a is None else a:>5}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -34,7 +34,13 @@ def parse(bundle):
if blk+36 > len(bundle) or blk+36 > end: break
g.append(dict(fade=be32(bundle,blk), sx=be32(bundle,blk+16), sy=be32(bundle,blk+20),
x=struct.unpack_from(">i",bundle,blk+28)[0], y=struct.unpack_from(">i",bundle,blk+32)[0],
t=(be32(bundle,blk+36) if blk+40<=end else None)))
# A POSE'S TIME PRECEDES IT (ui-keyframe-record-layout.md).
# This read `blk+36` -- the NEXT record's time word --
# which shifted every time by one slot and left the
# last pose untimed. That stale association is what
# made screen-transitions.md print a 0.87-4.08 s
# fade-in and an untimed fade-out. Corrected 2026-08-30.
t=be32(bundle,blk-4)))
groups[idx]=g; pos=end
return names, groups