re(ui): answer four of the port's five asks -- splash, fade-out, focus, gamma

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.
This commit is contained in:
Sylpheed RE agent
2026-08-29 08:19:23 +00:00
parent 9a0ca0d71f
commit 9ca1eb50fd
7 changed files with 326 additions and 0 deletions

View File

@@ -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 7884 %). 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 ~060), 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

View File

@@ -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