# 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_8220D500` rebuilds it out of `XINPUT_GAMEPAD` before 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 fields `this+0x70` and `this+0x84`. > > โœ… **What survives:** ยง"What the game reads: the whole of `XINPUT_GAMEPAD`". > That was read off `sub_82457038`, which really is XINPUT-layout. > > Corrected in > [`input-button-numbering-is-remapped.md`](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`](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_GAMEPAD` state 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`](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*.