port: generalise the frame finding, refute my own premultiplied-alpha hypothesis

tools/port/element-residual turns last iteration's one-off suppression into a tool:
pose a screen as verify-capture does, shadow each sprite with a transparent PNG,
rank elements by residual on the pixels they actually paint. Two controls, both
mandatory -- the metric's zero on identity, and a mod that shadows nothing must
move zero pixels, or a footprint is the harness rather than the element.

It generalises. Frames rank 1 and 2 on both screens with captures: ptframe1 9.47x
and ptframe2 5.46x on main_menu, ptframe3 14.23x and ptframe4 10.46x on extras,
all four body-dominated and all four negative. The sign splits, so it is not global
darkness: ptmsg +5.02, ptmsg2 +8.83, pttitle +7.88 and every button positive.

Refuted my own next hypothesis. Premultiplied alpha decoded as straight alpha
predicts error proportional to partial-alpha fraction; the frames are 6.7-7.3 %
partial and the most darkened, while pteff10 is 100 % partial and nearly exact.

What the frames are: neither has a single fully-opaque pixel, against ptbase's
99.1 %. For a wholly semi-transparent overlay the blend equation decides the
result, and alpha-over's shortfall against a background-scaling blend is a*bg. So
the shortfall should track the BACKGROUND, not the frame's own contribution.
Measured on render, frame-suppressed render and capture, needing no placement:
ptframe1 +0.772 vs +0.244, ptframe3 +0.797 vs +0.237.

Not adopting anything. The Decoder has established no blend mode is on the disc
for .t32 and both frames are kind 0, declared identically to elements drawn almost
exactly right, so any blend I pick is authored and that is not mine to take alone.
Recorded as an extension of their negative, not a challenge: the behaviour is real
and replicated, so if nothing in the data selects it, something in the code does --
the draw path they named as untaken.

BLOCKED.md updated: the ask is answered, as a negative.
This commit is contained in:
Sylpheed port agent
2026-08-31 05:18:37 +00:00
parent 93d36f0f3b
commit 3b7345c49a
3 changed files with 243 additions and 2 deletions

View File

