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
This commit is contained in:
sylph-decoder
2026-09-01 18:33:42 +00:00
parent 0eb5637329
commit 1e388d0176
7 changed files with 377 additions and 1 deletions

View File

@@ -143,6 +143,7 @@ files, which is how the same ground got covered twice.
| [`h3-units-per-frame-measured.md`](h3-units-per-frame-measured.md) | How many keyframe units elapse per guest frame, and which declared time the title's settle anchor is | ✅ **measured against a pre-registration, 2026-09-01. 2 units per guest frame**, on `ptbtn00`'s own declared ramp (`t=214→236`, T=22): three consecutive gap-free steps of **exactly 23** = 255×2/22, ramp span 10 labels against a predicted 11 (±1). The Port's inferred **5 is excluded by >2×**. 🔴 Why 5 appeared: **an alpha step is not a clock rate**`Δα/frame = 255·(units/frame)/T`, so splash B's 34-with-T=15 and the plate's 23-with-T=22 are ONE clock. Also: the **title settle anchor is t≈160**, not t=118 — it is `ptcopyright`, the last build-in element and the only glyph one, full at label 5350 → t≈168176. 🔴 **units/SECOND is NOT settled** and two of my own captures disagree ~2.9× on it; the gap is in frames→seconds, not units→frames. |
| [`h3-units-per-frame-preregistration.md`](h3-units-per-frame-preregistration.md) | The prediction, committed before the capture was read | ✅ kept as the control on the row above |
| [`input-pad-read-path.md`](input-pad-read-path.md) | What the game asks the console for, on the pad | ✅ **decoded from the image, 2026-09-01.** `sub_82457038` is the only function that reads controller *data*, and it compares **every** field of `XINPUT_GAMEPAD``wButtons` (full 16 bits), both triggers, all four stick axes — against the previous state. **14/14** loads verified byte-for-byte against `/image/sylpheed.pe`, database used as an index only. **Two input paths**: the polled state and an 8-byte-record `XamInputGetKeystrokeEx` ring. ⚠️ This is the **superset the game can see, not the per-screen set** — reading a field is not acting on it. ❔ Which bits each screen tests is open; footholds are the image's own `C_PAD_DECODER` / `C_PAD_RINGBUF` trace strings (`sub_8220B610` / `sub_821A6470`). |
| [`input-button-numbering-is-remapped.md`](input-button-numbering-is-remapped.md) | The bit numbering the game's pad decoder actually uses | ✅ **DECODED ⟨image⟩**, and 🔴 **refutes `input-pad-read-path.md`'s central claim** that there is *"no shift and no remap — the bit positions are XINPUT's own"*. `sub_8220D500` rebuilds the word out of `XINPUT_GAMEPAD` into the game's numbering: bits **03 A B X Y**, **47 left stick** (±20000), **811 right stick**, **1215 D-pad**, **1617 START/BACK**, **1819 LB/RB**, **2021 LT/RT** (digital >220), **2223 L3/R3**. ✅ **Control**: the 24 assignments land on bits 0…23, each used exactly once — a misdecode does not yield a clean bijection. 🔴 **The old page's negative "LB and RB are not menu inputs" is FALSE** — bound at config fields `+0x70`/`+0x84`; it was searched at XINPUT's bit positions in a word that does not use them. ✅ Also decodes the ring record: `+12` HELD, `+16` PRESSED, `+20` RELEASED, `+28`/`+32` raw trigger bytes — so **edge and level are one struct** and the keystroke queue is not needed to tell a press from a hold. 📌 The left stick is digitised to 4 direction bits at 61 % deflection, which is why the game cannot move a cursor at a speed (play-test finding 2). ⚠️ Not decoded: which output bit means which ACTION, and per-screen sets; 5 of 18 output sites unresolved, so the map is a lower bound |
| [`ui-splash-draw-pass.md`](ui-splash-draw-pass.md) | The splash draw pass — is there a post-process, and where does the fade come from | ✅ **decoded from GPU state, 2026-09-01.** **No post-process pass exists**: over all 1 048 draws of both splashes, one render target (`rt0=[tile=0…]` 1 048/1 048), one pitch, no MSAA, only kColorDepth/kCopy modes, resolve destinations only the two front buffers, and **no texture base anywhere equals a resolve destination**. The only texture bound is the sprite page. Three trivial pixel shaders; the sprite shader **premultiplies**, so `ONE/ONE_MINUS_SRC_ALPHA` is algebraically **source-over**. Parameters come from **per-vertex `k_8_8_8_8` colour** in a per-frame vertex buffer — the shaders read **zero** float constants (`ps_c[n=0]` 1 048/1 048). Ramp reproduces the ✅ 34/frame law. ⚠️ Censusing the whole 600-frame log instead finds the attract movie's 640×360 chroma planes and reads as a half-res blur chain — the frame window is what avoids that. |
| [`splash-glow-is-a-baked-texture.md`](splash-glow-is-a-baked-texture.md) | What the splash "blur" actually is, and which quad is which sprite | ✅ **DECODED (disc), confirmed against the oracle 8/8.** Answers play-test finding 4 as a mechanism: **there is no blur pass and no filter — each logo ships a SECOND texture that IS the blur.** `palogo_<x>_eff.t32` is the same artwork **outset by exactly 10 px on every side**, concentric to ≤ 1.5 px, drawn as its own alpha-over quad. ✅ **The capture's Q0…Q7 are now NAMED**: predicting each NDC rect from the declared position + decoded sprite size matches all eight bijectively, every match ≤ 0.0061 with every runner-up ≥ 0.0272 (4.58.9× margin — the control). 🔴 **Refutes `splash-quad-timeline.txt`'s "the same rects scaled slightly larger"**: x/y scale factors differ by up to 0.28, so a port must **load the `_eff` texture, not transform the logo**. ✅ **Blend bit tested OUT of sample** on entries 10/11 (never in its 35-row fit): pre-registered `additive = false` for all eight against 0 additive draws in 1 048 — held 8/8, control still reports 9 additive on entry 6. 📌 The ⟨our-reader⟩ 🟡 on splash geometry resolves **in the reader's favour** — the prediction is ours, the target is the oracle, so agreement is evidence *about* the reader |
| [`ui-keyframe-record-layout.md`](ui-keyframe-record-layout.md) | A keyframe's time word comes **before** its pose — the placement record, decoded | ✅ CONFIRMED, **decoded**. A group is an 8-byte header then `frames` records of `{u32 time; 36-byte pose}`, so the time precedes the pose; the group's lead-in word at `header+8` is pose 0's time and **every** pose is timed. Disc-wide over 13 991 groups in 33 archives, each test with a control: lead-in prepended is non-decreasing **13 991/13 991**; a non-zero lead-in is strictly below the next time **5 058/5 058** (control 70.9 %); a multi-segment alpha ramp runs at a constant `dα/dt` **857/1 540** against **0/1 042** under the old reading. 🔴 Retires two long-standing corpus claims — *"a group's data stops 4 bytes short of its final block's time slot"* and *"the last keyframe carries no time"* — both of which were this off-by-one. Adoption is free: all 12 `GP_TITLE` builds render byte-identically, and over 217 builds only two elements pick a different `rest()` pose, both between equally invisible ones. ❔ the executable's own parser was **not** found (the 40/60 stride query is weak, not negative) |

