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
177 lines
8.7 KiB
Markdown
177 lines
8.7 KiB
Markdown
# 🔴 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.
|