# ๐Ÿ”ด 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`](input-pad-read-path.md), which says of the word the `C_PAD_DECODER` reads: > *"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."* **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`](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`](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`](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+0x24C` is 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 `EXTRAS` acts 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.