input-pad-read-path.md says of the word the C_PAD_DECODER reads: "There is no shift and no remap on the way in -- the bit positions are XINPUT's own." That is wrong. sub_8220D500 rebuilds the word out of XINPUT_GAMEPAD before anything else sees it, into the game's own numbering. bits 0-3 A B X Y bits 4-7 left stick UP DOWN LEFT RIGHT (+/-20000 of 32767) bits 8-11 right stick UP DOWN LEFT RIGHT bits 12-15 D-pad UP DOWN LEFT RIGHT bits 16-17 START, BACK bits 18-19 LB, RB bits 20-21 LT, RT -- digital, threshold >220 of 255 bits 22-23 L3, R3 Extracted mechanically from the image, no row typed by hand. The control is the shape of the result: the 24 assignments land on bits 0..23, each used exactly once, none repeated. A misdecode does not produce a bijection over a contiguous range, and coincidence does not put the stick and D-pad directions in the same order in two aligned nibbles. So every mask in that page's tables names the wrong button. The 0xE000 x18 site, read there as "B | X | Y", is "D-pad DOWN | LEFT | RIGHT" -- eighteen sites testing a menu cursor, which is what 18 sites should be. And its headline negative is REFUTED: "LB and RB are not menu inputs" is false. They are bound at config fields this+0x70 and this+0x84, LT/RT at +0x74/+0x80. The negative was searched for 0x0100/0x0200 -- LB and RB in XINPUT's numbering -- in a word where they live at 0x40000/0x80000. Right function, right buttons, wrong bit positions, so it could only come back empty. A negative is only as good as the numbering it was searched in. Also decodes the ring record: +12 HELD, +16 PRESSED, +20 RELEASED, +28/+32 raw trigger bytes. Edge and level are one struct four bytes apart, which displaces that page's guess that press-vs-hold was split between GetState and the XamInputGetKeystrokeEx queue. The superset claim in that page survives and is untouched: sub_82457038 really does compare every XINPUT_GAMEPAD field, and it really is XINPUT-layout. This page depends on it. Not decoded: which output bit means which ACTION, and per-screen sets. 5 of 18 output-bit sites did not resolve to a pad guard, so the output map is a lower bound -- in particular "START is not tested" is NOT claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t
9.7 KiB
The pad read path — what the game asks the console for
🔴 CORRECTION 2026-09-01 — the bit tables below are MISLABELLED
The word this page calls "XINPUT's own bit positions, no shift and no remap" is remapped.
sub_8220D500rebuilds it out ofXINPUT_GAMEPADbefore the decoder sees it, into the game's own numbering:0x1=A,0x10=left-stick UP,0x1000=D-pad UP,0x40000=LB. Every mask in the two tables below names the wrong button, and the negative "LB and RB are not menu inputs" is false — they are bound at config fieldsthis+0x70andthis+0x84.✅ What survives: §"What the game reads: the whole of
XINPUT_GAMEPAD". That was read offsub_82457038, which really is XINPUT-layout.Corrected in
input-button-numbering-is-remapped.md. The sections below are kept for the addresses, which are right; only the labels are wrong.
Status: 🔴 partly REFUTED — see the correction above. ✅ 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.