Two more emulator runs close two of the three reach limits on the blend page. THE TITLE (entries 4 + 2 composited). Everything alpha-over except the two rotated sweep strips and -- the finding -- ptbtn00f, the PRESS (A) plate's focused variant, which is ADDITIVE while its own base ptbtn00 is not. That is what the documented plate pulse is made of, and a port drawing both alpha-over cannot reach the pulse's peak by any pacing. And ptlogo_back2 / ptlogo_back2eff are ALPHA-OVER. They are frame-shaped, large, dark and 94 %/87 % transparent -- every surface property the menu's ptframe* have -- so 'frame-shaped and mostly transparent implies additive' is refuted on the one screen that could test it. THE MENU replicates draw for draw in two further sessions; the one-session caveat is retired. THE VERTEX CAP was 8 = two quads, so a batched draw reported its first two elements and dropped the rest. Raised to 64. This is why ptframe4, pteff21, pteff22 and pteff23 looked like elements the game never draws: EXTRAS' 24-index additive draw holds six quads and the log printed two. THE SWEEPS are on screen on the main menu in every captured frame, stepping ~0.03 NDC per frame in opposite directions with their vertex alpha ramping, at different phases in two sessions. The leaf group runs on the menu. What that does not say is how much they contribute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
11 KiB
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 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 ·
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.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 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. 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 ·
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.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.