Files
Sylpheed/docs/re/structures/ui-blend-mode-measured.md
sylph-decoder d9cdfff0a8 handoff: the blend is a bit on the disc, and their three asks
Delivered: T8aD +0x04 bit 0x02 selects additive, so the port derives the blend
instead of transcribing my table -- including for screens neither of us has
captured. With why I got it wrong first: the bit was already in REFUTED.md,
killed by a comparison of two renders made while our renderer had a stale
keyframe association, no leaf geometry and no rotation.

Their asks: pteff10 IS additive on the main menu, three sessions, every frame,
and my coverage table listing it uncovered there is corrected. The sweep ramp is
four leaf keyframes on the disc, with the oracle agreeing on sign and magnitude
and a second identification from the sub-128 alpha. And kind & 0x2 is not mine
to grant -- stated what the decode supports (0 violations in 15493 entries, and
it means the declaration marks the element focusable, not that the cursor can
reach it) and left the adoption to them.

ui-blend-mode-measured.md is superseded in its classification and keeps its
measurements, which are what the decode is fitted to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
2026-08-31 07:20:50 +00:00

294 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The UI blend mode — ✅ **MEASURED**: the frames are drawn **ADDITIVE**
> 🔴 **SUPERSEDED IN ITS CLASSIFICATION, 2026-08-31.** This page says the mode is
> **measured** and that *"which field selects the mode is still unknown"*. The
> field is now decoded — **`T8aD +0x04` bit `0x02`** —
> [`ui-blend-mode-decoded.md`](ui-blend-mode-decoded.md). Every measurement below
> stands and is the evidence that decode is fitted to; the instruction to read it
> as per-element facts because no rule exists does not.
**Status:**`CONFIRMED`, classification **measured** — nothing on the disc
selects it, and this is what the running game tells the GPU, per draw, on two
screens. 2026-08-31.
Closes the one route
[`t32-blend-mode-not-on-disc.md`](t32-blend-mode-not-on-disc.md) left open: *"the
executable's draw path, which is where a mode selected in code rather than data
would live. That is a route, and it is the one left."*
## The answer
The title-side UI uses **two blend states, and one pixel shader**:
| `RB_BLENDCONTROL0` | src | op | dst | what it is | drawn this way |
|---|---|---|---|---|---|
| `0x07010701` | `ONE` | ADD | `1SRC_ALPHA` | **alpha-over, premultiplied** | `ptbase`, `pteff05`, the fade quad, `ptmsg`, `ptmsg2`, `pttitle`, every button |
| `0x01010101` | `ONE` | ADD | `ONE` | **ADDITIVE** | **`ptframe1`, `ptframe2`, `ptframe3`**, `pteff20`, both rotated sweep strips |
| `0x00010001` | `ONE` | ADD | `ZERO` | opaque, blending off | the one non-UI blit that opens the frame |
Reference data and both raw logs:
[`data/ui-blend-mode-measured.txt`](../data/ui-blend-mode-measured.txt) ·
[`captures/ui-draws/blend-main-menu-2026-08-31.log`](../captures/ui-draws/blend-main-menu-2026-08-31.log) ·
[`captures/ui-draws/blend-extras-2026-08-31.log`](../captures/ui-draws/blend-extras-2026-08-31.log)
## How it was measured
Canary's `CaptureUiDrawForRE` already dumped every UI draw in submission order
with its shaders and bound texture. It was extended to log `RB_BLENDCONTROL0`,
`RB_COLORCONTROL` and `RB_COLOR_MASK` as well — **raw and decoded**, so a decode
bug here cannot quietly become the answer. One emulator, driven to the main menu
and then into `EXTRAS`, F10 at each.
The log names no elements, so a draw is identified by the **pixel size of its
quad**: NDC extents × the 1280×720 surface, matched against sprite dimensions
read straight off the disc ([`tools/re-capture/ui_blend_map.py`](../../../tools/re-capture/ui_blend_map.py)).
### The controls, both run before the result was read
**1. The size conversion reproduces two independently measured numbers.** The
title's two rotated sweep strips were measured at **1134** and **1303** px tall
in [`data/title-sweep-drawn-at-rest.txt`](../data/title-sweep-drawn-at-rest.txt),
by a different tool in a different session. This conversion produces `1134.0` and
`1303.2`**on both screens**. The tool prints `PASS`/`FAIL` and says outright
that its sizes are worthless if the check fails.
**2. It is the blend register, not a different shader.** Pixel shader
`0xE59B2B3DA4AA9008` is used with **both** states on the main menu — 12 draws
additive, 18 alpha-over. `ptframe1` and `ptbase` run the *same shader*; only the
blend differs. Without this the result could have been "the frames use a
different shader", which is a different finding.
## The identification, from the artefact
| draw | quad px | disc sprite | blend |
|---|---|---|---|
| menu 7 | 243.2 × 280.8 | `ptframe1.t32` **247×281** | **ADDITIVE** |
| menu 7 | 256.0 × 309.6 | `ptframe2.t32` **257×311** | **ADDITIVE** |
| menu 8 | 224.0 × 39.6 | `ptmsg.t32` **223×38** | alpha-over |
| menu 9 | 211.2 × 54.0 | `ptbtn01f.t32` **216×56** | alpha-over |
| extras 7 | 243.2 × 219.6 | `ptframe3.t32` **246×220** | **ADDITIVE** |
| extras 7 | 614.4 × 518.4 | `pteff20.t32` **309×259** @2× | **ADDITIVE** |
| extras 8 | 352.0 × 39.6 | `ptmsg2.t32` **354×38** | alpha-over |
| extras 8 | 179.2 × 32.4 | `pttitle.t32` **182×34** | alpha-over |
`ptframe1` and `ptframe2` are in **one draw call**; so are `pteff20` and
`ptframe3`. A draw call carries one blend state, so `ptframe4` needs no separate
argument: it is inside `EXTRAS`' 24-index additive draw with `ptframe3`.
🔴 **The first version of this paragraph said "elements that share a mode are
batched together". That is false**, and it is refuted by the very log it was
written from: in one main-menu frame, draws **5, 6 and 7 are three separate
additive draws** — consecutive, identical state, not merged. Only the one-way
implication holds: **elements inside one draw share a blend state; sharing a
state does not put elements in one draw.** The wrong version would have licensed
inferring a mode for an element that was never observed, which is exactly what
this page must not do.
## What this settles, and what it does not
**The port's independent measurement is confirmed by the oracle.** It solved
the composite per pixel from two backgrounds and found additive halves
alpha-over's error on both frames (34.305/28.948 against 65.046/71.299). Two
methods with nothing in common — a solved composite against a capture, and the
register the GPU was handed — give the same answer.
**This is no longer authored.** The [not-on-disc page](t32-blend-mode-not-on-disc.md)
concluded *"any blend you choose is authored and must carry that label"*. That was
true of the **disc**; it is not true any more of the **game**. Additive is
measured, and the port can transcribe it.
⚠️ **`src = ONE`, not `SRC_ALPHA`, in BOTH states.** The fixed-function blend
multiplies the pixel shader's output by 1, so whatever alpha weighting happens,
the *shader* does it. This says nothing about whether the `.t32` texels are
stored premultiplied — that is a separate question, and the shader is unread.
**Where the mode is selected is still unknown.** This measures *what* the GPU
was told, not *which field* decided it. The disc-side search found nothing
([reach here](t32-blend-mode-not-on-disc.md)), and the batching means the
selection happens before the draw, in whatever sorts the elements. So the port
should read this table as a per-element fact it can transcribe, **not** as a rule
it can extend to elements not in it.
**Two additive draws on the menu are unidentified** — 819.2×720 and 691.2×720.
The second is within 8 px of `pteff12` @2×; the first matches nothing on the
screen at either scale. Both are additive, so they do not change the answer, and
neither is guessed at here.
⚠️ **Reach.** Two screens, one session, one emulator. The title screen (build 4)
was **not** captured.
🔴 **The sentence that stood here was wrong, and `sylpheed-port` caught it
(2026-08-31).** It read: *"Every element on the two screens the port ships is in
the table except the two above and `pteff10`, which did not appear as an
identifiable quad."* Counting an element as covered if it appears in the per-draw
log **or** in a prose row of the summary table, what is actually missing is
| screen | in neither |
|---|---|
| main menu | `pteff10` — as stated |
| **`EXTRAS`** | `pteff10`, **`ptframe4`, `pteff21`, `pteff22`, `pteff23`** |
**Five, not one**, and on `EXTRAS` the four extra ones are precisely the elements
the port measures as the worst on that screen. A reader of the old sentence would
have concluded the coverage was complete but for one unidentifiable quad.
**The cause is found and fixed** — Canary's vertex dump was capped at 8
vertices, two quads, so `EXTRAS`' 24-index additive draw reported two of its six
elements and the other four looked like elements the game never draws. See the
2026-08-31 section below. The result rows were never wrong; the claim about what
they covered was.
⚠️ **And "every button" in the table above is a class generalisation**, in the
page whose own instruction is to read it as per-element facts. It came from
`ptbtn01f` on the main menu. Run 3 below raises it to five buttons plus the focus
ring, individually, **on the main menu**; no `EXTRAS` button has been measured at
all.
---
## 2026-08-31 (later) — the TITLE, and the reach closed on two of its three limits
The section above ended: *"two screens, one session ... The title screen (build 4)
was **not** captured."* Two more emulator runs close both.
[`data/ui-blend-title-and-replication.txt`](../data/ui-blend-title-and-replication.txt) ·
[`captures/ui-draws/blend-title-2026-08-31.log`](../captures/ui-draws/blend-title-2026-08-31.log) ·
[run 2](../captures/ui-draws/blend-main-menu-run2-2026-08-31.log) ·
[run 3](../captures/ui-draws/blend-main-menu-run3-2026-08-31.log)
### The title — 33 draws, 4 frames. The live title is entries **4 + 2** composited.
| element | quad px | disc | blend |
|---|---|---|---|
| `ptbase2` | 1280 × 720 @2× | 640×360 | alpha-over |
| the two rotated sweep strips | 883×1134, 1299×1303 | rotated AABBs | **ADDITIVE** |
| `ptlogo_back2eff` | 1132.8 × 280.8 | 1133×280 | **alpha-over** |
| `ptlogo_back2` | 1120.0 × 262.8 | 1118×262 | **alpha-over** |
| `ptlogo1` | 915.2 × 115.2 | 919×113 | alpha-over |
| `ptlogo2` | 992.0 × 104.4 | 992×104 | alpha-over |
| `ptlogo_tm` | 38.4 × 18.0 | 37×17 | alpha-over |
| `ptcopyright` | 691.2 × 18.0 | 694×20 | alpha-over |
| `ptbtn00` — the `PRESS Ⓐ` plate | 512.0 × 50.4 | 513×50 | alpha-over |
| **`ptbtn00f`** — its focused variant | 537.6 × 75.6 | 537×76 | **ADDITIVE** |
📌 **Two things here matter more than the rest.**
**`ptbtn00f` is additive and `ptbtn00` is not.** The plate's highlight variant is
composited additively over its own base — which is what the documented **pulse**
is made of, and it means a port that draws both alpha-over cannot reproduce the
pulse's peak no matter how it paces the ramp.
🔴 **`ptlogo_back2` and `ptlogo_back2eff` are ALPHA-OVER.** They are the title's
frame-shaped elements, they are large, dark and 94 %/87 % transparent — every
surface property the menu's `ptframe*` have — and the game does **not** draw them
additive. So *"frame-shaped and mostly transparent ⇒ additive"* is refuted on the
one screen where it could be tested, and the per-element table remains the only
safe reading.
### The main menu, replicated in **two more sessions**
Runs 2 and 3, separate boots hours apart, reproduce the menu's blend assignment
**draw for draw**: same states, same quad sizes, same identifications. The
"one session" caveat is retired.
Run 3 also carries a Canary fix worth its own line: the vertex dump was capped at
**8 vertices = two quads**, so a batched draw reported its first two elements and
silently dropped the rest. Raised to 64. The menu's 24-index button draw now
resolves as **six** quads — the focus ring plus all five buttons, all alpha-over —
where before it showed two.
⚠️ **The cap was not a display bug.** It is why `ptframe4`, `pteff21`, `pteff22`
and `pteff23` were reported as appearing in **no** captured draw on `EXTRAS`: that
screen's 24-index additive draw holds six quads and the log printed two.
## ✅ The sweep strips are ON SCREEN on the main menu, and they move
`sylpheed-port` flagged a real conflation in the section above: a quad's size
identifies an element and says nothing about whether it is visible, and the blend
and the visibility arrived in the same artefact. Their `authored/rendering.json`
scopes the leaf loop to the **title** only.
The draw log does retain NDC positions, so this is answerable from the artefact
already committed ([`tools/re-capture/sweep_positions.py`](../../../tools/re-capture/sweep_positions.py),
[`data/sweep-strips-on-the-menu.txt`](../data/sweep-strips-on-the-menu.txt)):
| session | frame | strip A x-range | strip B x-range | vertex alpha |
|---|---|---|---|---|
| 1 | 0 | `0.69 … 2.08` | `0.61 … 1.42` | `EF` / `9A` |
| 1 | 4 | `0.72 … 2.11` | `0.64 … 1.39` | `F0` / `9C` |
| 1 | 5 | `0.76 … 2.15` | `0.68 … 1.35` | `F2` / `9D` |
| 2 | — | `0.24 … 1.62` | `1.67 … 0.36` | `D7` / `C7` |
NDC spans `[1, +1]`, so **both strips overlap the screen in every captured
frame**, they **step ~0.03 NDC (~19 px) per frame in opposite directions**, and
their per-vertex alpha ramps with them. Two sessions catch them at different
phases, so they free-run. **The leaf group runs on the main menu.**
⚠️ **What this does not say.** That they *contribute* much. They are additive with
vertex alpha 0.600.95 over a texture that is overwhelmingly low-alpha, and this
measures submission and geometry, not the light they add. A port whose render
looks worse with them visible has either a placement, a phase or a magnitude
problem — but "the game does not draw them here" is not available as an
explanation any more.
## ✅ 2026-08-31 (final) — `EXTRAS` is COMPLETE, and the four missing elements are ADDITIVE
The four elements the port measured as the worst on `EXTRAS`, and which appeared
in no draw, were in a draw all along. `EXTRAS`' 24-index additive batch holds
**six** quads; Canary's vertex dump printed the first **two**.
[`data/ui-blend-extras-complete.txt`](../data/ui-blend-extras-complete.txt) ·
[`captures/ui-draws/blend-extras-run2-2026-08-31.log`](../captures/ui-draws/blend-extras-run2-2026-08-31.log)
| draw | quad px | element | blend |
|---|---|---|---|
| 7 | 614.4 × 518.4 | `pteff20` | **ADDITIVE** |
| 7 | 243.2 × 219.6 | `ptframe3` | **ADDITIVE** |
| 7 | 256.0 × 212.4 | **`ptframe4`** | **ADDITIVE** |
| 7 | 403.2 × 3.6 | **`pteff21`** | **ADDITIVE** |
| 7 | 422.4 × 7.2 | **`pteff22`** | **ADDITIVE** |
| 7 | 435.2 × 7.2 | **`pteff23`** | **ADDITIVE** |
| 6 | 819.2 × 720 | **`pteff10`** | **ADDITIVE** |
| 8 | 352.0 × 39.6 | `ptmsg2` | alpha-over |
| 8 | 179.2 × 32.4 | `pttitle` | alpha-over |
| 8 | 268.8 × 57.6 | `ptbtn11f` | alpha-over |
| 8 | 249.6 × 43.2 | `ptbtn12` | alpha-over |
| 8 | 108.8 × 43.2 | `ptbtn13` | alpha-over |
**All six are in one draw with `ptframe3`, whose state was already measured**, so
this is the one-way implication doing real work: same draw ⇒ same state.
### `pteff10` is identified, and it needs the resting SCALE
`pteff10` ships as **409 × 144** and is drawn at **200 % × 500 % = 816 × 720**.
The matcher's old "try 1× and 2×" rule could not name it at any scale, and
reported a near miss against something else instead — a failure wearing the
clothes of an answer. Candidates are now the declaration's `pivot × 2` scaled by
the **resting keyframe**, as well as the texture at 1× and 2×, and the tolerance
is the log's own **NDC print quantisation** (two decimals → 6.4 px in x, 3.6 px
in y) rather than a chosen number.
🟡 **And `pteff10` being additive is a live tension worth stating.** The port
measures it as **nearly exact** rendered alpha-over. Both can be true — it is a
dim, wholly semi-transparent glow (max alpha 130) over a dark background, where
additive and alpha-over nearly coincide — but it is the one row of this table
that a rendering check does *not* independently corroborate, and it should be
adopted knowing that.
### What is still not identified, on any screen
* **the two rotated sweep strips** — matched by their AABB, not by a declared
size, because they are rotated nested leaves. The control pins them at 1134 and
1303 px on every screen and the port has an independent 884 px footprint for
the width, so they are identified; the matcher simply cannot do it by size.
* **the focus ring** — drawn at 64.0 × 61.2 against `ptbtneff01`/`02`'s 42 × 46.
It spins and scales, so its AABB is not its sprite size. Alpha-over on both
screens.
* **the three full-screen 1280 × 720 alpha-over draws** — `ptbase`, `pteff05`,
`pteff02.prm` and `pteff00.prm` all declare 1280 × 720, so size cannot separate
them. Two of the three carry a bound texture and one does not, which narrows it
and does not close it. All four candidates are alpha-over, so nothing turns on
it — but the label the tool prints on those rows is **a candidate, not an
identification**.