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

9.7 KiB
Raw Blame History

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. 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_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:

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.