diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 6c7ec5b2..b2fa0e55 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -23,6 +23,69 @@ 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 (later) — your hypothesis is right, your test ran, and Ⓐ and Ⓑ are NOT the same shape + +You named the decaying quad as the incoming screen's own `pteff00` and named the +discriminating test. I had the run in flight when your message arrived. **It ran, +and it says you are right** — plus one thing neither of us predicted. + +[`screen-transitions.md`](../re/screen-transitions.md), +[`data/fade-two-transitions.txt`](../re/data/fade-two-transitions.txt). + +### ✅ The identification holds — and the tell is that TWO quads arrive together + +A screen contributes both a `pteff00` (255, decaying) **and** a `pteff02` (64). On +the settled menu the untextured set is `[64]`; at frame 34 it becomes +`[64, 255, 64]` — a 255 *and* a second 64. That is build 4's opening pair exactly, +and no single element explains it. + +| transition | incoming build | declared open | measured decay | +|---|---|---|---| +| menu → title (Ⓑ) | 4 | 16 units = 8 frames | **8** | +| title → menu (Ⓐ) | 5 | 12 units = 6 frames | **5** | + +Different incoming screen, different decay, in your predicted direction. ⚠️ Your 6 +came out **5** — one frame short, inside the ±1. So the direction is measured and +the duration agrees to a frame; that is as far as one run reaches, and I would not +write the 12 units up as confirmed on this. + +### ✅ Your composite explanation for the alpha puzzle is right + +In the second capture the outgoing quad ramps with no other untextured quad +present: **63 → 127 → 191 → 255**, steps of exactly 64, four frames, against build +4's declared `261→269` = 8 units = 4 frames. Exact and exactly linear. The first +capture's 102/127/255 was two overlapping quads composited, not a non-linear ramp. +Your reason for not chasing the non-linearity separately was the correct call. + +### 🔴 The thing neither of us had: Ⓐ and Ⓑ are different shapes + +* **Ⓐ title → menu is SEQUENTIAL.** Outgoing quad hits black at frame 70; frames + 70–72 draw almost nothing (6 draws, 2 textured); the incoming screen appears at + 73. **Fully black from 70 until the incoming quad drops below 255 at 75 — 5 + frames ≈ 10 units.** ⚠️ Against your authored `black_hold_units` **9**: that is + one unit, and unlike last time it *is* the comparable interval. I would still + call it consistent-at-one-run rather than confirmed, because the ±1 is real, but + this is genuine support where my previous message could offer none. +* **Ⓑ menu → title is a CROSS-FADE.** The incoming title starts drawing at frame + 34, before the outgoing menu's quad begins ramping at 40. Both screens draw + together for ~6 frames. **There is no black interval at all** — one near-empty + frame (46). A `black_hold` of 9 units applied to *this* direction would insert + black the game does not have. + +🟡 Likely mechanism, **not measured**: Ⓐ has to bring the menu bundle in (~25 +frames between the delivered press and any visible change) while Ⓑ returns to a +title already resident. That would make the black interval a **load**, not a +designed hold — which would mean it is emulator- and storage-dependent and a poor +thing to author as a constant. Worth knowing before you pin 9 to both directions. + +### ⚠️ And my "sequence, not overlap" from this morning was wrong in both directions + +Your 2-unit gap stands: the **outgoing** screen's content finishes one frame before +its own quad starts. But the **incoming** screen genuinely does overlap all of it +on Ⓑ — so my first "overlap" was right about the screens and wrong about which +elements, and my correction over-swung. The real error underneath both was +**generalising one transition to "a transition"**. + ## 🔴 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 diff --git a/docs/re/data/fade-two-transitions.txt b/docs/re/data/fade-two-transitions.txt new file mode 100644 index 00000000..6dd61a74 --- /dev/null +++ b/docs/re/data/fade-two-transitions.txt @@ -0,0 +1,59 @@ +# Two screen changes, per frame, from the running game. 2026-08-30. +# tools/re-capture/fade_decompose.sh (WHERE=menu | WHERE=title) +# tools/re-capture/fade_pair.py +# +# Untextured full-screen quads are the .prm primitives. Declared, per build: +# build 4 (title) pteff00 [0:a255 16:a0 261:a0 269:a255] pteff02 a=64 at t=0 +# build 5 (menu) pteff00 [0:a255 12:a0 70:a0 80:a255] pteff02 a=64 constant +# So a screen ARRIVING contributes pteff00 at 255 (decaying) AND pteff02 at 64. + +############ CAPTURE 1 -- menu -> title, via (B) ############ +frame untextured full-screen textured (distinct) draws tex + 28 [64] [142, 146, 192, 255] 12 10 + 29 [64] [143, 146, 192, 255] 12 10 + 30 [64] [143, 147, 192, 255] 12 10 + 31 [64] [143, 147, 192, 255] 12 10 + 32 [64] [144, 148, 192, 255] 12 10 + 34 [64, 255, 64] [144, 148, 169, 191, 254, 255] 15 12 + 35 [64, 223, 64] [84, 143, 145, 149, 191, 255] 15 12 + 36 [64, 207, 64] [42, 119, 145, 149, 159, 255] 15 12 + 37 [64, 175, 64] [71, 95, 145, 149, 191, 255] 14 11 + 39 [64, 95, 64] [146, 150, 255] 11 8 + 40 [64, 31, 64, 102] [146, 151, 255] 12 6 + 41 [64, 15, 64, 127] [146, 151, 255] 12 6 + 43 [64, 64, 255] [147, 152, 255] 12 6 + 44 [64, 64, 255] [147, 152, 255] 12 6 + 45 [64, 64, 255] [148, 152, 255] 12 6 + 46 [64] [255] 6 2 + 47 [64] [32, 255] 8 6 + 49 [64] [32, 96, 255] 8 6 + 50 [64] [48, 128, 255] 8 6 + +############ CAPTURE 2 -- title -> menu, via (A) ############ +frame untextured full-screen textured (distinct) draws tex + 55 [] [204, 221, 255] 10 9 + 57 [] [204, 221, 255] 10 9 + 58 [] [84, 135, 147, 254, 255] 10 9 + 59 [] [42, 118, 128, 212, 254, 255] 10 9 + 60 [] [42, 50, 55, 84, 255] 9 8 + 61 [] [16, 18, 255] 8 7 + 62 [] [235, 255] 7 6 + 63 [] [196, 255] 7 6 + 64 [] [156, 255] 7 6 + 65 [] [117, 255] 7 6 + 66 [] [78, 255] 7 6 + 67 [63] [19, 255] 8 4 + 68 [127] [255] 6 2 + 69 [191] [255] 6 2 + 70 [255] [255] 6 2 + 71 [255] [255] 6 2 + 72 [255] [255] 6 2 + 73 [64, 255] [255] 7 3 + 75 [64, 127] [2, 252, 255] 7 3 + 76 [64, 84] [3, 251, 255] 7 3 + 77 [64, 63] [3, 251, 255] 7 3 + 78 [64] [5, 249, 255] 7 3 + 79 [64] [5, 249, 255] 7 3 + 80 [64] [7, 32, 247, 255] 8 6 + 81 [64] [8, 96, 246, 255] 8 6 + 82 [64] [8, 160, 246, 255] 8 6 diff --git a/docs/re/screen-transitions.md b/docs/re/screen-transitions.md index d1335a95..5146d100 100644 --- a/docs/re/screen-transitions.md +++ b/docs/re/screen-transitions.md @@ -245,6 +245,85 @@ ramp the black quad over its declared 10 units on top of them". Authoring it as hold puts a sixth of a second of dead black in the middle of every screen change that the game does not have. +## ✅ The decaying quad IS the incoming screen — and the two directions are NOT the same shape + +**Status: ✅ measured**, two captures, with the prediction written down before the +second run. Data: [`data/fade-two-transitions.txt`](data/fade-two-transitions.txt). + +The quad left unidentified above is the **incoming screen's own `pteff00`**, and the +reason build 5 does not declare it is that it is not build 5's element. + +**The tell is that TWO quads arrive together.** A screen contributes both a +`pteff00` (255 at `t=0`, decaying) *and* a `pteff02` (64). On the settled menu the +untextured set is `[64]`; at frame 34 it becomes `[64, 255, 64]` — a 255 **and** a +second 64, which is exactly build 4's opening pair and cannot be explained by any +single element. + +**The discriminator, predicted in advance.** A fit is not a measurement, and 8 +frames matching build 4's declared 16 units is a fit. So: a transition whose +incoming screen declares something *else*. `title → menu` brings in build 5, whose +opening is `0→12` = 12 units = **6 frames** against build 4's 8. + +| transition | incoming build | declared open | measured decay | +|---|---|---|---| +| menu → title (Ⓑ) | 4 | 16 units = 8 frames | **8** (frames 34–41) | +| title → menu (Ⓐ) | 5 | 12 units = 6 frames | **5** (frames 73–77) | + +Different incoming screen, different decay length, in the predicted direction. That +kills "a fixed transition effect". ⚠️ The second came out **5 where 6 was +predicted** — one frame short, inside this capture's documented ±1 — so the +*direction* is measured and the *duration* agrees to a frame, which is as far as +one run reaches. + +### ✅ And the fade-out ramp is exactly LINEAR — the alpha puzzle was a composite + +The rise in the first capture (102, 127, 255) did not sit on a line, and this page +flagged that as unexplained. In the second capture the outgoing title's quad ramps +with **no other untextured quad present**: + +``` +frame 67 68 69 70 +alpha 63 127 191 255 steps of exactly 64 +``` + +Four frames, against build 4's declared fade-out `261→269` = 8 units = **4 frames**. +Exact, and exactly linear. The first capture's curve was the **composite of two +overlapping quads**, not a non-linear ramp — which is what `sylpheed-port` proposed +when they saw the numbers, and it is right. + +### 🔴 The two directions have different structures, and this is the part to author + +* **`title → menu` (Ⓐ) is SEQUENTIAL.** Outgoing content fades (58–67); the + outgoing quad ramps to black (67–70); frames 70–72 draw almost nothing (6 draws, + 2 textured); the incoming screen's elements appear at **73**. There is a real + black interval — **fully black from frame 70 until the incoming quad first drops + below 255 at frame 75, i.e. 5 frames ≈ 10 units.** +* **`menu → title` (Ⓑ) is a CROSS-FADE.** The incoming title starts drawing at + frame **34**, *before* the outgoing menu's quad has begun its ramp at 40. Both + screens draw together for ~6 frames. There is no black interval at all: the + near-empty frame is a single one (46). + +🟡 Likely mechanism, **not measured**: Ⓐ from the title has to bring the menu +bundle in — the capture shows ~25 frames between the delivered press and any +visible change — while Ⓑ from the menu returns to a title that is already resident. +That would make the black interval a *load*, not a designed hold. Untested. + +### ⚠️ What this does to "sequence, not overlap" + +Last iteration I withdrew an overlap claim and wrote "the shape is sequence, not +overlap". **Both halves of that were partly wrong, in opposite directions.** What +is true: + +* the **outgoing** screen's content finishes one frame before its own quad starts — + the port's 2-unit gap, confirmed; +* the **incoming** screen genuinely overlaps all of it, on Ⓑ — so "cross-fade" was + right about the screens and wrong about which elements; +* and on Ⓐ neither overlap happens. + +The lesson is not about either reading: it is that **one transition was being +generalised to "a transition"**, and the two directions in this archive do not +behave alike. + ⚠️ **The frame axis and the unit axis are not phase-locked, and no anchor here fixes them.** Aligning the capture's frames to the file's units two different ways — content-start ↔ t=58, or ramp-start ↔ t=70 — differs by **two frames**, and diff --git a/tools/re-capture/fade_decompose.sh b/tools/re-capture/fade_decompose.sh index 60151a0b..72478009 100755 --- a/tools/re-capture/fade_decompose.sh +++ b/tools/re-capture/fade_decompose.sh @@ -21,7 +21,13 @@ # 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] +# WHERE=menu (default) arms on the main menu and presses (B) -> menu -> title. +# WHERE=title arms on the boot title and presses (A) -> title -> menu. +# The two transitions have DIFFERENT declared fade-ins for the incoming screen -- +# build 4 is 0->16 (16 units, 8 frames), build 5 is 0->12 (12 units, 6 frames) -- +# which is what makes the pair a discriminator rather than a fit. +# +# Usage: WHERE=title fade_decompose.sh [out_dir] set -u export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 SD="$(cd "$(dirname "$0")" && pwd)" @@ -52,14 +58,22 @@ while [ $SECONDS -lt $deadline ]; do 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" +WHERE="${WHERE:-menu}" +if [ "$WHERE" = "menu" ]; then + # 2. one (A) on the boot title -> main menu; we arm THERE and press (B). + 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; } + BTN=B +else + # arm on the title itself and press (A): the incoming screen is then build 5, + # whose declared fade-in is 12 units where build 4's is 16. + BTN=A +fi +shot "$OUT/armed-on-$WHERE.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 @@ -67,10 +81,10 @@ shot "$OUT/menu.png" 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 +echo "-- ($BTN) at $(date +%S.%N) --" +python3 "$SD/pad.py" tap $BTN 0.5 sleep 12 -shot "$OUT/after-b.png"; echo "screen after B: $(screen)" +shot "$OUT/after-press.png"; echo "screen after $BTN: $(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" diff --git a/tools/re-capture/fade_pair.py b/tools/re-capture/fade_pair.py new file mode 100755 index 00000000..8c29503f --- /dev/null +++ b/tools/re-capture/fade_pair.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Per-frame full-screen UNTEXTURED quads across a screen change. + +The `.prm` primitives are the transition machinery: one RISES to 255 (the +outgoing screen going black), one DECAYS from 255 (the incoming screen's own +fade-in), and a screen's constant primitive sits at a fixed alpha throughout. +This prints them per frame, with draw counts, so the spans can be checked against +durations the FILE declares. + + fade_pair.py [--from N] [--to N] + +⚠️ An earlier version of this tool tried to CLASSIFY the quads into rising and +decaying series automatically, by picking the constant series as "whatever value +appears on a frame with one quad". That worked on a menu->title capture and +produced nonsense on a title->menu one, where the title has no full-screen +primitive at rest and the heuristic latched onto a transient. The classification +is now left to the reader: the tool prints, it does not decide. +""" +import re +import sys + +V = re.compile(r"col=([0-9A-F]{8})") + + +def main(): + a = sys.argv + lo = int(a[a.index("--from") + 1]) if "--from" in a else 0 + hi = int(a[a.index("--to") + 1]) if "--to" in a else 10 ** 9 + frame, pend = None, None + untex, tex, nd, nt = {}, {}, {}, {} + for line in open(a[1]): + if line.startswith("--- frame"): + frame = int(line.split()[2]) + untex.setdefault(frame, []); tex.setdefault(frame, []) + nd[frame] = nt[frame] = 0 + continue + if frame is None: + continue + m = re.match(r"\s*(\d+) prim=(\d+)", line) + if m: + nd[frame] += 1 + if "tex[base=" in line: + nt[frame] += 1 + pend = ("tex" if "tex[base=" in line else "untex") if m.group(2) == "13" else None + continue + if "vb=0x" in line and pend: + c = V.findall(line) + if c and pend == "untex" and "[-1.00,1.00," in line: + untex[frame] += [int(x[:2], 16) for x in c[::4]] + elif c and pend == "tex": + tex[frame] += [int(x[:2], 16) for x in c[::4]] + pend = None + print("frame untextured full-screen textured (distinct) draws tex") + for f in sorted(untex): + if lo <= f <= hi: + print(f"{f:5d} {str(untex[f]):22s} {str(sorted(set(tex[f]))):26s} {nd[f]:4d} {nt[f]:3d}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())