# 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`](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*.