From 4518b5c76a4dabe834e9d5faa9df5a3b4b4f8a11 Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Tue, 1 Sep 2026 18:33:42 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Jc4pciRArGHfxGGhEbwp5t --- docs/port/HANDOFF.md | 64 +++++++ docs/re/INDEX.md | 1 + docs/re/data/input-decoder-output-map.txt | 52 ++++++ docs/re/data/input-ring-record-layout.txt | 32 ++++ docs/re/data/input-ring-word-remap.txt | 34 ++++ docs/re/input-button-numbering-is-remapped.md | 176 ++++++++++++++++++ docs/re/input-pad-read-path.md | 19 +- 7 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 docs/re/data/input-decoder-output-map.txt create mode 100644 docs/re/data/input-ring-record-layout.txt create mode 100644 docs/re/data/input-ring-word-remap.txt create mode 100644 docs/re/input-button-numbering-is-remapped.md diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 80a75094..f9596e10 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -499,6 +499,70 @@ constructed in `sub_8220B610` and released in `sub_821A6470`. A *decoder* betwee `wButtons` and the menus is where repeat timing, edge detection and remapping live. That is the next read. +--- +## πŸ”΄πŸ”΄ 2026-09-01 (seventh) β€” **THE PAD BIT NUMBERING IS NOT XINPUT'S.** I gave you a wrong table + +[`../re/input-button-numbering-is-remapped.md`](../re/input-button-numbering-is-remapped.md) +Β· [remap](../re/data/input-ring-word-remap.txt) +Β· [output map](../re/data/input-decoder-output-map.txt) +Β· [record layout](../re/data/input-ring-record-layout.txt) + +I told you the game reads `wButtons` with *"no shift and no remap β€” the bit +positions are XINPUT's own."* **That is wrong.** `sub_8220D500` rebuilds the word +out of `XINPUT_GAMEPAD` first, into the game's own numbering: + +| ring bits | are | not | +|---|---|---| +| 0–3 | **A B X Y** | ~~D-pad~~ | +| 4–7 | **left stick UP DOWN LEFT RIGHT** (Β±20000 of 32767) | ~~START/BACK/thumbs~~ | +| 8–11 | **right stick UP DOWN LEFT RIGHT** | β€” | +| 12–15 | **D-pad UP DOWN LEFT RIGHT** | ~~A B X Y~~ | +| 16–17 | **START, BACK** | β€” | +| 18–19 | **LB, RB** | β€” | +| 20–21 | **LT, RT** β€” digital at **> 220** of 255 | β€” | +| 22–23 | **L3, R3** | β€” | + +Extracted mechanically, and the control is the shape: bits **0…23, each used +exactly once, none repeated**. A misdecode does not produce a clean bijection. + +### πŸ”΄ And "LB and RB are not menu inputs" β€” which I told you β€” is FALSE + +They are bound at config fields `this+0x70` and `this+0x84`, and LT/RT at +`+0x74`/`+0x80`. **My negative was searched at XINPUT's bit positions in a word +that does not use them**, so it could only ever come back empty. If you dropped +LB/RB bindings on my say-so, put them back. + +### The part you can use immediately: edge and level are one struct + +| ring record | is | +|---|---| +| `+12` | buttons **HELD** (level) | +| `+16` | buttons **PRESSED** this frame (rising edge) | +| `+20` | buttons **RELEASED** this frame (falling edge) | +| `+28` / `+32` | `bLeftTrigger` / `bRightTrigger`, **raw 0…255** | + +I previously guessed that press-vs-hold was split between `XamInputGetState` and +the `XamInputGetKeystrokeEx` queue. It is not β€” the game computes both, four +bytes apart, and picks per action. **You do not need a keystroke queue.** + +πŸ“Œ **And this is the mechanism behind play-test finding 2.** The game digitises +the left stick to four direction bits at a **61 % deflection threshold**. It never +sees a velocity, so it cannot move a cursor at a speed. One bit, one step. + +### The complete input set, every row ⟨image⟩ β€” decoded, none guessed + +A B X Y Β· D-pad Γ—4 Β· START Β· BACK Β· LB Β· RB Β· LT Β· RT Β· L3 Β· R3 Β· left stick Γ—4 Β· +right stick Γ—4. **No field of `XINPUT_GAMEPAD` is dropped.** + +### What I still owe you + +* ❔ **Which output bit means which ACTION** (confirm / cancel / up / down). The + decoder's output word is `this+0x24C` in its own numbering; naming those bits + needs the layer above, and I have not read it. +* ❔ **Per-screen sets.** This is the game-wide layer. +* 🟑 5 of 18 output-bit sites did not resolve to a pad guard, so the output map is + a **lower bound**. In particular I am **not** claiming START is untested. + --- ## βœ…βœ… 2026-09-01 (sixth) β€” **THE BLUR IS A TEXTURE.** `palogo_*_eff.t32` is a baked 10-px glow diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index a9937b78..e4dbe22a 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -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β‰ˆ168–176. πŸ”΄ **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 **0–3 A B X Y**, **4–7 left stick** (Β±20000), **8–11 right stick**, **12–15 D-pad**, **16–17 START/BACK**, **18–19 LB/RB**, **20–21 LT/RT** (digital >220), **22–23 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__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.5–8.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) | diff --git a/docs/re/data/input-decoder-output-map.txt b/docs/re/data/input-decoder-output-map.txt new file mode 100644 index 00000000..1ee435b9 --- /dev/null +++ b/docs/re/data/input-decoder-output-map.txt @@ -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 diff --git a/docs/re/data/input-ring-record-layout.txt b/docs/re/data/input-ring-record-layout.txt new file mode 100644 index 00000000..5f7f8d88 --- /dev/null +++ b/docs/re/data/input-ring-record-layout.txt @@ -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 diff --git a/docs/re/data/input-ring-word-remap.txt b/docs/re/data/input-ring-word-remap.txt new file mode 100644 index 00000000..b2ecf2f3 --- /dev/null +++ b/docs/re/data/input-ring-word-remap.txt @@ -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 diff --git a/docs/re/input-button-numbering-is-remapped.md b/docs/re/input-button-numbering-is-remapped.md new file mode 100644 index 00000000..d3bf035e --- /dev/null +++ b/docs/re/input-button-numbering-is-remapped.md @@ -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 | +|---|---|---|---| +| 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. diff --git a/docs/re/input-pad-read-path.md b/docs/re/input-pad-read-path.md index 613c2ec5..d0c8a0ae 100644 --- a/docs/re/input-pad-read-path.md +++ b/docs/re/input-pad-read-path.md @@ -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