# 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 | `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. 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.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. ## โœ… 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**.