View File

@@ -0,0 +1,52 @@
# C_PAD_DECODER: what sets each bit of the OUTPUT word at this+0x24C.
# Guards are in the RING word's numbering (see ringmap.txt), NOT XINPUT's.
# 'cfg +0xNN' = a REMAPPABLE binding written by the ctor sub_8220B610
# 'literal' = a mask hard-coded in the update sub_8220B8C0
# '(internal)'= guarded by decoder state, not by a pad bit, in this window
site out bit guard means
8220BBB0 0x000004 (internal) -
8220BE78 0x002000 cfg +0x90 R3
8220BEBC 0x000002 cfg +0x84 RB
8220BF00 0x000001 cfg +0x80 RT>220
8220BFB8 0x000800 cfg +0x74 LT>220
8220C0B0 0x000800 cfg +0x70 LB
8220C118 0x000020 (internal) -
8220C13C 0x000040 (internal) -
8220C2F0 0x000010 cfg +0xA0 B
8220C338 0x000100 cfg +0x7C X
8220C37C 0x200000 cfg +0xA4 DPAD DOWN
8220C404 0x010000 cfg +0x98 BACK
8220C458 0x040000 literal LS LEFT
8220C474 0x080000 literal LS RIGHT
8220C490 0x000200 literal LS UP
8220C4AC 0x000400 literal LS DOWN
8220C4EC 0x100000 (internal) -
8220CB48 0x000008 (internal) -
# 13 of 18 output bits resolve to a pad guard.
# every config field the ctor sets to a ring-word button set:
# +0x4C = 0x00000100 RS UP
# +0x64 = 0x00000001 A
# +0x70 = 0x00040000 LB
# +0x74 = 0x00100000 LT>220
# +0x7C = 0x00000004 X
# +0x80 = 0x00200000 RT>220
# +0x84 = 0x00080000 RB
# +0x8C = 0x00400000 L3
# +0x90 = 0x00800000 R3
# +0x94 = 0x00000001 A
# +0x98 = 0x00020000 BACK
# +0x9C = 0x00000008 Y
# +0xA0 = 0x00000002 B
# +0xA4 = 0x00002000 DPAD DOWN
# +0xAC = 0x00000014 X | LS UP
# +0xB4 = 0x0000000A B | Y
# +0xB8 = 0x0000005A B | Y | LS UP | LS LEFT
# +0xBC = 0x0000000A B | Y
# +0xC0 = 0x0000005A B | Y | LS UP | LS LEFT
# +0xC4 = 0x0000000A B | Y
# +0xC8 = 0x00000008 Y
# +0xD4 = 0x00000002 B
# +0xD8 = 0x00000078 Y | LS UP | LS DOWN | LS LEFT

