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

16 KiB
Raw Blame History

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 0x02ui-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 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 · captures/ui-draws/blend-main-menu-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).

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, by a different tool in a different session. This conversion produces 1134.0 and 1303.2on 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 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), 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 · captures/ui-draws/blend-title-2026-08-31.log · run 2 · run 3

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, 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 · 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 drawsptbase, 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.