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
216 lines
9.7 KiB
Markdown
216 lines
9.7 KiB
Markdown
# 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*.
|