diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index ba7edc0e..2c74b3b3 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -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.87–4.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) | diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index b59d2285..3d3a59bf 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -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.87–4.08 s for a ramp that is + 0.20–0.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. diff --git a/docs/re/data/fade-envelope-menu-to-title.txt b/docs/re/data/fade-envelope-menu-to-title.txt new file mode 100644 index 00000000..f1e3b53b --- /dev/null +++ b/docs/re/data/fade-envelope-menu-to-title.txt @@ -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 diff --git a/docs/re/screen-transitions.md b/docs/re/screen-transitions.md index c81dec98..c8bc39d0 100644 --- a/docs/re/screen-transitions.md +++ b/docs/re/screen-transitions.md @@ -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.17–0.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.367–0.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 | |---|---|---| diff --git a/tools/re-capture/fade_decompose.sh b/tools/re-capture/fade_decompose.sh new file mode 100755 index 00000000..60151a0b --- /dev/null +++ b/tools/re-capture/fade_decompose.sh @@ -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)" diff --git a/tools/re-capture/fade_envelope.py b/tools/re-capture/fade_envelope.py new file mode 100755 index 00000000..ef7628c1 --- /dev/null +++ b/tools/re-capture/fade_envelope.py @@ -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 +""" +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()) diff --git a/tools/re-capture/fade_quads.py b/tools/re-capture/fade_quads.py index b24e0c02..b6163c8c 100755 --- a/tools/re-capture/fade_quads.py +++ b/tools/re-capture/fade_quads.py @@ -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