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 3ad8e7228f
commit 4518b5c76a
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) |