diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index a79e45cf..04d6b60f 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -6583,3 +6583,13 @@ and it competes with a second reading of the evidence that isn't resolved either (see the page for both). **Keep authoring against `-1.0` until a capture backs one of these**, same as before — this narrows why the number is missing, it doesn't supply one. + +**2026-09-12 update, same page:** traced `C_PAD_RINGBUF` (the structure the +game's own pad decoder reads) and it carries four axis-shaped fields +alongside its button word — a shape only the **polled** controller state has +(analog sticks have no keystroke equivalent), not a keystroke queue as the +name alone had suggested. That favours the "the driver was always capable, +the coarse screen-diff detector undercounted" reading over the +Keystroke/400-100ms one, though neither is confirmed yet and the actual +producer still isn't found. Still no number — still don't take one from +here. diff --git a/docs/re/f1-no-repeat-was-the-harness.md b/docs/re/f1-no-repeat-was-the-harness.md index db101602..72eeefa4 100644 --- a/docs/re/f1-no-repeat-was-the-harness.md +++ b/docs/re/f1-no-repeat-was-the-harness.md @@ -108,44 +108,93 @@ longer hold through this same file driver and saw more than one step. Both routes converge on the same next step below. +## 2026-09-12 update — traced `C_PAD_RINGBUF`, and the naming inference was wrong + +Last iteration's open question was whether `C_PAD_RINGBUF` is fed by the +keystroke ring or the polled state, and said its *name* suggested a queue. +Traced this time, ⟨image⟩, addresses re-verified against `/xenia-rs/sylpheed.db` +(not run against the raw `.pe` this pass — see reach below): + +* `C_PAD_DECODER`'s own constructor (`sub_8220B610`) allocates + `C_PAD_RINGBUF` itself — a 52-byte control struct plus a 1024-byte backing + buffer (`リングバッファ確保エラー size=%d` / `C_PAD_RINGBUF 初期化`, both + read off the disc's own Shift-JIS strings) — and stores the pointer at + `this+76`. +* `C_PAD_DECODER`'s update (`sub_8220B8C0`) takes the **input-manager + singleton** (`sub_824574C0`, already named in + [`structures/title-a-press-fault.md`](structures/title-a-press-fault.md)) + as its third argument, and reads `this+76`'s ring at offsets **12, 36, 40, + 44 and 48** — not just the one button word `input-pad-read-path.md`'s + `sub_8220B8C0` excerpt showed. Offsets 36–48 are four consecutive 32-bit + fields, read together, converted through the same int→double stack + round-trip a stick axis conversion would use. + +**That is the tell.** `XamInputGetKeystrokeEx` reports discrete digital +button edges — it has no field for an analog stick position. A structure +that carries four axis-shaped fields alongside a button word cannot be a +keystroke queue; it reads as a **periodically-refreshed input snapshot** +(buttons and both sticks together), which only the **polled** +`XINPUT_GAMEPAD` path has to offer. The "ring buffer" name is the authors' +own choice of implementation (a reusable slot, not a growing queue), not +evidence of an event stream — my prior reading of the name was the wrong +inference, now corrected by tracing it. + +**This favours the second hypothesis in the section above**: `pad.py`'s +"longer holds auto-repeat" caution is more likely the real mechanism, and +`nav_repeat_and_b.py`'s null result is more likely a **sampling artifact** +of its coarse ~4–5 fps screen-diff detector than a structural driver limit. +It does not flip the Keystroke/SDL-driver prediction to false outright — +a menu could still layer Keystroke-driven navigation on top of a +polled-state decoder for other purposes — but the balance of evidence moved. + +**Not found this pass, and still open:** the actual *producer* that writes +into `C_PAD_RINGBUF`'s offsets 12/36-48 each frame. `sub_8220B8C0` only +*reads* them; nothing in its own 5 856 bytes stores through the chased +`this+76` pointer, so some other function holds a reference to the same +ring instance. `sub_821A9DC8` (one of `sub_8220B8C0`'s three callers) fetches +the input-manager singleton immediately before each call, which is the +strongest lead for where to look next, not yet followed to its own producer. + ## What is NOT established, and is the reach -* Whether `C_PAD_DECODER` (which the human's play-test almost certainly went - through, and which `pad-decoder-double-tap-not-key-repeat.md` already found - has **no timer** on any direction path) reads its bits from the keystroke - queue or from the polled `XINPUT_GAMEPAD` state. `input-pad-read-path.md` - names this exact gap: *"which of the two each menu action uses is not - decoded."* `C_PAD_RINGBUF`'s name suggests the former (a queue, matching - the keystroke ring's own `{ptr, count, capacity}` shape at `sub_82457038` - `r31+68`) but that is a naming inference, not a trace. +* The actual producer of `C_PAD_RINGBUF`'s contents, per the update above — + narrowed to "somewhere reachable from the input-manager singleton," not + found. * Whether the menu's consumer of that ring treats a `REPEAT`-flagged - keystroke identically to a fresh `KEYDOWN`, or filters it out. + keystroke identically to a fresh `KEYDOWN` — now less likely to be the + relevant question at all, per the update above, but not ruled out. * Which `--hid` driver `run-canary` uses by default, i.e. which of these two numbers (none, or 400/100) the human's own play-test actually went through. Any of these could be wrong without changing the one thing this page does establish: **the previous "none" result cannot stand as evidence either -way**, because its instrument was built to prevent repeat by design. +way**, because its instrument was built to prevent Keystroke repeat by +design, and now more plausibly was never testing the path that matters. ## What would close it -Two options, cheapest first: +Two options, cheapest first — and the 2026-09-12 update changes which one is +cheapest: -1. **Read, not run.** Trace `C_PAD_RINGBUF`'s producer (whatever writes - `this+12` that `sub_8220B8C0` masks) back to either the keystroke ring or - the polled state buffer. Settles the mechanism question with no emulator - time, and turns the 400/100 ms prediction into a decoded fact or rules it - out, in either direction. -2. **Measure.** Add an opt-in repeat mode to `file_input_driver`'s - `GetKeystroke()` (mirroring the SDL driver's `Waiting`/`Repeating` state - machine and the same two constants, default OFF so every other scripted - script keeps its current one-event-per-press behaviour), boot to the main - menu via the established `boot_menu.sh`/`skip_intro.sh` gate, hold a - direction, and read the cursor's position per frame off the draw log — - the same instrument `f1-menu-repeat-harness-built-not-answered.md` already - validated for this exact purpose. Report frame counts at the achieved - present rate, per `TEMPORAL-VERIFICATION.md`, not a 400/100 ms guess - dressed as measured. +1. **Read, not run.** Finish tracing `C_PAD_RINGBUF`'s producer from + `sub_821A9DC8`'s input-manager fetch forward, to confirm it copies from + the polled `XINPUT_GAMEPAD` fields rather than the keystroke ring. + Settles the mechanism with no emulator time. Given the axis-shaped fields + already found, this is now confirmation work, not a coin flip. +2. **Measure — and the existing driver may already be enough.** If the + producer is polled-state as the update above now favours, + `file_input_driver`'s `GetState()` needs **no modification**: it already + holds a button continuously with no edge suppression. The likelier defect + is the *detector*, not the driver — `nav_repeat_and_b.py`'s ~4–5 fps + screen-diff undercounts a fast repeat. Re-run with the draw-log + position-tracking instrument `f1-menu-repeat-harness-built-not-answered.md` + already built and validated (per-frame cursor position, not a coarse + pixel-diff), gated on the main menu via `boot_menu.sh`/`skip_intro.sh` + rather than blind sleeps, holding a direction through the plain + `--hid=file` driver as-is. Report frame counts at the achieved present + rate, per `TEMPORAL-VERIFICATION.md`. The opt-in Keystroke-repeat mode + this page originally proposed adding to the file driver is now a + fallback for if this comes back null again, not the first thing to try. Not run this iteration — this is the static half, and it is large enough on its own (a reversal of a standing claim) not to stack a build-and-boot unit