handoff+index: H3 answered, the settle anchor, and the pad's full field set
Three rows the port is blocked on: H3 -- 2 units per guest frame, measured on the plate's own ramp against a pre-registration. Their 5 excluded by >2x, and the reason named: an alpha step is not a clock rate without the element's declared T, and my own splash-quad-timeline.txt published alpha against frame with no T column. That file now warns at its head. The title settle anchor is t~160, not t=118 -- it is ptcopyright, the last build-in element and the only glyph one. Said plainly that this is the reading under which their clock:'shared' collapses, and that a consequence is not a counter-argument. Told them NOT to change their 60 units/s yet: units/second is untouched by this and two of my own captures disagree ~2.9x on frames->seconds. The pad: sub_82457038 reads every field of XINPUT_GAMEPAD, 14/14 loads verified against the image, plus a second keystroke-queue path. Flagged as the superset the game can SEE, not the per-screen set, so it is used to check a binding table and not to write one.
This commit is contained in:
@@ -137,6 +137,9 @@ files, which is how the same ground got covered twice.
|
||||
| [`structures/stage-mission-tables.md`](structures/stage-mission-tables.md) | The stage table set — phases, routes, sub-objectives and AI parameters | ✅ the table set and how the stage record reaches it, validated across; **`AIParams` disc-wide: 23 objects, one shared 34-profile roster (782 records), loader `sub_8233C368`; `Type`→field-count holds except the two `_Test` templates** |
|
||||
| [`structures/texture-color-k8888.md`](structures/texture-color-k8888.md) | Texture colour interpretation — `k_8_8_8_8` (32bpp UI/HUD textures) | — |
|
||||
| [`ui-keyframe-time-unit.md`](ui-keyframe-time-unit.md) | What a keyframe time is worth, and what shape the ramp has | ✅ CONFIRMED from the running game's own draw stream — the ramp is **linear** (a declared 15-unit fade lands on `round(255·k/15)` for all seven samples) and the animation clock advances **2 time units per submitted frame**. 🟡 the seconds conversion (`1 unit = 1/60 s`) rests on a measured 27.6 present-frames/second |
|
||||
| [`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`). |
|
||||
| [`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. |
|
||||
| [`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) |
|
||||
| [`structures/ui-composable-bundles.md`](structures/ui-composable-bundles.md) | A screen build is not the only thing `compose` can draw | ✅ CONFIRMED by measurement over the disc, with the artifact to |
|
||||
|
||||
57
docs/re/data/input-pad-fields.txt
Normal file
57
docs/re/data/input-pad-fields.txt
Normal file
@@ -0,0 +1,57 @@
|
||||
# The pad poll sub_82457038: every XINPUT_GAMEPAD field it reads, and the
|
||||
# image bytes that prove it. VA -> file offset is VA - 0x82000000.
|
||||
# Database used as an index only; the image is the authority.
|
||||
|
||||
## disassembly of the field-compare block, image bytes beside each line
|
||||
0x82457220 817E0000 lwz r11, 0(r30)
|
||||
0x82457224 815F0034 lwz r10, 52(r31)
|
||||
0x82457228 7F0B5040 cmpl cr6, 0, r11, r10
|
||||
0x8245722C 419A0074 bc 12, 4*cr6+eq, 0x824572A0
|
||||
0x82457230 A17F0038 lhz r11, 56(r31)
|
||||
0x82457234 A15F0028 lhz r10, 40(r31)
|
||||
0x82457238 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245723C 409AFFCC bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457240 897F003A lbz r11, 58(r31)
|
||||
0x82457244 895F002A lbz r10, 42(r31)
|
||||
0x82457248 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245724C 409AFFBC bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457250 897F003B lbz r11, 59(r31)
|
||||
0x82457254 895F002B lbz r10, 43(r31)
|
||||
0x82457258 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245725C 409AFFAC bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457260 A17F003C lhz r11, 60(r31)
|
||||
0x82457264 A15F002C lhz r10, 44(r31)
|
||||
0x82457268 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245726C 409AFF9C bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457270 A17F003E lhz r11, 62(r31)
|
||||
0x82457274 A15F002E lhz r10, 46(r31)
|
||||
0x82457278 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245727C 409AFF8C bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457280 A17F0040 lhz r11, 64(r31)
|
||||
0x82457284 A15F0030 lhz r10, 48(r31)
|
||||
0x82457288 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245728C 409AFF7C bc 4, 4*cr6+eq, 0x82457208
|
||||
0x82457290 A17F0042 lhz r11, 66(r31)
|
||||
0x82457294 A15F0032 lhz r10, 50(r31)
|
||||
0x82457298 7F0A5840 cmpl cr6, 0, r10, r11
|
||||
0x8245729C 409AFF6C bc 4, 4*cr6+eq, 0x82457208
|
||||
0x824572A0 38600000 addi r3, r0, 0
|
||||
0x824572A4 38210090 addi r1, r1, 144
|
||||
|
||||
## independent re-encoding check (opcode|rD|rA|d rebuilt from the operands)
|
||||
0x82457230 image=0xA17F0038 expect=0xA17F0038 OK lhz r11,56(r31) prev wButtons
|
||||
0x82457234 image=0xA15F0028 expect=0xA15F0028 OK lhz r10,40(r31) new wButtons
|
||||
0x82457240 image=0x897F003A expect=0x897F003A OK lbz r11,58(r31) prev bLeftTrigger
|
||||
0x82457244 image=0x895F002A expect=0x895F002A OK lbz r10,42(r31) new bLeftTrigger
|
||||
0x82457250 image=0x897F003B expect=0x897F003B OK lbz r11,59(r31) prev bRightTrigger
|
||||
0x82457254 image=0x895F002B expect=0x895F002B OK lbz r10,43(r31) new bRightTrigger
|
||||
0x82457260 image=0xA17F003C expect=0xA17F003C OK lhz r11,60(r31) prev sThumbLX
|
||||
0x82457264 image=0xA15F002C expect=0xA15F002C OK lhz r10,44(r31) new sThumbLX
|
||||
0x82457270 image=0xA17F003E expect=0xA17F003E OK lhz r11,62(r31) prev sThumbLY
|
||||
0x82457274 image=0xA15F002E expect=0xA15F002E OK lhz r10,46(r31) new sThumbLY
|
||||
0x82457280 image=0xA17F0040 expect=0xA17F0040 OK lhz r11,64(r31) prev sThumbRX
|
||||
0x82457284 image=0xA15F0030 expect=0xA15F0030 OK lhz r10,48(r31) new sThumbRX
|
||||
0x82457290 image=0xA17F0042 expect=0xA17F0042 OK lhz r11,66(r31) prev sThumbRY
|
||||
0x82457294 image=0xA15F0032 expect=0xA15F0032 OK lhz r10,50(r31) new sThumbRY
|
||||
|
||||
14/14 agree. The state buffer is at r31+36; the previous copy at r31+52.
|
||||
118
docs/re/input-pad-read-path.md
Normal file
118
docs/re/input-pad-read-path.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# The pad read path — what the game asks the console for
|
||||
|
||||
**Status: ✅ decoded from the image** for the *driver* half; ❔ **not yet decoded**
|
||||
for which button bits the menus test. Instrument: ⟨image⟩ — the executable's own
|
||||
bytes, with the database used only as an index and every load verified against
|
||||
the file. 2026-09-01.
|
||||
|
||||
Asked by the 2026-09-01 play-test: the port shipped a milestone with **no joypad
|
||||
binding for Ⓐ or Ⓑ** and nothing caught it. The other half of that is knowing
|
||||
what the game itself reads, so a binding table can be checked against a fact
|
||||
rather than against whoever pressed which button.
|
||||
|
||||
---
|
||||
|
||||
## The three entry points, and there are only three
|
||||
|
||||
Every import the game has for controller input, and every caller, from `xrefs`:
|
||||
|
||||
| import | thunk | called from |
|
||||
|---|---|---|
|
||||
| `XamInputGetCapabilities` | `0x824AA840` | `sub_82456F58` |
|
||||
| `XamInputGetState` | `0x824AA848` | `sub_82173DC8`, `sub_82456F58`, `sub_82457038` |
|
||||
| `XamInputGetKeystrokeEx` | `0x824AA870` | `sub_82457038` (×3) |
|
||||
| `XamInputSetState` | — | no caller found (rumble is imported and unused, or reached indirectly) |
|
||||
|
||||
`sub_82173DC8` is **not** a button reader: it calls `XamInputGetState` only to
|
||||
compare the result against **1167** (`ERROR_DEVICE_NOT_CONNECTED`) and raise a
|
||||
flag. It is the controller-disconnected watcher.
|
||||
|
||||
**`sub_82457038` is the pad poll.** It is the only function that reads controller
|
||||
*data*.
|
||||
|
||||
## ✅ What the game reads: the whole of `XINPUT_GAMEPAD`
|
||||
|
||||
`sub_82457038` calls `XamInputGetState` with the output buffer at `r31+36`, which
|
||||
lays `XINPUT_STATE` over the pad object. It then compares **every field** of the
|
||||
new state against a 16-byte copy of the previous one at `r31+52`, and reports "no
|
||||
change" only if all seven match:
|
||||
|
||||
| offset (new / prev) | load | field |
|
||||
|---|---|---|
|
||||
| `+36` / `+52` | `lwz` | `dwPacketNumber` |
|
||||
| `+40` / `+56` | `lhz` | **`wButtons`** — the full 16-bit word |
|
||||
| `+42` / `+58` | `lbz` | **`bLeftTrigger`** |
|
||||
| `+43` / `+59` | `lbz` | **`bRightTrigger`** |
|
||||
| `+44` / `+60` | `lhz` | **`sThumbLX`** |
|
||||
| `+46` / `+62` | `lhz` | **`sThumbLY`** |
|
||||
| `+48` / `+64` | `lhz` | **`sThumbRX`** |
|
||||
| `+50` / `+66` | `lhz` | **`sThumbRY`** |
|
||||
|
||||
**Verified against the image, not the database.** All fourteen loads
|
||||
re-encoded from their operands and compared byte-for-byte with
|
||||
`/image/sylpheed.pe` at `VA − 0x82000000`:
|
||||
|
||||
```
|
||||
0x82457230 image=0xA17F0038 expect=0xA17F0038 OK lhz r11,56(r31)
|
||||
0x82457234 image=0xA15F0028 expect=0xA15F0028 OK lhz r10,40(r31)
|
||||
…
|
||||
14/14 instructions in the image agree with the database
|
||||
```
|
||||
|
||||
Full listing: [`data/input-pad-fields.txt`](data/input-pad-fields.txt).
|
||||
|
||||
**So the answer to "does the game read the triggers / the right stick / both
|
||||
axes" is yes, all of them, and it is decoded rather than observed.** There is no
|
||||
field of `XINPUT_GAMEPAD` the poll ignores.
|
||||
|
||||
⚠️ **What this does not say.** Reading a field is not using it. The poll's job is
|
||||
to detect *any* change; a screen may test only two bits of `wButtons`. This
|
||||
establishes the **superset** the game can see, which is exactly what a binding
|
||||
table needs to be checked against, and **not** the per-screen set.
|
||||
|
||||
## The second path: a keystroke queue
|
||||
|
||||
The same function calls `XamInputGetKeystrokeEx` three times with flags `= 3`,
|
||||
draining into a ring at `r31+68` (`{ptr, count, capacity}`) in **8-byte** records
|
||||
— the size of `XINPUT_KEYSTROKE`. So the game runs **two** input paths at once:
|
||||
|
||||
* the polled `XINPUT_GAMEPAD` state above, and
|
||||
* an **event queue** of keystrokes.
|
||||
|
||||
📌 This matters for the port and is already half-recorded elsewhere:
|
||||
`run-canary`'s own header notes that *"360 menus poll `XamInputGetKeystrokeEx`,
|
||||
not `GetState`, so a stubbed `GetKeystroke` looks like a completely dead pad"*,
|
||||
and the capture corpus counts hundreds of `XamInputGetKeystrokeEx` calls on the
|
||||
title. A menu that responds to a **press** is likely reading the queue; anything
|
||||
that responds to a **hold** must be reading the polled state. Which of the two
|
||||
each menu action uses is not decoded.
|
||||
|
||||
## ❔ Not decoded — which bits each screen tests
|
||||
|
||||
The consumer side is open. Two named footholds exist for it, from the image's own
|
||||
strings:
|
||||
|
||||
```
|
||||
0x820A5550 'C_PAD_DECODER 初期化' referenced from sub_8220B610
|
||||
0x820A5568 'C_PAD_DECODER 開放' referenced from sub_821A6470
|
||||
0x820A55BC 'C_PAD_RINGBUF 初期化' referenced from sub_8220B610
|
||||
0x820A55D4 'C_PAD_RINGBUF 開放' referenced from sub_821A6470
|
||||
```
|
||||
|
||||
`C_PAD_DECODER` and `C_PAD_RINGBUF` are the game's **own names** for this
|
||||
subsystem — construction and release traces, Shift-JIS, in one constructor
|
||||
(`sub_8220B610`) and one destructor (`sub_821A6470`). A *decoder* between the raw
|
||||
`wButtons` and the menus is where a game normally puts its repeat timing, its
|
||||
edge detection and its button remap, and it is the next place to read.
|
||||
|
||||
**Next experiment:** disassemble `sub_8220B610` for the pad object's layout, then
|
||||
find where `wButtons` at `+40` is consumed and what masks are tested against it.
|
||||
Report per-screen only if the code is per-screen; otherwise report the game-wide
|
||||
set and say so.
|
||||
|
||||
## Reach
|
||||
|
||||
⟨image⟩, so it is a fact about the shipped executable and holds for every screen
|
||||
— that is the point of doing it statically. It says nothing about which of these
|
||||
inputs any particular screen acts on, and nothing has been measured in a capture
|
||||
yet, so no row here may be labelled *measured*.
|
||||
Reference in New Issue
Block a user