From 43dba9d6e0b0cd8b48a679427db25724d0071ca8 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Fri, 28 Aug 2026 18:05:19 +0000 Subject: [PATCH] re: the transition between screens is a fade through black, and most of its timing is on the disc Q7. Every title-side screen carries a full-screen black .prm quad that paints last, and its keyframe group IS the transition: black at T0, clear by T1, clear until T2, then back to black on exit. Read with the corpus's start-of-a-ramp rule and Q1's time unit that gives 0.87s for EXTRAS, 0.97s for the main menu, 4.08s for the title -- from the file, not from a stopwatch. The disc-wide check is per-pak all-or-nothing rather than the 41% the headline count suggests, and GP_TITLE's 6 of 12 is the useful row: the six builds carrying a fade quad are exactly the six SCREENS, and the six without are exactly the six overlays. GP_DIALOG is 0 of 133. That is independent corroboration of the overlay finding from two iterations ago. One piece is NOT on the disc and says so: the fade-OUT length. The fourth keyframe has no time slot, because a group's last block stops four bytes short. Measured instead, at 30fps, ~0.4s and the same both directions. And a warning I earned: the luminance rise after a transition is NOT the quad's ramp. The incoming screen's own elements animate in after the quad has cleared -- 1.47s observed against a declared 0.97s. Time the fade from where the frame is pure black. Rig: screenshot samples at 0.5 Hz and cannot see a 0.4s fade at all, which is why an earlier burst called this an instant cut. ffmpeg x11grab at 30fps instead; both go in METHOD. --- docs/port/HANDOFF.md | 23 +- docs/re/INDEX.md | 1 + docs/re/METHOD.md | 12 + docs/re/REFUTED.md | 5 + .../transitions/transition-luminance.csv | 421 ++++++++++++++++++ docs/re/screen-transitions.md | 117 +++++ tools/re-capture/fade_quads.py | 52 +++ 7 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 docs/re/captures/transitions/transition-luminance.csv create mode 100644 docs/re/screen-transitions.md create mode 100755 tools/re-capture/fade_quads.py diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 9773e935..1c12fd16 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -31,9 +31,9 @@ authored version can be deleted. | Q2 | which build is which screen state | ✅ answered | `GP_TITLE` is **8 screens shipped twice, EN/JP**: 4/7 title art, 2/3 the `PRESS Ⓐ` plate, 5/8 main menu, 6/9 `EXTRAS`, 0/1 and 10/11 two unidentified `DELTASABER` plates — [`ui-title-build-map.md`](../re/ui-title-build-map.md) | | Q3 | paint order for the six screens | ✅ answered | **decoded**: a `u16` layer key at `+0x0A` of each `T8aD` sprite header, stable-sorted with declaration index; unkeyed elements get an implied key. Confirmed on 5 measured orders + `EXTRAS` vs a capture. One residual: the **tie-break** is unknown and bites on one element of the title — [`structures/ui-paint-order-key.md`](../re/structures/ui-paint-order-key.md) | | Q4 | button → GamePart | 🟡 mostly answered | **measured** which screen each button opens (4 of 5; `NEW GAME` untested — it hangs). The **GamePart id is a name match** onto the decoded id table, not a measurement — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | -| Q5 | navigation semantics | ✅ answered | **measured**: opens on `TUTORIAL` (not the top); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | +| Q5 | navigation semantics | ✅ answered | **measured**: initial focus varies boot to boot (2× `TUTORIAL`, 1× `NEW GAME`); ⬆⬇ one step, **wraps both ends**; ⬅➡ do nothing; Ⓑ returns to the parent **with focus restored**; Ⓑ on the main menu → title; Ⓑ on the title → nothing — [`menu-navigation-semantics.md`](../re/menu-navigation-semantics.md) | | Q6 | boot sequence + what drives it | 🟡 partial | order observed, and the **attract cycle is timed**: ~8–10 s idle on the title → fade to black → ~85 s of video → title again, plate and all. The driver is still not decoded | -| Q7 | transitions | ❔ open | | +| 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) | | Q8 | menu audio bindings | ❔ open | cue table complete, event binding is not | | Q9 | video binding + playback rules | 🟡 partial | `ADV.wmv` is the boot intro; new-game intro unidentified | | Q10 | music-bank sub-wave roles (intro+loop?) | ❔ open | we concatenate blindly today | @@ -97,10 +97,9 @@ authored version can be deleted. * **Menu movement, measured off the running game.** ⬆⬇ move one item per press and **wrap at both ends** (5-item main menu and 3-item `EXTRAS` both). ⬅➡ do nothing. Ⓑ goes up one level **and restores focus to the item you came from**; - Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. The main - menu opens focused on **`TUTORIAL`**, the middle item — 🟡 reproducible in this - harness (2/2) but a third recorded run implies `NEW GAME`, so re-test before - hardcoding it. All **measured**, none of it on the disc. + Ⓑ on the main menu returns to the title; Ⓑ on the title does nothing. **Initial + focus is not stable**: three boots of the same script gave `TUTORIAL`, + `TUTORIAL`, `NEW GAME`. Do not hardcode it; pick one and say you picked it. All **measured**, none of it on the disc. * **Each button's destination is measured; its GamePart id is not.** `LOAD GAME` → the save-slot list, `TUTORIAL` → the lesson list, `OPTIONS` → the settings menu, `EXTRAS` → `GP_TITLE` build 6, `EXTRAS ▸ MISSION SELECT` → @@ -108,6 +107,18 @@ authored version can be deleted. GamePart ids (`3`, `25`, `8`, `5`, `7`) are the entries of the decoded id table whose **names match the screens seen**; that binding is authored, not measured. +* **A screen change is a fade through black.** Each screen carries a full-screen + black `.prm` quad that paints last (`pteff00.prm` / `pfeff00.prm`) whose + keyframe group *is* the transition: black at `T0`, clear by `T1`, clear until + `T2`, then back to black on exit. ✅ decoded, with a disc-wide check — and in + `GP_TITLE` exactly the six **screen** builds carry it while the six overlays do + not. The fade-in length is `T1 − T0` and is read from the file (0.87 s for + `EXTRAS`, 0.97 s main menu, 4.08 s title). ❔ **The fade-OUT length is not on + the disc** — the last keyframe has no time slot; **measured ~0.4 s**, twice. + The black hold measures 0.17–0.23 s. ⚠️ Do not time the fade-in off a capture's + brightness: the incoming screen's own element animations dominate it and run + much longer than the quad. + * **The GamePart id table** — 29 entries at `.rdata 0x820A1630`, confirmed by the executable's own registration strings. ✅ This is the screen vocabulary; which button reaches which entry is Q4 and is *not* part of it. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index a9831e10..90957d48 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -150,6 +150,7 @@ files, which is how the same ground got covered twice. | [`ui-paint-order-third-permutation.md`](ui-paint-order-third-permutation.md) | A third measured paint order — tool built and validated, screen not reached | ✅ the reader works and is CONFIRMED against both previously | | [`ui-quad-class-foothold.md`](ui-quad-class-foothold.md) | The guest's UI quad class — a foothold found from the capture's vertex layout | 🟡 PROBABLE for the identification below (it is a static read, but | | [`menu-navigation-semantics.md`](menu-navigation-semantics.md) | The title menu — how it moves, and where each button goes | ✅ measured: wraps both ends, Ⓑ restores focus, ⬅➡ inert; 4 of 5 destinations driven. 🟡 GamePart id is a name match, ❔ `NEW GAME` untested | +| [`screen-transitions.md`](screen-transitions.md) | Between two screens — a fade through black, and where its timing lives | ✅ the fade quad's keyframe group is decoded (disc-wide: per-pak all-or-nothing; `GP_TITLE` = the 6 screens, not the 6 overlays); the ~0.4 s fade-OUT is measured, not on the disc | | [`ui-title-build-map.md`](ui-title-build-map.md) | Which `GP_TITLE` build is which screen state | ✅ CONFIRMED for title / `PRESS Ⓐ` / main menu / `EXTRAS` against live captures; the archive is 8 screens × EN/JP, and "6/8/9 are submenus" is withdrawn | | [`ui-title-paint-order-capture.md`](ui-title-paint-order-capture.md) | The title screen's paint order, measured from the guest's draw submissions | ✅ CONFIRMED — the order in which the running game paints the title | | [`upstream-baseline.md`](upstream-baseline.md) | A stock-upstream baseline runs Stage 02 crash-free | ✅ CONFIRMED — upstream canary_experimental + only the pad | diff --git a/docs/re/METHOD.md b/docs/re/METHOD.md index 279427ae..0c542a35 100644 --- a/docs/re/METHOD.md +++ b/docs/re/METHOD.md @@ -140,3 +140,15 @@ agent's loop prompt, i.e. nowhere durable. See [`README.md`](README.md) for the Detect the **focus ring** in the gutter left of the labels instead (`tools/re-capture/menu_focus.py`, 254 vs <82 — no threshold tuning needed), and look at the PNG before believing either. +* **`screenshot` samples at 0.5 Hz — it cannot time an animation.** Measured: + ~2 s per grab (an `import` of the root plus an ImageMagick crop). A 0.4 s fade + falls entirely between two samples, which is why a 40-frame burst across a + screen change looked like an instant cut. For anything timed, record the + display instead: `ffmpeg -f x11grab -framerate 30 -video_size x -i + :98+, -t `, then read per-frame statistics off the file. Take the + geometry from `xwininfo -root -tree`, the same way `bin/screenshot` does. +* **A screen's brightness curve is not its fade quad.** The incoming screen's own + elements animate in *after* the transition quad has cleared, so mean luminance + keeps rising long after the fade is over — 1.47 s against a declared 0.97 s on + one screen. Time the fade from where the frame is *pure black*, and take the + ramp itself from the keyframes. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 38d2ffe5..e5fb1551 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -35,6 +35,11 @@ neighbourhood, not just the line. ## Screens, classes and RTTI +* "the transition between menu screens is a cut" → it is a **fade through + black**; a 0.5 Hz screenshot burst simply samples too slowly to see it. + [`screen-transitions.md`](screen-transitions.md) +* "the main menu's initial focus is fixed" → three boots of one script gave + `TUTORIAL`, `TUTORIAL`, `NEW GAME`. * "the title menus drop d-pad presses shorter than ~0.3 s" → **refuted by my own data.** The menu **wraps at both ends**; every press registered, and the "missing" step was the wrap. See [`menu-navigation-semantics.md`](menu-navigation-semantics.md). diff --git a/docs/re/captures/transitions/transition-luminance.csv b/docs/re/captures/transitions/transition-luminance.csv new file mode 100644 index 00000000..6948a0cf --- /dev/null +++ b/docs/re/captures/transitions/transition-luminance.csv @@ -0,0 +1,421 @@ +frame,seconds,mean_luminance +0,0.0000,26.045 +1,0.0333,26.054 +2,0.0667,26.051 +3,0.1000,26.054 +4,0.1333,26.049 +5,0.1667,26.054 +6,0.2000,26.052 +7,0.2333,26.053 +8,0.2667,26.046 +9,0.3000,26.053 +10,0.3333,26.049 +11,0.3667,26.052 +12,0.4000,26.047 +13,0.4333,26.050 +14,0.4667,26.048 +15,0.5000,26.049 +16,0.5333,26.047 +17,0.5667,26.049 +18,0.6000,26.048 +19,0.6333,26.048 +20,0.6667,26.045 +21,0.7000,26.048 +22,0.7333,26.049 +23,0.7667,26.048 +24,0.8000,26.046 +25,0.8333,26.048 +26,0.8667,26.048 +27,0.9000,26.049 +28,0.9333,26.047 +29,0.9667,26.048 +30,1.0000,26.048 +31,1.0333,26.048 +32,1.0667,26.047 +33,1.1000,26.048 +34,1.1333,26.047 +35,1.1667,26.047 +36,1.2000,26.046 +37,1.2333,26.046 +38,1.2667,26.046 +39,1.3000,26.047 +40,1.3333,26.046 +41,1.3667,26.047 +42,1.4000,26.047 +43,1.4333,26.048 +44,1.4667,26.047 +45,1.5000,26.049 +46,1.5333,26.050 +47,1.5667,26.052 +48,1.6000,26.051 +49,1.6333,26.052 +50,1.6667,26.052 +51,1.7000,26.052 +52,1.7333,26.052 +53,1.7667,26.055 +54,1.8000,26.056 +55,1.8333,26.056 +56,1.8667,26.055 +57,1.9000,26.058 +58,1.9333,26.059 +59,1.9667,26.059 +60,2.0000,26.057 +61,2.0333,26.062 +62,2.0667,26.022 +63,2.1000,22.032 +64,2.1333,21.744 +65,2.1667,17.909 +66,2.2000,13.996 +67,2.2333,13.966 +68,2.2667,11.113 +69,2.3000,8.072 +70,2.3333,7.862 +71,2.3667,6.270 +72,2.4000,3.375 +73,2.4333,1.771 +74,2.4667,0.020 +75,2.5000,0.020 +76,2.5333,0.020 +77,2.5667,0.020 +78,2.6000,0.020 +79,2.6333,0.020 +80,2.6667,0.020 +81,2.7000,5.985 +82,2.7333,5.991 +83,2.7667,5.990 +84,2.8000,5.991 +85,2.8333,5.986 +86,2.8667,7.432 +87,2.9000,9.300 +88,2.9333,9.283 +89,2.9667,9.484 +90,3.0000,11.294 +91,3.0333,11.263 +92,3.0667,11.606 +93,3.1000,12.608 +94,3.1333,12.807 +95,3.1667,12.790 +96,3.2000,14.517 +97,3.2333,18.497 +98,3.2667,26.935 +99,3.3000,27.008 +100,3.3333,27.010 +101,3.3667,27.039 +102,3.4000,27.036 +103,3.4333,27.083 +104,3.4667,27.239 +105,3.5000,27.294 +106,3.5333,27.293 +107,3.5667,27.294 +108,3.6000,27.289 +109,3.6333,27.291 +110,3.6667,27.290 +111,3.7000,27.291 +112,3.7333,27.289 +113,3.7667,27.292 +114,3.8000,27.291 +115,3.8333,27.292 +116,3.8667,27.291 +117,3.9000,27.294 +118,3.9333,27.291 +119,3.9667,27.293 +120,4.0000,27.290 +121,4.0333,27.292 +122,4.0667,27.291 +123,4.1000,27.292 +124,4.1333,27.290 +125,4.1667,27.293 +126,4.2000,27.293 +127,4.2333,27.293 +128,4.2667,27.292 +129,4.3000,27.297 +130,4.3333,27.296 +131,4.3667,27.297 +132,4.4000,27.296 +133,4.4333,27.299 +134,4.4667,27.300 +135,4.5000,27.302 +136,4.5333,27.301 +137,4.5667,27.304 +138,4.6000,27.304 +139,4.6333,27.304 +140,4.6667,27.302 +141,4.7000,27.306 +142,4.7333,27.308 +143,4.7667,27.309 +144,4.8000,27.309 +145,4.8333,27.312 +146,4.8667,27.311 +147,4.9000,27.311 +148,4.9333,27.310 +149,4.9667,27.317 +150,5.0000,27.319 +151,5.0333,27.321 +152,5.0667,27.322 +153,5.1000,27.327 +154,5.1333,27.327 +155,5.1667,27.328 +156,5.2000,27.327 +157,5.2333,27.333 +158,5.2667,27.334 +159,5.3000,27.335 +160,5.3333,27.334 +161,5.3667,27.344 +162,5.4000,27.347 +163,5.4333,27.350 +164,5.4667,27.351 +165,5.5000,27.359 +166,5.5333,27.359 +167,5.5667,27.360 +168,5.6000,27.358 +169,5.6333,27.365 +170,5.6667,27.366 +171,5.7000,27.369 +172,5.7333,27.367 +173,5.7667,27.383 +174,5.8000,27.386 +175,5.8333,27.387 +176,5.8667,27.386 +177,5.9000,27.399 +178,5.9333,27.400 +179,5.9667,27.402 +180,6.0000,27.400 +181,6.0333,27.413 +182,6.0667,27.414 +183,6.1000,27.415 +184,6.1333,27.413 +185,6.1667,27.432 +186,6.2000,27.434 +187,6.2333,27.438 +188,6.2667,27.436 +189,6.3000,27.449 +190,6.3333,27.448 +191,6.3667,27.451 +192,6.4000,27.449 +193,6.4333,27.466 +194,6.4667,27.471 +195,6.5000,27.472 +196,6.5333,27.470 +197,6.5667,27.480 +198,6.6000,27.478 +199,6.6333,27.480 +200,6.6667,27.476 +201,6.7000,27.509 +202,6.7333,27.510 +203,6.7667,27.516 +204,6.8000,27.514 +205,6.8333,27.519 +206,6.8667,27.519 +207,6.9000,27.518 +208,6.9333,27.516 +209,6.9667,27.538 +210,7.0000,27.537 +211,7.0333,27.539 +212,7.0667,27.538 +213,7.1000,27.558 +214,7.1333,27.556 +215,7.1667,27.558 +216,7.2000,27.554 +217,7.2333,27.577 +218,7.2667,27.576 +219,7.3000,27.577 +220,7.3333,27.572 +221,7.3667,27.596 +222,7.4000,27.598 +223,7.4333,27.603 +224,7.4667,27.597 +225,7.5000,27.610 +226,7.5333,27.611 +227,7.5667,27.613 +228,7.6000,27.607 +229,7.6333,27.621 +230,7.6667,27.621 +231,7.7000,27.622 +232,7.7333,27.617 +233,7.7667,27.624 +234,7.8000,27.626 +235,7.8333,27.630 +236,7.8667,27.622 +237,7.9000,27.640 +238,7.9333,27.637 +239,7.9667,27.639 +240,8.0000,27.633 +241,8.0333,27.642 +242,8.0667,27.639 +243,8.1000,27.642 +244,8.1333,27.637 +245,8.1667,27.658 +246,8.2000,27.653 +247,8.2333,27.655 +248,8.2667,27.649 +249,8.3000,27.645 +250,8.3333,27.648 +251,8.3667,26.704 +252,8.4000,23.887 +253,8.4333,23.723 +254,8.4667,16.471 +255,8.5000,13.119 +256,8.5333,13.102 +257,8.5667,12.118 +258,8.6000,10.955 +259,8.6333,10.956 +260,8.6667,5.365 +261,8.7000,3.422 +262,8.7333,3.416 +263,8.7667,2.820 +264,8.8000,0.020 +265,8.8333,0.020 +266,8.8667,0.020 +267,8.9000,0.020 +268,8.9333,0.020 +269,8.9667,3.267 +270,9.0000,5.012 +271,9.0333,5.449 +272,9.0667,7.339 +273,9.1000,9.208 +274,9.1333,10.415 +275,9.1667,10.418 +276,9.2000,10.462 +277,9.2333,10.459 +278,9.2667,10.569 +279,9.3000,10.701 +280,9.3333,10.868 +281,9.3667,12.924 +282,9.4000,14.688 +283,9.4333,14.756 +284,9.4667,17.184 +285,9.5000,19.441 +286,9.5333,19.438 +287,9.5667,19.492 +288,9.6000,19.492 +289,9.6333,19.492 +290,9.6667,19.508 +291,9.7000,23.405 +292,9.7333,23.417 +293,9.7667,23.414 +294,9.8000,23.416 +295,9.8333,23.409 +296,9.8667,23.467 +297,9.9000,24.927 +298,9.9333,24.929 +299,9.9667,24.926 +300,10.0000,25.788 +301,10.0333,25.785 +302,10.0667,25.789 +303,10.1000,25.893 +304,10.1333,25.908 +305,10.1667,25.905 +306,10.2000,25.905 +307,10.2333,25.976 +308,10.2667,25.986 +309,10.3000,25.980 +310,10.3333,25.981 +311,10.3667,25.976 +312,10.4000,25.984 +313,10.4333,26.020 +314,10.4667,26.022 +315,10.5000,26.016 +316,10.5333,26.021 +317,10.5667,26.018 +318,10.6000,26.020 +319,10.6333,26.016 +320,10.6667,26.021 +321,10.7000,26.021 +322,10.7333,26.021 +323,10.7667,26.019 +324,10.8000,26.022 +325,10.8333,26.022 +326,10.8667,26.022 +327,10.9000,26.020 +328,10.9333,26.025 +329,10.9667,26.025 +330,11.0000,26.025 +331,11.0333,26.023 +332,11.0667,26.029 +333,11.1000,26.027 +334,11.1333,26.028 +335,11.1667,26.024 +336,11.2000,26.024 +337,11.2333,26.024 +338,11.2667,26.024 +339,11.3000,26.029 +340,11.3333,26.029 +341,11.3667,26.027 +342,11.4000,26.029 +343,11.4333,26.024 +344,11.4667,26.027 +345,11.5000,26.027 +346,11.5333,26.027 +347,11.5667,26.025 +348,11.6000,26.031 +349,11.6333,26.030 +350,11.6667,26.033 +351,11.7000,26.028 +352,11.7333,26.032 +353,11.7667,26.032 +354,11.8000,26.032 +355,11.8333,26.029 +356,11.8667,26.034 +357,11.9000,26.039 +358,11.9333,26.041 +359,11.9667,26.041 +360,12.0000,26.040 +361,12.0333,26.046 +362,12.0667,26.047 +363,12.1000,26.046 +364,12.1333,26.050 +365,12.1667,26.051 +366,12.2000,26.051 +367,12.2333,26.051 +368,12.2667,26.051 +369,12.3000,26.051 +370,12.3333,26.051 +371,12.3667,26.051 +372,12.4000,26.050 +373,12.4333,26.055 +374,12.4667,26.056 +375,12.5000,26.056 +376,12.5333,26.055 +377,12.5667,26.056 +378,12.6000,26.056 +379,12.6333,26.056 +380,12.6667,26.055 +381,12.7000,26.056 +382,12.7333,26.056 +383,12.7667,26.056 +384,12.8000,26.055 +385,12.8333,26.055 +386,12.8667,26.056 +387,12.9000,26.056 +388,12.9333,26.056 +389,12.9667,26.056 +390,13.0000,26.055 +391,13.0333,26.071 +392,13.0667,26.072 +393,13.1000,26.074 +394,13.1333,26.082 +395,13.1667,26.083 +396,13.2000,26.083 +397,13.2333,26.083 +398,13.2667,26.083 +399,13.3000,26.110 +400,13.3333,26.111 +401,13.3667,26.114 +402,13.4000,26.113 +403,13.4333,26.118 +404,13.4667,26.119 +405,13.5000,26.121 +406,13.5333,26.119 +407,13.5667,26.140 +408,13.6000,26.140 +409,13.6333,26.144 +410,13.6667,26.142 +411,13.7000,26.155 +412,13.7333,26.157 +413,13.7667,26.158 +414,13.8000,26.157 +415,13.8333,26.165 +416,13.8667,26.165 +417,13.9000,26.168 +418,13.9333,26.165 +419,13.9667,26.165 diff --git a/docs/re/screen-transitions.md b/docs/re/screen-transitions.md new file mode 100644 index 00000000..b4428b2e --- /dev/null +++ b/docs/re/screen-transitions.md @@ -0,0 +1,117 @@ +# What happens between two screens — a fade through black, and where its timing lives + +**Status:** ✅ `CONFIRMED`. The quad and its ramp are **decoded** (a keyframe +group on the disc, with a disc-wide check); the wall-clock timings are +**measured** off the running game at 30 fps. One piece is **undecodable from this +field** and is called out below. + +Answers [MISSION Q7](../port/MISSION.md). + +## The mechanism — decoded + +Every title-side screen carries a full-screen untextured primitive that is +**black, and paints last**: `pteff00.prm` in `GP_TITLE`, `pfeff00.prm` in +`GP_SAVE_LOAD`. That it sorts last was already established +([`structures/ui-paint-order-key.md`](structures/ui-paint-order-key.md)); what is +new here is that its **keyframe group is the transition**. + +The group is always four blocks, and always this shape: + +| block | alpha | meaning | +|---|---|---| +| 1 | `0xff` at `t = T0` | the screen starts **black** | +| 2 | `0x00` at `t = T1` | ramp to fully clear — the screen **fades in** | +| 3 | `0x00` at `t = T2` | clear; this is the resting pose, the quad is invisible | +| 4 | `0xff`, **no time** | ramp back to black — the screen **fades out** on exit | + +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)). + +``` +$ 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 +``` + +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). + +### ❔ The fade-OUT duration is not in this field + +The fourth block has **no time** — a group's last block stops 4 bytes short and +that word is already the next group's element index +([`ui_layout.rs`](../../crates/sylpheed-formats/src/ui_layout.rs) documents the +packing). So the disc gives the ramp's *target* (black) and not its length. That +duration is **measured** below, and the port is authoring it. + +### The disc-wide check, and what it shows about overlays + +Over every `GP_*.pak`, counting bundles that have a `.prm` element **and** ≥8 +elements (screen-sized rather than a two-element fragment): + +> **40 of 97** carry a 255 → 0 → 255 quad; 56 of the 60 found disc-wide have +> exactly 4 keyframes. + +41 % sounds weak until it is read per pak, where it is nearly all-or-nothing: + +| pak | with quad / screen-sized | +|---|---| +| `GP_MISSION_SELECT`, `GP_MOVIE_THEATER`, `GP_OPTIONS`, `GP_TUTORIAL`, `GP_SYSTEM` | 2/2 each | +| `GP_BUNK`, `GP_CHALLENGE` | 6/6 | +| `GP_STAGE_CLEAR` | 4/4 | +| `GP_SAVE_LOAD` | 10/12 | +| **`GP_TITLE`** | **6/12** | +| `GP_DIALOG` | 0/133 | +| `GP_DEBRIEFING_PILOTLOG` | 4/102 | + +**`GP_TITLE`'s 6 of 12 is the interesting row, and it is not a gap.** The six +that carry the quad are exactly the six *screen* builds — title, main menu and +`EXTRAS`, English and Japanese. The six that do not are exactly the six +**overlays**: the `PRESS Ⓐ BUTTON` plate and the two `DELTASABER` plates +([`ui-title-build-map.md`](ui-title-build-map.md)). An overlay composited onto a +screen has no transition of its own, so it has no fade quad — which is +independent corroboration that those builds are overlays rather than screens. +`GP_DIALOG`'s 0/133 says the same thing about dialog boxes. + +## The timing — measured + +Recorded with `ffmpeg -f x11grab -framerate 30` over the game surface, mean +frame luminance per frame; raw data in +[`captures/transitions/transition-luminance.csv`](captures/transitions/transition-luminance.csv), +filmstrip in +[`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png). + +| | main menu → `EXTRAS` (Ⓐ) | `EXTRAS` → main menu (Ⓑ) | +|---|---|---| +| press → first visible change | 0.07 s | 0.37 s | +| **fade-out to black** | **0.367 s** | **0.400 s** | +| **pure black** (luminance 0.02) | 0.233 s | 0.167 s | +| luminance rise until settled | 0.567 s | 1.467 s | + +The fade-out is the number the file cannot give, and it comes out the same both +ways: **~0.4 s**, i.e. ~24 units under Q1's rule. + +The black hold is *consistent with* the file's 12 units (0.20 s) but does not +confirm it — the plateau spans the tail of the outgoing screen's fade-out and the +head of the incoming screen's black, and this measurement cannot separate them. + +### ⚠️ The luminance rise is **not** the quad's ramp + +The two columns differ by 2.6× where the quad's declared ramps differ by only +1.12× (58 units vs 52). The filmstrip says why: the background reappears *first* +and the labels arrive after it, so what the luminance curve is timing is the +incoming screen's **own element animations**, not the fade quad. Quoting 1.47 s +as "the main menu's fade" would be wrong. The quad's ramp is decoded; the +screen's build-in is a separate, longer thing. + +## For the port + +* a screen change is: **fade the outgoing screen to black over ~0.4 s**, hold + black briefly, then **fade the incoming screen in over its own declared ramp** + while its elements play their own keyframes; +* the fade-in ramp is **read from the file** (`T1 − T0` on the screen's fade + quad); +* the ~0.4 s fade-out and the black hold are **authored from this page** — the + disc does not carry them. diff --git a/tools/re-capture/fade_quads.py b/tools/re-capture/fade_quads.py new file mode 100755 index 00000000..bd642539 --- /dev/null +++ b/tools/re-capture/fade_quads.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Dump every `.prm` primitive's keyframe group (alpha + time) for a UI build. + +The screen-transition fade lives here -- see docs/re/screen-transitions.md. +Usage: PAK= fade_quads.py [build...] (default: GP_TITLE, builds 2 4 5 6)""" +import struct, sys, glob, os, zlib +sys.path.insert(0, "/work/Syplheed-Reborn/tools/re-capture") +src = open("/work/Syplheed-Reborn/tools/re-capture/regn_decode.py").read() +exec(src.split("# ── POF0")[0]) + +DECL_AT, DECL_ENTRY, KF = 0x20, 60, 40 +def be32(b,o): return struct.unpack_from(">I", b, o)[0] + +def parse(bundle): + n = be32(bundle, 0x14) + names=[] + for i in range(n): + o = DECL_AT + i*DECL_ENTRY + names.append(bundle[o:o+28].split(b"\0")[0].decode("ascii","replace")) + groups={} + pos = DECL_AT + n*DECL_ENTRY + for _ in range(n): + if pos+8 > len(bundle): break + idx = be32(bundle,pos); frames = be32(bundle,pos+4) + if idx>=n or frames==0 or frames>4096: break + first = pos+12; end = first + frames*KF - 4 + g=[] + for k in range(frames): + blk = first + k*KF + 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))) + groups[idx]=g; pos=end + return names, groups + +pak = os.environ.get("PAK", "/work/sylph_extract/dat/GP_TITLE.pak") +E = pak_entries(pak) +E = [b for h,b in E] +# build index -> pak entry index, from `screen list`: 0..9 then 12, 15 +BUILDS = {0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,10:12,11:15} +want = [int(a) for a in sys.argv[1:]] or [2,4,5,6] +for b in want: + names, groups = parse(E[BUILDS[b]]) + print(f"=== build {b} ===") + for i,nm in enumerate(names): + if not nm.endswith(".prm"): continue + g = groups.get(i, []) + print(f" [{i}] {nm} {len(g)} kf") + for k in g: + a=(k['fade']>>24)&0xff + print(f" t={str(k['t']):>5} fade=0x{k['fade']:08x} (alpha {a:3d}) scale={k['sx']}x{k['sy']} pos=({k['x']},{k['y']})")