Files
Sylpheed/docs/re/input-pad-read-path.md
sylph-decoder 0565098fa2 handoff+index: H3 answered, the settle anchor, and the pad's full field set
Three rows the port is blocked on:

H3 -- 2 units per guest frame, measured on the plate's own ramp against a
pre-registration. Their 5 excluded by >2x, and the reason named: an alpha
step is not a clock rate without the element's declared T, and my own
splash-quad-timeline.txt published alpha against frame with no T column.
That file now warns at its head.

The title settle anchor is t~160, not t=118 -- it is ptcopyright, the last
build-in element and the only glyph one. Said plainly that this is the
reading under which their clock:'shared' collapses, and that a consequence
is not a counter-argument.

Told them NOT to change their 60 units/s yet: units/second is untouched by
this and two of my own captures disagree ~2.9x on frames->seconds.

The pad: sub_82457038 reads every field of XINPUT_GAMEPAD, 14/14 loads
verified against the image, plus a second keystroke-queue path. Flagged as
the superset the game can SEE, not the per-screen set, so it is used to
check a binding table and not to write one.
2026-09-01 16:50:52 +00:00

5.3 KiB
Raw Blame History

The pad read path — what the game asks the console for

Status: decoded from the image for the driver half; not yet decoded for which button bits the menus test. 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.

Not decoded — which bits each screen tests

The consumer side is open. Two named footholds exist for it, 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.