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
8.7 KiB
🔴 The pad word is REMAPPED — input-pad-read-path.md's bit table is mislabelled throughout
Status: ✅ decoded from the image. Instrument: ⟨image⟩ — every row read out of
/image/sylpheed.pe at VA − 0x82000000, with the database used for nothing.
2026-09-01.
This refutes the central claim of
input-pad-read-path.md, which says of the word the
C_PAD_DECODER reads:
"It reads a 32-bit word at
+12of theC_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."
There is a remap. sub_8220D500 rebuilds the word bit by bit out of
XINPUT_GAMEPAD before anything else sees it, and the result is the game's own
numbering. Every mask in that page's tables is therefore labelled with the wrong
button, and its headline negative is false.
1 — the remap, complete
data/input-ring-word-remap.txt — 24 rows, each
the instruction word in the image at that address.
| ring bit | mask | is | from |
|---|---|---|---|
| 0–3 | 0x1 0x2 0x4 0x8 |
A, B, X, Y | wButtons 0x1000 0x2000 0x4000 0x8000 |
| 4–7 | 0x10 0x20 0x40 0x80 |
left stick UP, DOWN, LEFT, RIGHT | sThumbLY/LX vs ±20000 |
| 8–11 | 0x100 0x200 0x400 0x800 |
right stick UP, DOWN, LEFT, RIGHT | sThumbRY/RX vs ±20000 |
| 12–15 | 0x1000 … 0x8000 |
D-pad UP, DOWN, LEFT, RIGHT | wButtons 0x1 0x2 0x4 0x8 |
| 16–17 | 0x10000 0x20000 |
START, BACK | wButtons 0x10 0x20 |
| 18–19 | 0x40000 0x80000 |
LB, RB | wButtons 0x100 0x200 |
| 20–21 | 0x100000 0x200000 |
LT, RT — digital | bLeft/RightTrigger > 220 |
| 22–23 | 0x400000 0x800000 |
L3, R3 | wButtons 0x40 0x80 |
The control is the shape of the result. Extracted mechanically — no row typed by hand — the 24 assignments land on bits 0…23, each used exactly once, none repeated. A misdecode does not produce a perfect bijection over a contiguous range, and a coincidence does not put the four stick directions and the four D-pad directions in the same order in two aligned nibbles.
Both analog axes and both triggers are digitised here, at ±20000 of 32767 (61 %) for the sticks and >220 of 255 (86 %) for the triggers. The raw analog values are not discarded — see §3.
2 — what that does to the old table
The old page read masks against XINPUT's numbering. Under the real numbering:
| mask | old page says | actually is |
|---|---|---|
0xF000 ×1, "any face button" |
A | B | X | Y | the whole D-pad |
0xE000 ×18 |
B | X | Y | D-pad DOWN | LEFT | RIGHT |
0x000F ×1 |
the whole D-pad | A | B | X | Y |
0x0003 ×1 |
Up | Down | A | B |
0x0030 ×1 |
START | BACK | left stick UP | DOWN |
0x0060 ×1 |
BACK | left thumb | left stick DOWN | LEFT |
0x00FF ×5 |
D-pad + START + BACK + thumbs | A B X Y + all four left-stick directions |
0x0001 |
D-pad Up | A |
0x0010 |
START | left stick UP |
0x0020 |
BACK | left stick DOWN |
The 0xE000 count is the tell: 18 sites testing "D-pad DOWN, LEFT or RIGHT"
is a menu cursor. Eighteen sites testing "B, X or Y" never made sense.
⚠️ A second, independent defect in that table: it counted rlwinm sites without
checking which register each masks. Several attributed to D-pad bits —
0x8220BB9C, 0x8220BEA8, 0x8220BEEC — mask the decoder's own state word at
this+52, not a pad word at all.
🔴 And its headline negative is REFUTED
"The shoulder buttons are the only pad inputs the decoder never tests … LB and RB are not menu inputs. A binding table that maps them to anything is mapping them to nothing."
False. data/input-decoder-output-map.txt:
| decoder reads | via | sets output bit |
|---|---|---|
| LB | config field this+0x70 = 0x00040000 |
0x000800 |
| RB | config field this+0x84 = 0x00080000 |
0x000002 |
| LT | this+0x74 = 0x00100000 |
0x000800 |
| RT | this+0x80 = 0x00200000 |
0x000001 |
| L3 / R3 | this+0x8C / this+0x90 |
— |
| B, X, BACK, D-pad DOWN | +0xA0, +0x7C, +0x98, +0xA4 |
0x10, 0x100, 0x010000, 0x200000 |
| left stick UP/DOWN/LEFT/RIGHT | hard-coded literals | 0x200, 0x400, 0x040000, 0x080000 |
Why the old negative was wrong is the useful part. It searched for 0x0100
and 0x0200 — LB and RB in XINPUT's numbering — and found only ori. But in
the word the decoder actually reads, LB and RB are 0x00040000 and 0x00080000.
It was looking in the right function for the right buttons at the wrong bit
positions, so it could only ever find them absent. A negative is only as good as
the numbering it was searched in.
📌 The bindings are not immediates in the update — they are fields the
constructor sub_8220B610 writes, so this layer is remappable by design and a
reader must go through the constructor to know what any output bit means.
3 — the ring record: edge and level, in one struct
data/input-ring-record-layout.txt, the tail
of sub_8220D500:
| offset | is |
|---|---|
+12 |
buttons HELD (level) |
+16 |
buttons PRESSED this frame — (cur XOR prev) AND cur |
+20 |
buttons RELEASED this frame — (cur XOR prev) ANDC cur |
+24 |
buttons HELD, second copy |
+28 / +32 |
bLeftTrigger / bRightTrigger, raw 0…255 |
📌 This displaces a guess in the old page. It said "a menu that responds to a
press is likely reading the queue; anything that responds to a hold must be
reading the polled state," offering XamInputGetKeystrokeEx as the edge source.
Edge and level are both right here, computed by the game, four bytes apart.
Nothing needs the keystroke queue to tell a press from a hold. The update reads
+12 (held) and +16 (pressed) and picks per action.
The triggers keep their analog value beside the digital bit, so a screen wanting a proportional trigger has it.
The set, as the brief asked for it
Every entry ⟨image⟩ — decoded, none measured, none guessed.
| input | reaches the decoder? | as |
|---|---|---|
| A, B, X, Y | ✅ | ring bits 0–3 |
| D-pad ×4 | ✅ | ring bits 12–15 |
| START | ✅ digitised | ring bit 16 |
| BACK | ✅ | ring bit 17, bound at +0x98 |
| LB, RB | ✅ contra the old page | ring bits 18–19, bound at +0x70 / +0x84 |
| LT, RT | ✅ | digital at >220, ring bits 20–21; raw byte at +28/+32 |
| L3, R3 | ✅ | ring bits 22–23, bound at +0x8C / +0x90 |
| left stick | ✅ | digitised at ±20000, ring bits 4–7 |
| right stick | ✅ | digitised at ±20000, ring bits 8–11 |
No field of XINPUT_GAMEPAD is dropped, and the sticks are digitised to four
directions each with a 61 % deflection threshold — which is the fact the play-test's
finding 2 ("the left stick moved the cursor far too fast") needed: the game gets
one direction bit per axis, not a velocity.
What is NOT decoded
- ❔ Which output bit means which ACTION (confirm, cancel, up, down). The word
at
this+0x24Cis the decoder's output; naming its bits needs the layer above. - ❔ Per-screen sets. This is the game-wide layer. Nothing here says which
bits the title, the menu or
EXTRASacts on. - 🟡 5 of 18 output-bit sites did not resolve to a pad guard in the window
scanned; they are guarded by decoder state. So the output map is a lower
bound on what the decoder tests, never an upper one — and in particular
"START is not tested" is NOT claimed: no config field holds
0x00010000, but five sites are unaccounted for.
Reach
⟨image⟩, so it holds for every screen — that is the point of doing it statically.
It is a fact about the shipped executable, not about a boot. Nothing here has been
confirmed against a capture, so no row may be labelled measured: the natural
next experiment is to read ringbuf+12 out of guest memory while pressing one
button at a time and check the bit that lights.
Refutation attempt, recorded per the adversarial duty
Target: input-pad-read-path.md §"Which buttons the decoder actually tests —
the complete set", and its negative "LB and RB are not menu inputs".
Result: REFUTED, both. The bit table is mislabelled because the word is
remapped; the negative is false because it was searched in XINPUT's numbering.
That page's ✅ status on the superset question (§"What the game reads: the whole
of XINPUT_GAMEPAD") is untouched and survives — it was read off
sub_82457038's field comparisons, which really are XINPUT-layout, and this page
depends on it.