# The UI blend mode — ✅ **MEASURED**: the frames are drawn **ADDITIVE** **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 | `1−SRC_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. 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. The title screen (build 4) was **not** captured. --- ## 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.60–0.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.