Files
Sylpheed/docs/re/structures/ui-blend-mode-measured.md
sylph-decoder e61c60029a re: the UI blend mode is MEASURED -- the frames are drawn ADDITIVE
Closes the one route t32-blend-mode-not-on-disc.md left open: the executable's
draw path. Canary's UI draw capture now logs RB_BLENDCONTROL0 per draw, and the
game was driven to the main menu and to EXTRAS with F10 at each.

The title-side UI uses two blend states and ONE pixel shader:

  0x07010701  src=ONE dst=1-SRC_ALPHA  alpha-over (premultiplied)
              ptbase, pteff05, the fade quad, ptmsg, ptmsg2, pttitle, buttons
  0x01010101  src=ONE dst=ONE          ADDITIVE
              ptframe1, ptframe2, ptframe3, pteff20, both rotated sweep strips

Two controls, both run before the result was read:

* the NDC->pixel conversion that identifies a draw by its quad size reproduces
  1134 and 1303 px for the two rotated sweep strips -- numbers measured by a
  different tool in a different session -- on BOTH screens. The tool prints
  PASS/FAIL and disclaims its own output on FAIL.
* pixel shader 0xE59B2B3DA4AA9008 is used with BOTH states, 12 draws additive
  and 18 alpha-over. ptframe1 and ptbase run the same shader; only the blend
  register differs. So this is a blend result, not a shader result.

This confirms the port's independent measurement -- it solved the composite per
pixel from two backgrounds and found additive halves alpha-over's error on both
frames -- by a route with nothing in common with it.

So the blend is no longer authored: 'any blend you choose is authored' was true
of the disc and is not true of the game. What is still unknown is which field
selects it; elements sharing a mode are batched into one draw call, so the
selection happens before the draw.

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

5.7 KiB
Raw Blame History

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 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 elements that share a mode are batched together — which is also why ptframe4 needs no separate argument: it is inside EXTRAS' 24-index additive batch with ptframe3.

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.