View File

@@ -0,0 +1,32 @@
# C_PAD_RINGBUF output record -- the tail of sub_8220D500, read from
# /image/sylpheed.pe. r3 = the ringbuf; r9 = the XINPUT_GAMEPAD source.
# This is where a menu gets edge-vs-level, and it is one struct, not two paths.
8220D7C0 7D0A582E
8220D7C4 9103000C +12 <- cur : HELD (level)
8220D7C8 7D0A582E
8220D7CC 80E3000C
8220D7D0 7D083278 r8 = cur XOR prev : CHANGED
8220D7D4 91030010 +16 <- changed : (overwritten below)
8220D7D8 5508003E
8220D7DC 7CCA582E
8220D7E0 7D063078 r6 = changed ANDC cur : RELEASED this frame
8220D7E4 90C30014 +20 <- released : FALLING EDGE
8220D7E8 7D6A582E
8220D7EC 90E30018 +24 <- cur : HELD (second copy)
8220D7F0 7D0B5838 r11 = changed AND cur : PRESSED this frame
8220D7F4 91630010 +16 <- pressed : RISING EDGE (final value)
8220D7F8 8969002A
8220D7FC 9163001C +28 <- bLeftTrigger : RAW ANALOG byte
8220D800 8969002B
8220D804 91630020 +32 <- bRightTrigger : RAW ANALOG byte
8220D808 A169002C
8220D80C 7D6B0734
# So the ring record is:
# +12 buttons HELD (level) -- a menu that repeats on hold reads this
# +16 buttons PRESSED (rising) -- a menu that fires once per press reads this
# +20 buttons RELEASED (falling)
# +24 buttons HELD (copy)
# +28 bLeftTrigger raw 0..255, analog value preserved alongside the bit
# +32 bRightTrigger raw 0..255

View File

