The Port asks whether the blend is evaluated on sRGB-encoded values or linearised and re-encoded. It is answered by one register field I already log, and the answer is the same on every draw of two full captures. RB_COLOR_INFO.color_format is k_8_8_8_8 (0) on 2402/2402 splash draws and 33779/33791 draws of the boot-through-title capture; the other 12 are k_32_FLOAT and k_16_16_FLOAT and are not colour passes. k_8_8_8_8_GAMMA (1) appears ZERO times, and color_exp_bias is 0 everywhere so nothing stands in for a gamma either. The mechanism is Canary's own source, not our inference: k_8_8_8_8_GAMMA is the ONLY colour format around which a piecewise-linear gamma<->linear conversion is applied (spirv_shader_translator.h:510 PWLGammaToLinear / LinearToPWLGamma, render_target_cache.h:720, dxbc_shader_translator_om.cc). With k_8_8_8_8 there is none, so the blender operates on the stored values as they are. So a renderer that linearises before blending and re-encodes after is doing a different operation -- and the difference is gamma-shaped and exactly zero on unblended pixels, which is the divergence signature the port reports. Also renamed my units-per-second pre-registration off the 'h4' prefix: the Port's BLOCKED.md numbers this blend-space question H4 and two different H4s in one corpus is how a citation goes wrong.
8.8 KiB
The pad read path — what the game asks the console for
Status: ✅ decoded from the image for the driver, for the field set, and for
the complete button set C_PAD_DECODER tests; ❔ not decoded for which of
those each individual screen acts on, or for the decoder's own output bit
numbering. Instrument: ⟨image⟩ — the executable's own
bytes, with the database used only as an index and every load verified against
the file. 2026-09-01.
Asked by the 2026-09-01 play-test: the port shipped a milestone with no joypad binding for Ⓐ or Ⓑ and nothing caught it. The other half of that is knowing what the game itself reads, so a binding table can be checked against a fact rather than against whoever pressed which button.
The three entry points, and there are only three
Every import the game has for controller input, and every caller, from xrefs:
| import | thunk | called from |
|---|---|---|
XamInputGetCapabilities |
0x824AA840 |
sub_82456F58 |
XamInputGetState |
0x824AA848 |
sub_82173DC8, sub_82456F58, sub_82457038 |
XamInputGetKeystrokeEx |
0x824AA870 |
sub_82457038 (×3) |
XamInputSetState |
— | no caller found (rumble is imported and unused, or reached indirectly) |
sub_82173DC8 is not a button reader: it calls XamInputGetState only to
compare the result against 1167 (ERROR_DEVICE_NOT_CONNECTED) and raise a
flag. It is the controller-disconnected watcher.
sub_82457038 is the pad poll. It is the only function that reads controller
data.
✅ What the game reads: the whole of XINPUT_GAMEPAD
sub_82457038 calls XamInputGetState with the output buffer at r31+36, which
lays XINPUT_STATE over the pad object. It then compares every field of the
new state against a 16-byte copy of the previous one at r31+52, and reports "no
change" only if all seven match:
| offset (new / prev) | load | field |
|---|---|---|
+36 / +52 |
lwz |
dwPacketNumber |
+40 / +56 |
lhz |
wButtons — the full 16-bit word |
+42 / +58 |
lbz |
bLeftTrigger |
+43 / +59 |
lbz |
bRightTrigger |
+44 / +60 |
lhz |
sThumbLX |
+46 / +62 |
lhz |
sThumbLY |
+48 / +64 |
lhz |
sThumbRX |
+50 / +66 |
lhz |
sThumbRY |
Verified against the image, not the database. All fourteen loads
re-encoded from their operands and compared byte-for-byte with
/image/sylpheed.pe at VA − 0x82000000:
0x82457230 image=0xA17F0038 expect=0xA17F0038 OK lhz r11,56(r31)
0x82457234 image=0xA15F0028 expect=0xA15F0028 OK lhz r10,40(r31)
…
14/14 instructions in the image agree with the database
Full listing: data/input-pad-fields.txt.
So the answer to "does the game read the triggers / the right stick / both
axes" is yes, all of them, and it is decoded rather than observed. There is no
field of XINPUT_GAMEPAD the poll ignores.
⚠️ What this does not say. Reading a field is not using it. The poll's job is
to detect any change; a screen may test only two bits of wButtons. This
establishes the superset the game can see, which is exactly what a binding
table needs to be checked against, and not the per-screen set.
The second path: a keystroke queue
The same function calls XamInputGetKeystrokeEx three times with flags = 3,
draining into a ring at r31+68 ({ptr, count, capacity}) in 8-byte records
— the size of XINPUT_KEYSTROKE. So the game runs two input paths at once:
- the polled
XINPUT_GAMEPADstate above, and - an event queue of keystrokes.
📌 This matters for the port and is already half-recorded elsewhere:
run-canary's own header notes that "360 menus poll XamInputGetKeystrokeEx,
not GetState, so a stubbed GetKeystroke looks like a completely dead pad",
and the capture corpus counts hundreds of XamInputGetKeystrokeEx calls on the
title. A menu that responds to a press is likely reading the queue; anything
that responds to a hold must be reading the polled state. Which of the two
each menu action uses is not decoded.
✅ Which buttons the decoder actually tests — the complete set
C_PAD_DECODER is the game's own name for the layer between wButtons and
the menus, from the executable's own Shift-JIS trace strings. Its constructor is
sub_8220B610 (684-byte object, memset then a block of configuration
constants) and its update is sub_8220B8C0, 1 400 instructions immediately
after it. The construction trace addi r3,r11,21840 → 0x820A5550 is
C_PAD_DECODER 初期化, verified against the image.
It reads a 32-bit word at +12 of the C_PAD_RINGBUF (this+76) and masks
its low 16 bits directly. There is no shift and no remap on the way in — the
bit positions are XINPUT's own:
0x8220C550 lwz r10, 76(r31) ; this+76 = the C_PAD_RINGBUF
0x8220C554 lwz r10, 12(r10) ; +12 = the button word
0x8220C558 rlwinm r10, r10, 0,16,19 ; mask 0xF000 = A|B|X|Y -> "any face button"
Every mask the update applies, from
data/input-decoder-masks.txt:
| bit | button | tested individually? |
|---|---|---|
0x0001 |
D-pad Up | ✅ ×21 |
0x0002 |
D-pad Down | ✅ ×2 |
0x0004 |
D-pad Left | ✅ ×3 |
0x0008 |
D-pad Right | ✅ ×1 |
0x0010 |
START | ✅ ×5 |
0x0020 |
BACK | ✅ ×4 |
0x0040 |
Left thumb click | ✅ ×2 |
0x0080 |
Right thumb click | ✅ ×2 |
0x0100 |
Left shoulder | 🔴 never |
0x0200 |
Right shoulder | 🔴 never |
0x0400 |
(unnamed) | ✅ ×1 |
0x0800 |
(unnamed) | 🔴 never |
0x1000 |
Ⓐ | ✅ ×2 |
0x2000 |
Ⓑ | ✅ ×1 |
0x4000 |
Ⓧ | ✅ ×1 |
0x8000 |
Ⓨ | ✅ ×1 |
and the group masks, which are what identify these as buttons rather than as coincidental bit constants — no other quantity in a program produces exactly these groupings:
| mask | meaning | × |
|---|---|---|
0x000F |
the whole D-pad | 1 |
0x0003 |
Up | Down | 1 |
0x0030 |
START | BACK | 1 |
0x0060 |
BACK | left thumb | 1 |
0x00FF |
D-pad + START + BACK + both thumbs | 5 |
0xE000 |
Ⓑ | Ⓧ | Ⓨ | 18 |
0xF000 |
Ⓐ | Ⓑ | Ⓧ | Ⓨ — "any face button" | 1 |
Verified 7/7 against /image/sylpheed.pe, database used only as an index.
🔴 The one negative, and it is the useful part
The shoulder buttons are the only pad inputs the decoder never tests.
0x0100 and 0x0200 appear in this function only as ori — the decoder
setting bits in its own output word (0x8220C334, 0x8220C48C), never
andi./rlwinm reading them. Same for 0x0800.
So on the evidence of this layer: LB and RB are not menu inputs. A binding table that maps them to anything is mapping them to nothing.
⚠️ Reach. This is C_PAD_DECODER, one layer, and it is the layer the menus
sit above — but gameplay code may read wButtons by another route, and this says
nothing about that. It is also not per-screen: it is the set the decoder
distinguishes, and a given screen may act on a subset.
📌 And the decoder emits its own bit assignment. The ori 0x100 / 0x200 / 0x400 / 0x800 sites build an output word whose bit positions are not
XINPUT's. Anything downstream reads that word, so a table found later in menu
code will be in the decoder's numbering and not the console's. Mapping the two is
not done and is the next read.
❔ Still not decoded — which bits each SCREEN acts on
The decoder's set is above. What remains is the layer past it. The footholds, from the image's own strings:
0x820A5550 'C_PAD_DECODER 初期化' referenced from sub_8220B610
0x820A5568 'C_PAD_DECODER 開放' referenced from sub_821A6470
0x820A55BC 'C_PAD_RINGBUF 初期化' referenced from sub_8220B610
0x820A55D4 'C_PAD_RINGBUF 開放' referenced from sub_821A6470
C_PAD_DECODER and C_PAD_RINGBUF are the game's own names for this
subsystem — construction and release traces, Shift-JIS, in one constructor
(sub_8220B610) and one destructor (sub_821A6470). A decoder between the raw
wButtons and the menus is where a game normally puts its repeat timing, its
edge detection and its button remap, and it is the next place to read.
Next experiment: disassemble sub_8220B610 for the pad object's layout, then
find where wButtons at +40 is consumed and what masks are tested against it.
Report per-screen only if the code is per-screen; otherwise report the game-wide
set and say so.
Reach
⟨image⟩, so it is a fact about the shipped executable and holds for every screen — that is the point of doing it statically. It says nothing about which of these inputs any particular screen acts on, and nothing has been measured in a capture yet, so no row here may be labelled measured.