re: F1 -- traced C_PAD_RINGBUF; the queue naming inference was wrong
Some checks failed
CI / Native — linux (pull_request) Failing after 34m6s
CI / WASM — Web (pull_request) Successful in 31m37s
CI / Formatting (pull_request) Failing after 1m17s

Continuing the F1 investigation rather than starting a fresh one. Last
iteration left two competing hypotheses open (Keystroke-queue-driven vs
polled-state-driven repeat) and flagged C_PAD_RINGBUF's producer as the
cheapest thing to trace next -- named but not traced.

Traced it this time: C_PAD_DECODER's own constructor (sub_8220B610)
allocates C_PAD_RINGBUF (52-byte control struct, 1024-byte backing buffer,
confirmed against its own Shift-JIS trace strings -- "C_PAD_RINGBUF
initialization" and its allocation-error message). Its update function
(sub_8220B8C0) takes the input-manager singleton as a parameter and reads
the ring at offsets 12, 36, 40, 44 and 48 -- not just the one button word.

Offsets 36-48 are four consecutive fields read together through the same
int-to-double conversion an analog axis would use. XamInputGetKeystrokeEx
has no field for a stick position, so a structure carrying four axis-shaped
fields cannot be a keystroke queue -- it reads as a periodically-refreshed
polled-state snapshot. My own prior reading of the "ring buffer" name as
implying a queue was the wrong inference; refuted by tracing it, recorded
either way per adversarial duty.

This shifts the balance toward the second, previously-uncertain hypothesis:
the file driver's GetState() was always capable of showing real repeat (no
modification needed), and nav_repeat_and_b.py's null result is more likely
a sampling artifact of its ~4-5 fps screen-diff detector than a structural
driver limit. Revises "what would close it" accordingly -- re-run the
existing draw-log position-tracking instrument, gated on the menu properly,
before reaching for a driver change.

Not found: the actual producer that writes into C_PAD_RINGBUF each frame --
narrowed to "reachable from the input-manager singleton fetch in
sub_821A9DC8," not traced to completion. Still no number for issue #1;
this narrows the path to one, further than last iteration but not there.
This commit is contained in:
sylph-decoder
2026-09-12 11:05:22 +00:00
parent 9141741777
commit dfbefff512
2 changed files with 85 additions and 26 deletions

View File

@@ -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.

View File

@@ -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 3648 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 ~45 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 ~45 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