Files
Sylpheed/docs/re/input-pad-read-path.md
sylph-decoder 4518b5c76a re: the pad word is REMAPPED -- my own bit table was mislabelled throughout
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
2026-09-01 18:33:42 +00:00

216 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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*.