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
This commit is contained in:
105
docs/re/structures/ui-blend-mode-measured.md
Normal file
105
docs/re/structures/ui-blend-mode-measured.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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 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](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.
|
||||
Reference in New Issue
Block a user