@@ -147,7 +147,26 @@ HANDOFF.
| ~~P1–P7 — the keyframe record layout~~ | ~~adopt the corrected pose/time pairing~~ | — | ✅ **ADOPTED 2026-08-29 by pinning `formats-pin-2026-08-29c`.** This row was wrong twice: it said the change *"cannot be taken yet"* and that it *"reaches the port only when that branch lands on `main`"*. **It arrives when the tag is pinned**, which is what MISSION §2's tagging rule exists for. ⚠️ And the knob I tested first, `SYLPHEED_KF_TIME_SHIFT`, is a **retired partial fix** that left pose 0 untimed — the real correction is the tagged crate's default, with the old reading behind `SYLPHEED_KF_TIME_LEGACY=1`. **The blast radius was far smaller than this row predicted**: under the correction *every pose is timed* (866 keyframes, 0 untimed), so `pose_at`'s synthetic-exit branch became dead code rather than wrong code and nothing needed re-deriving. Oracle: `publisher_logo` 1.00 %→**0.75 %**, `developer_logos` 0.39 %→**0.33 %**, `extras`' differing region collapsing from 736×525 to **398×295 at the sweep position**. 🔴 Open cost: `sylpheed-cli` builds from the workspace crate, so `verify-screen` compares two decoder eras until the tag reaches `main`. Revert to the path dependency then. |
| ~~P7 / naming — the four unnamed builds~~ | ~~which locale and variant is each of entries 0, 1, 12, 15?~~ | — | ✅ **answered 2026-08-29** (`docs/re/ui-title-build-map.md`): all four are the loading screen, two variants — plain (7 elements) and dressed (10) — decoded from their own `pgloading_*` element names. ⚠️ **Not adopted as names yet, for two reasons the RE agent gave and one the port found.** Theirs: the executable names exactly two, and *which* bundle takes which name is 🟡 undecided, so `LOADING`/`LOADING2` must not go in an asset path; and locale is 🟡 — the English member of a pair is the one in the first half of `GP_TITLE.p00`, 8/8 structurally but only 3/3 where a capture can check, and the three pairs that matter are the three no capture can check. Mine: **the message gives the bundles as "0/1 and 10/11", which is the `is_build` ordinal, and `authored/screen_names.json` is keyed by PAK ENTRY** — in entry space 10 and 11 are `palogo_sqex` and `palogo_gamearts`, the splashes. See the refutation section in `DECISIONS.md`. |
## New ask, 2026-08-31 — derived from HANDOFF `5a7f34d`, at port `HEAD` `6af06bd`
## ✅ ANSWERED SAME DAY, and the answer is a negative — HANDOFF `5a7f34d`, port `HEAD` `6af06bd`
**Asked: what blend mode do `ptframe1` and `ptframe2` use on the main menu?**
🔴 **There is none on the disc.** The Decoder read all 15 words of the 60-byte
`.t32` declaration: three are the name, eight are constant across every element,
the rest are kind, focus index, position and pivot. **Both frames are kind 0 —
identical to `ptbase`, `pteff05`, `pteff10`, `pteff12` and `ptmsg`.** Nothing
distinguishes them. They refuted their own single candidate (T8aD `+0x08` = 0x8050)
before sending: 38 sprites carry it disc-wide, only 8 named frame, and its high
byte tracks the archive — an atlas word.
[`docs/re/structures/t32-blend-mode-not-on-disc.md`]
📌 **So any blend the port picks is AUTHORED**, and my refusal to brighten these is
now evidenced rather than principled. ⚠️ Their stated reach: the negative covers
the *data*. **The executable's draw path is the route they have not taken**, and a
mode selected in code rather than data would live there — see the measurement in
`DECISIONS.md` arguing something must be there.
**(original ask below, kept because the measurement it carries is still current)**
**What blend mode do `ptframe1` and `ptframe2` use on the main menu?**

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
327 sections. Search this before re-deriving anything.
328 sections. Search this before re-deriving anything.
* [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28)
* [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28)
@@ -338,6 +338,7 @@ dies, which is what this file is for.
* [`GP_DIALOG` 2/3 restored to `authored/flow.json` — on a measurement this time](#gp_dialog-23-restored-to-authoredflowjson--on-a-measurement-this-time)
* [The residual map: no local displacement either, and the split I expected is not there](#the-residual-map-no-local-displacement-either-and-the-split-i-expected-is-not-there)
* [Suppression beats coordinates: the menu residual is two frame elements, drawn too dark](#suppression-beats-coordinates-the-menu-residual-is-two-frame-elements-drawn-too-dark)
* [The frames generalise, premultiplied alpha is refuted, and the shortfall tracks the background](#the-frames-generalise-premultiplied-alpha-is-refuted-and-the-shortfall-tracks-the-background)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -15935,3 +15936,80 @@ at 100.0 % of base-visible pixels."*
confirms: not "100 % of sampled base-visible pixels are covered" but "replacing
the base with opaque magenta changes the frame in zero pixels, exactly". I went
looking for a bug and independently re-derived a documented measurement.
## The frames generalise, premultiplied alpha is refuted, and the shortfall tracks the background
`tools/port/element-residual` generalises last iteration's suppression method: pose
a screen as `verify-capture` does, shadow each sprite with a transparent PNG, and
rank elements by residual on the pixels they actually paint. Two controls, both
mandatory — the metric's zero on identity, and **a mod that shadows nothing must
move zero pixels**, or a footprint is the harness rather than the element.
### It generalises: four frames, two screens, ranks 1 and 2 on both
| screen | element | foot % | \|resid\| | ×frame | edge | flat | signed |
|---|---|---|---|---|---|---|---|
| `main_menu` | **`ptframe1`** | 0.45 | 22.72 | **9.47×** | 19.85 | 25.41 | **−22.72** |
| `main_menu` | **`ptframe2`** | 0.50 | 13.09 | 5.46× | 9.82 | 16.54 | −12.31 |
| `extras` | **`ptframe3`** | 0.40 | 34.80 | **14.23×** | 32.41 | 41.93 | **−34.80** |
| `extras` | **`ptframe4`** | 0.40 | 25.58 | 10.46× | 23.29 | 33.55 | −25.36 |
📌 **And the sign splits, so this is not the port being globally dark.** Frames are
negative; `ptmsg` **+5.02**, `ptmsg2` **+8.83**, `pttitle` **+7.88** and every
button **+1.45…+4.07** are *too bright*. A global tone error cannot do that.
### 🔴 Premultiplied alpha: my hypothesis, refuted by its own prediction
The obvious exporter-side cause is a premultiplied-alpha texture decoded as
straight alpha, which darkens exactly where alpha is partial. **It predicts error
∝ partial-alpha fraction. The opposite holds:**
| element | % partial alpha | signed |
|---|---|---|
| `ptframe1` | **7.3 %** | **−22.72** |
| `ptframe3` | **6.7 %** | **−34.80** |
| `pteff10` | **100 %** | −1.33 |
| `ptbase` | 0.9 % | +1.04 |
✅ **Dead.** The most-darkened elements have the *least* partial alpha, and the one
element that is entirely partial is almost exactly right.
### What the frames actually are, and where the shortfall goes
**Neither frame has a single fully-opaque pixel** — 0 % at alpha ≥ 99 %, against
`ptbase`'s 99.1 %. They are wholly semi-transparent overlays, the one class where
the compositing equation decides the result.
That gives a falsifiable prediction. Under alpha-over, the shortfall against a
background-scaling blend is `a · background`, so it scales with **what is behind
the frame** — whereas a too-dark texture would scale with **the frame's own
contribution**. Using only the render, the frame-suppressed render and the capture,
so no placement or coordinate convention is assumed:
| | r(shortfall, **background**) | r(shortfall, frame's contribution) |
|---|---|---|
| `ptframe1` (`main_menu`) | **+0.772** | +0.244 |
| `ptframe3` (`extras`) | **+0.797** | +0.237 |
✅ **Replicated on two elements on two screens.** The missing light scales with the
background, which is what an additive or screen blend predicts and what a bad
texture does not. Implied `a` medians 0.316 and 0.532.
### ⚠️ What I am NOT doing about it
The Decoder has established there is **no blend mode on the disc** for `.t32` — both
frames are kind 0, declared identically to elements the port draws almost exactly
right (`ptbase` ×1.31, `pteff05` ×0.92). **So any blend I choose is authored**, and
adopting one on my own authority is precisely what the mission forbids. I am
proposing it, not taking it.
📌 **The measurement does say something their negative does not cover, and I want
it on the record as an extension rather than a challenge:** the behaviour exists
and is large and replicated, and if nothing in the *data* selects it, then it is
selected in **code** — the executable's draw path, which they named as the route
they have not taken. Their negative and this measurement are consistent; together
they locate the remaining question rather than closing it.
⚠️ Not settled: whether it is additive, screen, or something else — `+0.77` and
`+0.80` say "scales with the background", not which curve. Two screens; I have not
checked the title.