Files
Sylpheed/docs/re/f1-no-repeat-was-the-harness.md
sylph-decoder 531c2d0ca4 re: F1 -- the "no auto-repeat" finding was measuring our own driver
Issue #1's premise (the human's play-test: a held direction repeats) directly
contradicted menu-navigation-semantics.md's 2026-08-30 "no auto-repeat"
row. Read the instrument before trusting either: nav_repeat_and_b.py drove
input through Canary's --hid=file driver, and that driver's GetKeystroke()
is explicitly, deliberately built to deliver exactly one event per held
press ("scripted input wants precisely one event per press, and repeat is
what makes menu steps overshoot" -- file_input_driver.h's own comment).
input-pad-read-path.md already established the game reads menu input via
this same Keystroke API. A driver engineered to prevent repeat cannot be
evidence the game lacks it -- the counter's control (a tap gives 1 spike)
proved the counter works, not that the driver could show more than one.

Not a clean reversal, and said so: the same driver's GetState() holds a
button continuously with no edge suppression, and pad.py's own docstring --
written by an earlier session driving this exact tool -- warns that a longer
dpad hold "auto-repeats and overshoots," describing an observed effect
through this same driver. The two pieces of evidence disagree and this page
does not resolve which wins.

Also read from Canary's source: the SDL input driver (what a real controller
goes through) auto-repeats keystrokes at 400 ms initial delay then 100 ms
interval, guest time (HID_SDL_REPEAT_DELAY/RATE, upstream Xenia, not a
project change) -- a concrete, testable prediction for what the real number
could be if the menu treats repeat-flagged keystrokes as nav steps, matching
the human's "medium pace" description. Not yet measured.

Refutation attempt this iteration, recorded per adversarial duty: targeted
the 2026-08-30 "no auto-repeat,  measured" claim. Survives only partially --
demoted to unsettled, not flipped to a confident opposite. New REFUTED.md
section (Menu navigation and input) and the row in
menu-navigation-semantics.md both corrected in place, old text kept per
convention.

What would close it: trace C_PAD_RINGBUF's producer (keystroke ring vs
polled state) statically, or add an opt-in repeat mode to the file driver
and read cursor position off the draw log per frame. Neither run this
iteration -- this is the static half, and reversing a standing claim is
enough for one unit without stacking a build-and-boot run on top of it
unverified.
2026-09-12 10:57:46 +00:00

8.0 KiB
Raw Blame History

F1 — "no auto-repeat" was measuring our own scripted-input driver, not the game

Status: 🔴 refutes menu-navigation-semantics.md's "⬆⬇ — no auto-repeat none" row. decoded (⟨canary-source⟩) why that measurement could never have shown repeat. 🟡 a specific, testable prediction for what the real number is — not yet measured. Instrument: ⟨canary-source⟩, /canary/src/xenia/hid/*, read directly, no emulator run this iteration.

Issue #1 asks for the held-direction repeat's initial delay and interval, in frames, and states the existence half as already settled by the human's own play-test: "it actually continues to move when holding up/down... at a medium pace." That directly contradicts this corpus's own prior measurement. One of the two is wrong, and it matters which before anyone spends emulator time chasing a number.

The prior measurement, and why it could not have found repeat

nav_repeat_and_b.py drove a held ⬇ through Canary's --hid=file pad-file driver and found exactly one cursor-move spike over a 2.0 s hold, with a clean control (a single 0.12 s tap also gives exactly one spike). The counter is not the problem — the driver is:

/canary/src/xenia/hid/file/file_input_driver.h, GetKeystroke(), in its own words:

Deliberately NO auto-repeat — scripted input wants precisely one event per press, and repeat is what makes menu steps overshoot.

and, two lines above:

Menus do NOT read the pad through GetState. … 360 front-ends poll XamInputGetKeystrokeEx

Mechanically: GetKeystroke() computes changed = buttons_ ^ reported_ and returns X_ERROR_EMPTY whenever nothing has changed since the last call — reported_ is updated to match on every delivered edge, so a held button produces exactly one KEYDOWN, ever, no matter how long the pad file says it's down. This is a deliberate, documented design choice in our own harness, made for other scripts' benefit, not a property of the game. input-pad-read-path.md already established the game reads a keystroke queue via this exact API for menu input (§"The second path: a keystroke queue") — so a driver that cannot repeat a keystroke cannot show a menu repeating, structurally, regardless of hold duration.

Refutation attempt, recorded either way (adversarial duty, this iteration): targets menu-navigation-semantics.md's row "⬆⬇ — no auto-repeat none," believed since 2026-08-30, on the grounds that its instrument cannot deliver a repeated keystroke by design. Partly survives, partly doesn't — see the next section, which complicates this before it gets to be the whole story.

What a real controller would produce, read from the same source tree

Canary's sdl input driver — the one an actual joystick goes through, not the scripted file driver — implements keystroke repeat for real:

// sdl_input_driver.h
#define HID_SDL_REPEAT_DELAY 400
#define HID_SDL_REPEAT_RATE 100

sdl_input_driver.cc's GetKeystroke() uses Clock::QueryGuestUptimeMillis()guest time, not host wall-clock — to arm a Waiting state on the initial KEYDOWN, promote to Repeating after HID_SDL_REPEAT_DELAY (400 ms) elapses, then re-fire a KEYDOWN | KEYSTROKE_REPEAT event every HID_SDL_REPEAT_RATE (100 ms) after that, for as long as the button stays down. This is upstream Xenia machinery (not a project modification, unlike the file driver's comment above), present for every game Canary runs.

If the menu treats each incoming keystroke — including REPEAT-flagged ones — as one navigation step, then the on-screen behaviour a real controller would show is: first step on press, a 400 ms pause, then one step every 100 ms — which reads exactly as "medium pace, slow enough to see" against a "continues to move" description. That is a prediction, not yet a measurement.

A second, competing piece of evidence — and it points the other way

file_input_driver's GetState() is not edge-triggered at all: buttons_ is whatever the pad file's last write said, held continuously until the file changes, with no reported_-style consumption. So if a menu's repeat lives in polled state rather than the keystroke queue, this driver was always capable of showing it — the earlier section's "structurally cannot repeat" applies only to the GetKeystroke() path.

And there is direct, if secondhand, evidence that it does: pad.py's own docstring, written by an earlier session driving this exact driver, warns that its dpad helper defaults to a 0.06 s tap "longer auto-repeats and overshoots." That is a caution against a real observed effect, not a hypothetical — someone drove a longer hold through this same file driver and saw more than one step.

These two facts do not agree, and I am not resolving them here. Either:

  • the keystroke route is right, pad.py's caution is about something else (a different held-input path, or stale advice inherited from before GetKeystroke's no-repeat comment was written) — or
  • the polled-state route is right, and nav_repeat_and_b.py's null result is a sampling artifact: its cursor detector grabs frames at ~45 fps (_open()'s -r 4), a coarse enough rate that if the true interval is well under 200 ms, successive samples could straddle several real moves and something in that pipeline collapsed them to one spike rather than under-counting to a nonzero-but-wrong number — which is not obviously how a threshold-crossing detector fails, and is exactly why this needs checking rather than asserting.

Both routes converge on the same next step below.

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.
  • Whether the menu's consumer of that ring treats a REPEAT-flagged keystroke identically to a fresh KEYDOWN, or filters it 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.

What would close it

Two options, cheapest first:

  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.

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 on top of it unverified.