Files
Sylpheed/docs/re/input-button-numbering-is-remapped.md
sylph-decoder 4518b5c76a re: the pad word is REMAPPED -- my own bit table was mislabelled throughout
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
2026-09-01 18:33:42 +00:00

177 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔴 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 |
|---|---|---|---|
| 03 | `0x1` `0x2` `0x4` `0x8` | **A, B, X, Y** | `wButtons` `0x1000 0x2000 0x4000 0x8000` |
| 47 | `0x10` `0x20` `0x40` `0x80` | **left stick UP, DOWN, LEFT, RIGHT** | `sThumbLY/LX` vs **±20000** |
| 811 | `0x100` `0x200` `0x400` `0x800` | **right stick UP, DOWN, LEFT, RIGHT** | `sThumbRY/RX` vs **±20000** |
| 1215 | `0x1000``0x8000` | **D-pad UP, DOWN, LEFT, RIGHT** | `wButtons` `0x1 0x2 0x4 0x8` |
| 1617 | `0x10000` `0x20000` | **START, BACK** | `wButtons` `0x10 0x20` |
| 1819 | `0x40000` `0x80000` | **LB, RB** | `wButtons` `0x100 0x200` |
| 2021 | `0x100000` `0x200000` | **LT, RT** — digital | `bLeft/RightTrigger` **> 220** |
| 2223 | `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 03 |
| D-pad ×4 | ✅ | ring bits 1215 |
| START | ✅ digitised | ring bit 16 |
| BACK | ✅ | ring bit 17, bound at `+0x98` |
| LB, RB | ✅ **contra the old page** | ring bits 1819, bound at `+0x70` / `+0x84` |
| LT, RT | ✅ | digital at **>220**, ring bits 2021; raw byte at `+28`/`+32` |
| L3, R3 | ✅ | ring bits 2223, bound at `+0x8C` / `+0x90` |
| left stick | ✅ | digitised at **±20000**, ring bits 47 |
| right stick | ✅ | digitised at ±20000, ring bits 811 |
**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.