@@ -0,0 +1,34 @@
# C_PAD_RINGBUF button word -- the REMAP from XINPUT_GAMEPAD into the game's
# own bit numbering. Built by sub_8220D500 and stored to ringbuf+12.
# Every row read straight out of /image/sylpheed.pe (VA - 0x82000000); the
# 'raw' column is the instruction word in the image at that address.
site raw XINPUT source condition ring bit bit#
8220D55C 39000001 wButtons A 0x00000001 0
8220D578 61080002 wButtons B 0x00000002 1
8220D594 61080004 wButtons X 0x00000004 2
8220D5B0 61080008 wButtons Y 0x00000008 3
8220D5CC 65080001 wButtons START 0x00010000 16
8220D5E8 65080002 wButtons BACK 0x00020000 17
8220D604 61081000 wButtons DPAD_UP 0x00001000 12
8220D620 61082000 wButtons DPAD_DOWN 0x00002000 13
8220D63C 61084000 wButtons DPAD_LEFT 0x00004000 14
8220D658 61088000 wButtons DPAD_RIGHT 0x00008000 15
8220D674 65080004 wButtons LB (left shoulder) 0x00040000 18
8220D690 65080008 wButtons RB (right shoulder) 0x00080000 19
8220D6AC 65080040 wButtons L3 (left thumb click) 0x00400000 22
8220D6C8 65080080 wButtons R3 (right thumb click) 0x00800000 23
8220D6E0 65080010 bLeftTrigger bLeftTrigger > 0xDC (220) 0x00100000 20
8220D6F8 65080020 bRightTrigger bRightTrigger > 0xDC (220) 0x00200000 21
8220D714 61080040 sThumbLX sThumbLX < -20000 0x00000040 6
8220D728 61080080 sThumbLX sThumbLX > +20000 0x00000080 7
8220D744 61080020 sThumbLY sThumbLY < -20000 0x00000020 5
8220D758 61080010 sThumbLY sThumbLY > +20000 0x00000010 4
8220D774 61080400 sThumbRX sThumbRX < -20000 0x00000400 10
8220D788 61080800 sThumbRX sThumbRX > +20000 0x00000800 11
8220D7A4 61080200 sThumbRY sThumbRY < -20000 0x00000200 9
8220D7B8 61080100 sThumbRY sThumbRY > +20000 0x00000100 8
# 24 mappings, covering every field of XINPUT_GAMEPAD.
# ring bits used: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23
# contiguous 0..23 with none repeated: True

View File

@@ -0,0 +1,176 @@
# 🔴 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.

View File

@@ -1,6 +1,23 @@
# The pad read path — what the game asks the console for
**Status: ✅ decoded from the image** for the driver, for the field set, and for
> # 🔴 CORRECTION 2026-09-01 — the bit tables below are MISLABELLED
>
> The word this page calls "XINPUT's own bit positions, no shift and no remap"
> is **remapped**. `sub_8220D500` rebuilds it out of `XINPUT_GAMEPAD` before the
> decoder sees it, into the game's own numbering: `0x1`=A, `0x10`=left-stick UP,
> `0x1000`=D-pad UP, `0x40000`=LB. **Every mask in the two tables below names the
> wrong button**, and the negative *"LB and RB are not menu inputs"* is **false** —
> they are bound at config fields `this+0x70` and `this+0x84`.
>
> ✅ **What survives:** §"What the game reads: the whole of `XINPUT_GAMEPAD`".
> That was read off `sub_82457038`, which really is XINPUT-layout.
>
> Corrected in
> [`input-button-numbering-is-remapped.md`](input-button-numbering-is-remapped.md).
> The sections below are kept for the addresses, which are right; only the
> **labels** are wrong.
**Status: 🔴 partly REFUTED — see the correction above.** ✅ decoded from the image for the driver, for the field set, and for
the complete button set `C_PAD_DECODER` tests; ❔ **not decoded** for which of
those each individual screen acts on, or for the decoder's own output bit
numbering. Instrument: ⟨image⟩ — the executable's own