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.
5.3 KiB
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_GAMEPADstate 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.