From 0fd8e6953e4a7b8a3b8ea8b7965c48f4468b4baa Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sat, 29 Aug 2026 08:19:23 +0000 Subject: [PATCH] re(ui): answer four of the port's five asks -- splash, fade-out, focus, gamma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1 SPLASH ADDRESSING (was blocking P3). No content predicate exists: design size fails (every extra composable bundle sampled is 1280x720, like every screen) and element count fails (fragments run 2..15, the splash halves are 3 and 7). But GP_TITLE needs none -- `--all` adds exactly four bundles there and all four are real screens, with the --all index equal to the pak entry index 1:1. And there are TWO splash screens: 11/14 are the developer logos, 10/13 are the SQUARE ENIX publisher wordmark, which the port did not have and which the boot shows first. 2 FADE-OUT (was blocking P3). It is (a), and it is bigger than the fade quad. Every element ends on exactly ONE untimed keyframe, which rules out (b); that block is where the screen plays out -- quad to a=255, buttons/labels/glows to a=0, frames hold. (c) is refuted by a null test that discriminates: a black quad alone holds the button/background brightness ratio constant, and through the fade it falls 6.50 -> 1.94, 3.4x monotonic. 3 FOCUS (saves P5 rework). Over-vs-instead is unobservable -- the focused sprite covers the base at 100.0% of base-visible pixels on three pairs once aligned (true offset (7,7); the centre alignment reads a misleading 78-84%), and compositing both ways differs by RMSE 1.1 inside the button rect. The real defect is the focus record's SECOND element: ptbtn0Nf.rat declares ptbtneff01.t32 (a 42x46 glowing ring, focus only) plus the bright label, where the base declares one sprite. That ring is the marker the port draws nowhere. 5 GAMMA. The capture is not neutral: capture ~ 255*(render/255)^g, g ~ 1.34-1.49, and the chain says it is a ramp the GAME installed, not a capture artefact. So RMSE against captures has a floor. Reach stated: the flat patches are all dark (render ~0-60), so midtones and highlights are unconstrained. 4 ROTATION is a human's call and is recorded in MISSION, not acted on -- the port rotating while the reference renderer does not would make verify-screen report a large diff meaning "the port is right". The RE half is answered: rotation is about the declared pivot, measured against a GPU capture. The focus record's +20 element-count word is marked ๐ŸŸก not โœ… -- read on GP_TITLE's ten button records only; the disc-wide check is written and still running. --- docs/port/HANDOFF.md | 70 ++++++++++++ docs/port/MISSION.md | 22 ++++ docs/re/screen-transitions.md | 53 +++++++++ docs/re/structures/ui-button-focus-record.md | 111 +++++++++++++++++++ docs/re/ui-splash-addressing.md | 70 ++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 docs/re/structures/ui-button-focus-record.md create mode 100644 docs/re/ui-splash-addressing.md diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index e8988292..b69d0aef 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -41,6 +41,76 @@ authored version can be deleted. ## Already settled โ€” the port can rely on these today +### โฌ… Answers to the port's five asks (2026-08-29) + +* **1 โ€” how to recognise the splash.** โ” **No content rule exists; you are + authoring this.** Design size fails (every extra composable bundle sampled is + 1280ร—720, same as every screen) and element count fails (the fragments run + 2โ€ฆ15 elements, the splash halves have 3 and 7 โ€” the ranges overlap). + โœ… **But `GP_TITLE` needs no rule.** There, `--all` adds exactly **four** + bundles and all four are real screens โ€” no fragments at all โ€” and the `--all` + index equals the pak **entry** index 1:1 across all 16, so addressing by entry + index does not mean something different from elsewhere. + ๐Ÿ”ด **And there are TWO splash screens; you have one.** Entries **11/14** are the + developer logos (GAME ARTS / SETA / studio anima). Entries **10/13** are the + **SQUARE ENIX publisher** wordmark โ€” the *first* thing the boot shows โ€” and you + do not have them. The pairs are region twins (`โ„ข` on 10, `ยฎ` on 13). All four + draw every element they declare. + [`ui-splash-addressing.md`](../re/ui-splash-addressing.md) ยท + [render grid](../re/captures/title-builds/splash-both-halves-rendered.png) + +* **2 โ€” the ~0.4 s fade-out is (a)**, and it is bigger than the fade quad. + Every element of a screen ends on **exactly one** untimed keyframe โ€” so there + is one unknown duration per screen, which rules out (b). That final block is + where the screen plays out: the quad goes `a=255` (black) while the buttons, + `ptmsg` and the glows go `a=0` and the two frames hold. (c) is refuted by a + null test on the capture: a black quad alone keeps the buttonรทbackground + brightness **ratio constant**, and measured through the fade it falls + **6.50 โ†’ 1.94, a 3.4ร— monotonic drop**. So: write one authored constant + (~0.4 s / ~24 units) and **play the group to its end on every element** โ€” do + not fade a black rectangle over a frozen screen. + [`screen-transitions.md`](../re/screen-transitions.md) + +* **3 โ€” focus: your choice is fine, and it is not your bug.** โœ… The focused + sprite **completely covers** the base โ€” `f` alpha โ‰ฅ base alpha at **100.0 %** of + base-visible pixels on three pairs across both languages, once aligned properly + (the true offset is **(7,7)**, and at the centre alignment it reads a + misleading 78โ€“84 %). Compositing both ways differs by **RMSE 1.1 inside the + button rectangle**, max 12/255 on ~25 px โ€” unmeasurable at frame level. + ๐Ÿ”ด **What you are actually missing is the focus record's SECOND element.** + `ptbtn0Nf.rat` declares **two** sprites โ€” `ptbtneff01.t32` (a 42ร—46 **glowing + ring**, focus only) then `ptbtn0Nf.t32` (the bright label) โ€” where the base + record declares one. That ring is the marker you say you draw nowhere. โš ๏ธ Note + the small dot-in-circle at each underline's left end is *not* it: that is on + every button all the time, part of the base art. + [`structures/ui-button-focus-record.md`](../re/structures/ui-button-focus-record.md) + +* **4 โ€” rotation: not mine to decide alone.** Raised with the human; see + MISSION. What I can say without a decision: the two sub-questions are not + equally open. Rotation is about the **declared pivot** โ€” that anchor is + *measured*, not assumed: the title's two `ptloop` sweeps scale 600 %/800 % + vertically, where the pivot term is worth 450 and 630 px, and the GPU capture + puts both quad centres at y **359.1**/**360.0** against the pivot formula's + **360.0**; top-left anchoring predicts 810/990 and centre-as-position 270. So + if you draw rotation, rotate about the declared pivot. + โš ๏ธ It changes nothing on your five screens **at rest** โ€” they have zero + top-level rotations, and the title's two nested ones sit entirely off-screen at + rest. [`structures/ui-keyframe-rotation.md`](../re/structures/ui-keyframe-rotation.md) + +* **5 โ€” the capture is not gamma-neutral, and RMSE against it has a floor.** + Measured on flat patches (16ร—16, both images `std < 8`): + `capture โ‰ˆ 255ยท(render/255)^ฮณ` with **ฮณ โ‰ˆ 1.49** (main menu), **1.49** + (`EXTRAS`), **1.34** (title). The chain says this is a ramp **the game + installed**, not a capture-path artefact: canary's swap-path gamma stage is a + pure 256-entry LUT that defaults to identity, and the game is measured calling + `VdGetCurrentDisplayGamma` once at video init. โš ๏ธ **Reach: the flat patches are + almost all dark (render ~0โ€“60), so nothing here constrains midtones or + highlights** โ€” which is where ฮณ 1.4 does its visible work. So: yes, there is a + floor; a ฮณ โ‰ˆ 1.4 darkening gets closer and is **authored**, best applied where + it was measured rather than extrapolated. Do not chase RMSE below it. + [`structures/ui-render-tone-curve.md`](../re/structures/ui-render-tone-curve.md) + + * **`GP_TITLE.pak` is eight screens, each shipped twice โ€” English and Japanese.** Build 4 is the English title art and 7 its Japanese twin; **2/3 are the `PRESS โ’ถ BUTTON` plate, a build of their own** composited over the title and diff --git a/docs/port/MISSION.md b/docs/port/MISSION.md index 82aaf612..2a9572f9 100644 --- a/docs/port/MISSION.md +++ b/docs/port/MISSION.md @@ -208,6 +208,28 @@ settled and only multi-keyframe absolute timing is open; `rest()` differs from its alternative on **one** element across all five screens, and the current answer there is the defensible one. +## ๐Ÿ”ต Needs a human decision โ€” rotation (raised 2026-08-29) + +The port agent asks whether it should **render** `rotation_deg` (decoded at +keyframe `+12`) when `sylpheed-cli screen render` deliberately does not. Its own +framing is the reason this is not mine to settle: if the port rotates and the +reference renderer does not, then `verify-screen` reports a large title diff that +means *"the port is right"* โ€” a silently inverted signal. + +The RE half is answered and is in HANDOFF: rotation is about the **declared +pivot** (measured against a GPU capture, not assumed), and it changes nothing on +the five screens **at rest**. + +What needs a decision is which way the divergence gets closed: + +* teach `ui_layout::blit` a rotating path, so the two renderers stay comparable + and the diff keeps meaning "someone is wrong" โ€” costs work in the reference + renderer, which is otherwise not on the port's critical path; or +* let the port render rotation and mark the title as a known-divergent screen in + `verify-screen`, accepting a check that no longer guards the title. + +Recorded rather than chosen, per "do not improvise around a blocker". + ## Known unknowns โ€” say so, do not fill them in Some of these may turn out to be undecodable. That is a valid, useful answer, and diff --git a/docs/re/screen-transitions.md b/docs/re/screen-transitions.md index b4428b2e..5a2fe3ff 100644 --- a/docs/re/screen-transitions.md +++ b/docs/re/screen-transitions.md @@ -115,3 +115,56 @@ screen's build-in is a separate, longer thing. quad); * the ~0.4 s fade-out and the black hold are **authored from this page** โ€” the disc does not carry them. + +## โœ… Which quantity the ~0.4 s is โ€” measured 2026-08-29 + +Asked by the port: is 0.4 s **(a)** the ramp from the hold to the exit pose, i.e. +exactly the missing duration of that untimed keyframe, **(b)** several keyframes' +worth, or **(c)** something the game does independently of the group? + +**It is (a)** โ€” and it is bigger than the fade quad. Two facts. + +**1. There is exactly one untimed keyframe, and every element has it.** Reading +`screen info --build 5 --geometry` for the main menu, all 16 elements end on a +single timeless block; none has two. So there is one unknown duration per screen, +not a chain of them โ€” which rules out (b) outright. And that final block is not +idle: it is where the screen *plays out*. + +| elements | final untimed block | what it does | +|---|---|---| +| `pteff00.prm` (the fade quad) | `a = 255` | goes **black** | +| `pteff10`, `pteff12`, `ptbtn01โ€ฆ05`, `ptmsg` | `a = 0` | **fade out** | +| `ptframe1`, `ptframe2` | `a = 255` | hold, and get covered | +| `ptbase`, `pteff05`, `ptloop*`, `pteff02.prm` | single keyframe | hold | + +**2. The capture shows the content fading, not just a black quad arriving.** This +has a null hypothesis that discriminates: under (c) โ€” the game blackens the frame +independently โ€” every region is scaled by the same `1 โˆ’ ฮฑ`, so the **ratio** +between a button region and a background region is *constant* through the +fade-out. Under (a) it must fall, because the buttons ramp to `a = 0` while the +background elements hold at 255 and are only dimmed. + +Measured on [`transition-filmstrip.png`](captures/transitions/transition-filmstrip.png), +button column รท upper-right background art, frame by frame through the fade-out: + +``` +frame 0 1 2 3 4 5 +ratio 6.495 5.574 3.105 2.125 1.935 (black) +``` + +**A 3.4ร— monotonic fall.** Constant is refuted. The buttons really are fading +independently of the overall dim, exactly as their declared final block says. +(The incoming screen runs it in reverse, 2.22 โ†’ 3.47 over frames 7โ€“12.) + +โš ๏ธ **Reach.** The filmstrip is downsampled and the "button" region unavoidably +contains some background, so the ratio is a direction, not a clean alpha +measurement. It refutes the constant-ratio null decisively; it does not by itself +pin the 0.4 s to ยฑ0.05 s. And it is measured on **one** transition pair. + +### For the port + +Write **one** authored constant โ€” the duration of the final untimed keyframe, +~0.4 s / ~24 units โ€” and **play the group to its end on every element**. Do not +model the exit as a black rectangle fading over a frozen screen: the buttons and +labels ramp to transparent at the same time, and that difference is visible. + diff --git a/docs/re/structures/ui-button-focus-record.md b/docs/re/structures/ui-button-focus-record.md new file mode 100644 index 00000000..10652f63 --- /dev/null +++ b/docs/re/structures/ui-button-focus-record.md @@ -0,0 +1,111 @@ +# โœ… A focused button is a **two-element record**, and the second element is the ring + +**Status:** ๐ŸŸก `DECODED for GP_TITLE`, disc-wide check **not yet run**; the +sprite contents and the ring are โœ… `MEASURED` against a live capture. Answers the +port's *"focus: drawn over the base element, or instead of it?"* + +โš ๏ธ The element-count field at `+20` is read here on **`GP_TITLE`'s ten button +records only** (five buttons ร— two language bundles). A disc-wide check โ€” +"does `+20` equal the number of resource names the leaf embeds, for every `.rat` +leaf on the disc?" โ€” is written and was still running when this was committed. +Until it lands, treat `+20` as a **strong local reading, not a decoded field**: +the *observable* claim below (the focus record carries a second sprite, +`ptbtneff01.t32`, and the port must draw it) rests on the embedded names and the +capture, not on that word. + +**Short answer: it does not matter, and that is not the bug.** The focused sprite +covers the base completely, so over-vs-instead is worth at most 12/255 on ~25 +pixels. What *is* missing is the focus record's **second element** โ€” a glowing +ring that only exists when focused. + +## The record + +A button's base and focused variants are both `.rat` leaves, and they are not the +same shape: + +```text +ptbtn02.rat 164 B strings: RATC, ptbtn02.t32, opt ptbtn02f.rat +ptbtn02f.rat 312 B strings: RATC, ptbtneff01.t32, ptbtn02f.t32, opt ptbtn02b.rat +``` + +The header says so explicitly. A `.rat` leaf begins: + +```text ++0 "RATC" ++16 u32 flags 0x00008110 (base) 0x00008112 (focused) ++20 u32 ELEMENT COUNT 1 2 ++32 first element name +``` + +So the focused record declares **two** elements โ€” `ptbtneff01.t32` first, then +`ptbtn0Nf.t32` โ€” where the base declares one. Both `f` records of both language +bundles read the same way, and the flag word differs only in bit `0x02` +alongside the count. + +โš ๏ธ The `opt ` link on a `.rat` leaf is **not** simply "my focused variant". The +chain runs `ptbtn01.rat โ†’ ptbtn01f.rat โ†’ ptbtn02.rat โ†’ ptbtn02f.rat โ†’ +ptbtn02b.rat`, i.e. it threads base and focused records together in order. The +corpus calls it the focus link and for a *base* record that reading works; do not +generalise it to the `f` records. + +## The three sprites + +Decoded with `sylpheed-cli pak textures`, shown over a checkerboard in +[`button-base-focus-ring-sprites.png`](../captures/ui-layout/button-base-focus-ring-sprites.png): + +| sprite | size | what it is | +|---|---|---| +| `ptbtn02.t32` | 117ร—43 | the **dim** label, its underline, and a small dot-in-circle at the underline's left end | +| `ptbtn02f.t32` | 130ร—56 | the **same label, bright and glowing** โ€” a complete replacement, 13 px larger in each axis | +| `ptbtneff01.t32` | 42ร—46 | a **glowing ring**, focus only | + +The small dot-in-circle is on **every** button all the time; the large ring is +the focus marker. They are different things and sit side by side on the focused +row โ€” visible in +[`live-main-menu-options-focused.png`](../captures/title-builds/live-main-menu-options-focused.png) +against +[`live-main-menu.png`](../captures/title-builds/live-main-menu.png). + +## Over or instead? Measured, and the answer is "unobservable" + +Aligning base and focused by normalised cross-correlation of their alpha masks โ€” +the true offset is **(7,7)**, not the (6,6) that centring predicts โ€” + +| pair | base-visible px | `f` alpha โ‰ฅ base alpha there | +|---|---|---| +| `ptbtn02` / `f` (bundle `b58a0fe6`) | 1 444 | **100.0 %** | +| `ptbtn01` / `f` (bundle `b58a0fe6`) | 1 898 | **100.0 %** | +| `ptbtn02` / `f` (bundle `a715f485`) | 3 720 | **100.0 %** | + +โš ๏ธ **At the centre alignment it reads 78โ€“84 %, and that number is an artefact.** +A 1 px shift on strokes this thin manufactures a fifth of a sprite's worth of +"the focused art is thinner here". Solve the alignment before trusting a coverage +figure. + +Coverage is not the same as hiding, though, because `f` is not fully opaque +everywhere. Compositing both ways over the menu's own background colour: + +| pair | max channel difference | px > 8/255 | RMSE over the button rect | +|---|---|---|---| +| `ptbtn02` | 12.5 | 23 | 1.09 | +| `ptbtn01` | 12.2 | 27 | 1.08 | +| `ptbtn02` (JP) | 12.2 | 27 | 1.05 | + +So the two hypotheses differ by **~1.1 RMSE inside the button rectangle**, on a +couple of dozen pixels โ€” below the โ‰ˆ ฮณ 1.4 tone gap +([tone curve](ui-render-tone-curve.md)) and far below any frame-level RMSE this +corpus can resolve. **Either choice is defensible; neither is measurable.** +Replacing is the cheaper one and is what the file's structure suggests, since +`ptbtn0Nf.t32` is a whole label rather than an overlay. + +## โ” Not established + +* **Where `ptbtneff01.t32` is placed.** The record declares two elements; the + per-element placement inside a `.rat` leaf was not decoded here. The capture + shows the ring **left of** the label, roughly on the underline's left end. + A port should take the offset from the record, and until it is decoded, from + the capture. +* **`ptbtn02b.t32`** โ€” a third variant, `b`, exists for button 02 only, same size + as the base. Not seen on any capture. Not chased. +* Whether a **non-title** archive uses the same two-element convention. Checked + on `GP_TITLE` only. diff --git a/docs/re/ui-splash-addressing.md b/docs/re/ui-splash-addressing.md new file mode 100644 index 00000000..0366613e --- /dev/null +++ b/docs/re/ui-splash-addressing.md @@ -0,0 +1,70 @@ +# โ” How to address the developer-logo splash โ€” no content rule, but `GP_TITLE` needs none + +**Status:** โ” **undecodable as a general predicate, with reach** โ€” and โœ… +**exact for `GP_TITLE`**, which is the only archive in scope. Answers the port's +*"I need a predicate, not an index."* + +## The ask + +The splash has no `.rat` layout child, so `is_build` rejects it and it is +reachable only through `--all` / `is_composable`, which disc-wide also admits +~1 786 non-screen bundles. The port wanted a rule that admits the splash and not +those. + +## โœ… In `GP_TITLE` the problem does not arise + +``` +$ sylpheed-cli screen list GP_TITLE.pak 12 builds โ€” entries 0-9, 12, 15 +$ sylpheed-cli screen list --all GP_TITLE.pak 16 bundles โ€” entries 0-15 +``` + +`--all` adds exactly **four** bundles: entries **10, 11, 13, 14**. All four are +real screens, and **not one fragment appears**. So within this archive +`is_composable` *is* the predicate โ€” it is exact, with no filtering needed. + +โš ๏ธ And the renumbering worry is moot here: in `GP_TITLE` the `--all` index equals +the pak entry index **1:1 across all 16**. That is an accident of this archive +having 16 composable bundles at entries 0โ€ฆ15, not a general property โ€” but it +means the port can address these by entry index without the index meaning +something different from elsewhere. + +## โœ… And there are TWO splash screens, not one + +Rendered with `screen render --all --build --primitives` +([grid](captures/title-builds/splash-both-halves-rendered.png)): + +| entry | elements | what it draws | +|---|---|---| +| **10** | 3 | **SQUARE ENIX** publisher wordmark, `โ„ข` | +| **11** | 7 | **GAME ARTS** / **SETA** / **studio anima** developer logos | +| **13** | 3 | SQUARE ENIX, `ยฎ` โ€” the twin of 10 | +| **14** | 7 | the twin of 11 | + +โš ๏ธ **The port has 11/14 and is missing 10/13** โ€” the publisher half, which is the +*first* thing the boot sequence shows. The two halves differ only by the +trademark glyph (`โ„ข` on 10, `ยฎ` on 13), which is the region/language split this +archive uses everywhere else. + +All four draw every element they declare (3/3 and 7/7), so nothing is silently +dropped. + +## โ” The general rule: looked for, not found + +Two candidate predicates, both dead: + +* **Design size.** Every extra composable bundle sampled is **1280ร—720** โ€” the + same as every screen. It does not separate anything. +* **Element count.** The "two-element fragment" story holds in + `GP_MISSION_SELECT` (10 extras, all 2 elements) and **fails elsewhere**: + `GP_OPTIONS`'s extras have 5 and 15 elements, `GP_SAVE_LOAD`'s have 4, 5, 6, 9 + and 13. The splash's own halves have 3 and 7. The ranges overlap, so no + threshold separates them. + +Reach: three archives sampled beyond `GP_TITLE`, chosen because they have +non-build composables. Not a disc-wide sweep. A rule may exist in a field not +looked at โ€” the `.prm` fade quad is one candidate +([transitions](screen-transitions.md) shows overlays lack it), untested here. + +**So: the port is authoring this.** Locating the splash by entry index is the +honest description, and `name_source` should say it was located by index and not +by a rule.