From 91417417774d32b78de2f760d71b6223b63b030a Mon Sep 17 00:00:00 2001 From: sylph-decoder Date: Sat, 12 Sep 2026 10:57:46 +0000 Subject: [PATCH] re: F1 -- the "no auto-repeat" finding was measuring our own driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/port/HANDOFF.md | 23 ++++ docs/re/REFUTED.md | 23 ++++ docs/re/f1-no-repeat-was-the-harness.md | 152 ++++++++++++++++++++++++ docs/re/menu-navigation-semantics.md | 2 +- 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 docs/re/f1-no-repeat-was-the-harness.md diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 3c661234..a79e45cf 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -6560,3 +6560,26 @@ stops there"). boot's build-in β€” watch for a newly-decoded stream at the moment the plate reaches full alpha, no pad input needed. Not run this iteration; this page is the static half only. + +## πŸ”΄ F1 β€” keep your `-1.0` constants; the "no auto-repeat" measurement you might have seen doesn't hold up + +[`docs/re/f1-no-repeat-was-the-harness.md`](../re/f1-no-repeat-was-the-harness.md). +No new number for you β€” **do not take one from this page either.** + +If you'd read `menu-navigation-semantics.md` and concluded a held direction +never repeats: that row is now marked unsettled. The 2026-08-30 measurement +drove input through our own scripted pad driver, and that driver is +*deliberately* built to deliver exactly one event per held press β€” it could +not have shown repeat regardless of what the game does. The human's own +play-test description ("continues to move... at a medium pace") is not +contradicted by anything solid here. + +**A specific, testable prediction exists, but it is not yet measured:** +Canary's real-controller input driver auto-repeats keystrokes at a 400 ms +initial delay then a 100 ms interval (guest time), which β€” if the menu +treats each repeat event as one step β€” would read exactly like "medium +pace." That is a source-code fact about the emulator, not a game capture, +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. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 1409a5e2..caa38469 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -1071,3 +1071,26 @@ separate the four frames from anything. the right place to look, and it answered β€” the frames are drawn additive, exactly as their own two-background composite solve had ranked them. A wrong reason attached to a right direction; only the reason is refuted here. + +## Menu navigation and input (2026-09-12) + +* 🟑 "a held direction moves the cursor exactly once β€” no auto-repeat" + (`data/nav-autorepeat-and-settled-b.txt`, 2026-08-30) β€” **its own instrument + disagrees with itself.** `--hid=file`'s `GetKeystroke()` is written to + deliver exactly one event per held press, by explicit design ("scripted + input wants precisely one event per press, and repeat is what makes menu + steps overshoot" β€” `file_input_driver.h`), and `input-pad-read-path.md` + already found the game reads menu input through this same Keystroke API. + A driver built to prevent repeat cannot be evidence the game doesn't have + it. The counter's own control (a single tap gives exactly 1 spike) proves + the *counter* works; it says nothing about the *driver* it was counting + through. **Complication, not a clean reversal:** 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," which is a claim of an observed effect through this driver, + not a hypothetical. The two do not agree. **To settle:** trace + `C_PAD_RINGBUF`'s producer (keystroke ring vs. polled state) β€” no + emulator needed β€” or re-run with a repeat-capable file driver and read + cursor position off the draw log per frame, not a coarse screen-diff. + [`f1-no-repeat-was-the-harness.md`](f1-no-repeat-was-the-harness.md) ⟨harness⟩ diff --git a/docs/re/f1-no-repeat-was-the-harness.md b/docs/re/f1-no-repeat-was-the-harness.md new file mode 100644 index 00000000..db101602 --- /dev/null +++ b/docs/re/f1-no-repeat-was-the-harness.md @@ -0,0 +1,152 @@ +# F1 β€” "no auto-repeat" was measuring our own scripted-input driver, not the game + +**Status:** πŸ”΄ refutes [`menu-navigation-semantics.md`](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`](../../tools/re-capture/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`](../../tools/re-capture/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 ~4–5 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. diff --git a/docs/re/menu-navigation-semantics.md b/docs/re/menu-navigation-semantics.md index fd6dc29f..8af4d767 100644 --- a/docs/re/menu-navigation-semantics.md +++ b/docs/re/menu-navigation-semantics.md @@ -19,7 +19,7 @@ with [`tools/re-capture/menu_focus.py`](../../tools/re-capture/menu_focus.py). | **initial focus, main menu** | **`TUTORIAL`** β€” the *middle* item, not the top | 2/2 boots, the first frame after the menu appears | | **initial focus, `EXTRAS`** | `MISSION SELECT` β€” the top item | [`extras-wrap.png`](captures/menu-nav/extras-wrap.png) | | **⬆⬇ β€” one item per press** | one item per press | βœ… *indirect but sound*: the wrap montage's **4 presses from `EXTRAS` landing on `OPTIONS`** only counts out if each press moves exactly one | -| **⬆⬇ β€” no auto-repeat** | βœ… **none** β€” a **2.0 s held ⬇ moves the cursor exactly once** | measured 2026-08-30, [`data/nav-autorepeat-and-settled-b.txt`](data/nav-autorepeat-and-settled-b.txt). βœ… **The counter passes its control**: a single 0.12 s tap gives exactly **1** spike, and the hold gives **1**. Spike 0.0202–0.0220 against a 0.0003–0.0038 floor. ⚠️ one hold, 2.0 s, ⬇, main menu | +| **⬆⬇ β€” no auto-repeat** | πŸ”΄ **UNSETTLED, corrected 2026-09-12 β€” do not read this row as "none."** See [`f1-no-repeat-was-the-harness.md`](f1-no-repeat-was-the-harness.md): the driver this measurement drove input through (`--hid=file`'s `GetKeystroke`) is *deliberately* built to deliver exactly one event per press, by design, for other scripts' benefit β€” so it could not have shown repeat regardless of what the game does. A held direction moving the cursor exactly once was real and reproducible; what it proves is narrower than "no auto-repeat" and may be nothing at all. Kept below for the record | ~~measured 2026-08-30~~, [`data/nav-autorepeat-and-settled-b.txt`](data/nav-autorepeat-and-settled-b.txt). The counter passed its own control (a single 0.12 s tap gives exactly **1** spike, the hold gives **1**, against a 0.0003–0.0038 noise floor) β€” the counter was never the problem | | **wrap at the top** | ⬆ from the first item goes to the **last** | [`wrap-montage.png`](captures/menu-nav/wrap-montage.png), panels 1β†’2 | | **wrap at the bottom** | ⬇ from the last item goes to the **first** | same, panels 3β†’4, and 4 presses from `EXTRAS` landing on `OPTIONS` β€” i.e. wrapping β€” is what makes the count come out | | **left / right** | **nothing**, on the main menu | cursor unmoved across one β¬… and one ➑ |