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

8.7 KiB
Raw Blame History

🔴 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 +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 — 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 0x10000x8000 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:

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 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.