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 ➑ |