F1 — measure the held-direction repeat: initial delay and interval, in frames #1

Closed
opened 2026-09-04 15:48:05 +00:00 by fabi · 7 comments
Owner

Play-test 2026-09-02, the human, on the real game:

"Moving stick up/down and holding only moves one item. In game it actually continues to move when holding up/down, just at a medium pace so player does not need to move pad middle↔up/down, but also slow enough to see which item is selected and move to target."

The existence half is settled and settled against us: it repeats. The rate is not measured, and must not be guessed — that description bounds it and supplies no number.

What to produce. Two numbers, as frame counts at a stated present rate, not a stopwatch reading (docs/agents/TEMPORAL-VERIFICATION.md):

  • the initial delay before the first repeat;
  • the repeat interval after it.

And two yes/no answers: does the d-pad differ from the stick, and does the rate accelerate while held or stay flat?

Pass: counted in Canary, reproduced across two runs, gated on an observable rather than a wall-clock delay — the trap f5_snap_or_accelerate.sh already hit once.
Fail: a plausible interval with no capture behind it.

Blocks the Port's implementation.

Play-test 2026-09-02, the human, on the real game: > *"Moving stick up/down and holding only moves one item. In game it actually continues to move when holding up/down, just at a medium pace so player does not need to move pad middle↔up/down, but also slow enough to see which item is selected and move to target."* The existence half is settled and settled against us: it repeats. **The rate is not measured, and must not be guessed** — that description bounds it and supplies no number. **What to produce.** Two numbers, as frame counts at a stated present rate, not a stopwatch reading (`docs/agents/TEMPORAL-VERIFICATION.md`): * the **initial delay** before the first repeat; * the **repeat interval** after it. And two yes/no answers: does the d-pad differ from the stick, and does the rate accelerate while held or stay flat? **Pass:** counted in Canary, reproduced across two runs, gated on an observable rather than a wall-clock delay — the trap `f5_snap_or_accelerate.sh` already hit once. **Fail:** a plausible interval with no capture behind it. Blocks the Port's implementation.
fabi added this to the Menus milestone 2026-09-04 15:48:05 +00:00
fabi added the state/proposedagent/decoderkind/item labels 2026-09-04 15:48:05 +00:00
fabi added state/approved and removed state/proposed labels 2026-09-04 15:59:13 +00:00
Collaborator

Not answered yet, but the premise conflict is resolved enough to say why no
number exists: docs/re/f1-no-repeat-was-the-harness.md,
in #23 (9141741).

This corpus already had a contradicting result on file —
menu-navigation-semantics.md recorded "no auto-repeat, measured" on
2026-08-30, which flatly disagrees with the play-test this issue quotes.
Checked the instrument (⟨canary-source⟩, /canary/src/xenia/hid/file/file_input_driver.h):
that measurement drove input through our own scripted pad driver, whose
GetKeystroke() is deliberately built to deliver exactly one event per
held press — "repeat is what makes menu steps overshoot" is the driver's own
comment. The game reads menu input through this same Keystroke API
(input-pad-read-path.md), so a driver engineered to prevent repeat cannot
be evidence the game lacks it. Refutation attempt against that 2026-08-30
claim, recorded — it doesn't survive as stated.

Not a clean reversal, though, and I didn't force it into one: the same
driver's GetState() holds a button continuously, and pad.py's own
docstring warns a longer hold "auto-repeats and overshoots" — an observed
effect through this exact tool. That's in tension with the driver-can't-
repeat finding and I haven't resolved which wins.

A concrete, testable prediction, from Canary's source, not yet measured:
the real-controller (SDL) input driver auto-repeats keystrokes at 400 ms
initial delay then 100 ms interval, guest time — upstream Xenia, not
project code. If the menu treats a repeat-flagged keystroke as one nav step,
that predicts almost exactly the "medium pace, slow enough to see" the
play-test describes.

What would close it, cheapest first: (1) trace C_PAD_RINGBUF's
producer — keystroke ring or polled state — no emulator needed; (2) add an
opt-in repeat mode to the file driver and read cursor position per frame off
the draw log, the instrument f1-menu-repeat-harness-built-not-answered.md
already validated for this. Neither run this iteration — reversing a
standing claim is enough for one unit.

Stays state/approved; the numbers issue #1 wants are still open.

Not answered yet, but the premise conflict is resolved enough to say why no number exists: [`docs/re/f1-no-repeat-was-the-harness.md`](../../docs/re/f1-no-repeat-was-the-harness.md), in #23 (9141741). This corpus already had a *contradicting* result on file — `menu-navigation-semantics.md` recorded "no auto-repeat, measured" on 2026-08-30, which flatly disagrees with the play-test this issue quotes. Checked the instrument (⟨canary-source⟩, `/canary/src/xenia/hid/file/file_input_driver.h`): that measurement drove input through our own scripted pad driver, whose `GetKeystroke()` is *deliberately* built to deliver exactly one event per held press — "repeat is what makes menu steps overshoot" is the driver's own comment. The game reads menu input through this same Keystroke API (`input-pad-read-path.md`), so a driver engineered to prevent repeat cannot be evidence the game lacks it. Refutation attempt against that 2026-08-30 claim, recorded — it doesn't survive as stated. **Not a clean reversal, though**, and I didn't force it into one: the same driver's `GetState()` holds a button continuously, and `pad.py`'s own docstring warns a longer hold "auto-repeats and overshoots" — an *observed* effect through this exact tool. That's in tension with the driver-can't- repeat finding and I haven't resolved which wins. **A concrete, testable prediction, from Canary's source, not yet measured:** the real-controller (SDL) input driver auto-repeats keystrokes at 400 ms initial delay then 100 ms interval, guest time — upstream Xenia, not project code. If the menu treats a repeat-flagged keystroke as one nav step, that predicts almost exactly the "medium pace, slow enough to see" the play-test describes. **What would close it**, cheapest first: (1) trace `C_PAD_RINGBUF`'s producer — keystroke ring or polled state — no emulator needed; (2) add an opt-in repeat mode to the file driver and read cursor position per frame off the draw log, the instrument `f1-menu-repeat-harness-built-not-answered.md` already validated for this. Neither run this iteration — reversing a standing claim is enough for one unit. Stays `state/approved`; the numbers issue #1 wants are still open.
Collaborator

Continued from the last update, not a new question: traced C_PAD_RINGBUF
rather than reasoning from its name. Update appended to
docs/re/f1-no-repeat-was-the-harness.md,
in #23 (dfbefff).

C_PAD_DECODER's update reads that ring at offsets 12 (the button word
already known) and 36, 40, 44, 48 — four consecutive fields read
together through an int→double conversion an analog axis would use.
XamInputGetKeystrokeEx has no field for a stick position, so this cannot
be a keystroke queue; it reads as a periodically-refreshed polled-state
snapshot. My own prior reading of "ring buffer" as implying a queue was
wrong — refuted by tracing it, recorded per adversarial duty either way.

Practical effect: this favours the file driver's GetState() (already
capable of holding a button continuously, no changes needed) over the
Keystroke/400-100ms route, and makes nav_repeat_and_b.py's null result
more likely a sampling artifact (its ~4–5 fps screen-diff detector
against a possibly-fast repeat) than a structural driver limit. Revised
"what would close it" in the page: re-run the existing draw-log
position-tracking instrument, properly gated on the menu, before reaching
for a driver change.

Not found yet: the actual producer writing into the ring each frame —
narrowed to "reachable from the input-manager singleton fetch in
sub_821A9DC8," not traced to completion. Still no number. state/approved
stays.

Continued from the last update, not a new question: traced `C_PAD_RINGBUF` rather than reasoning from its name. Update appended to [`docs/re/f1-no-repeat-was-the-harness.md`](../../docs/re/f1-no-repeat-was-the-harness.md), in #23 (dfbefff). `C_PAD_DECODER`'s update reads that ring at offsets 12 (the button word already known) **and 36, 40, 44, 48** — four consecutive fields read together through an int→double conversion an analog axis would use. `XamInputGetKeystrokeEx` has no field for a stick position, so this cannot be a keystroke queue; it reads as a periodically-refreshed **polled-state** snapshot. My own prior reading of "ring buffer" as implying a queue was wrong — refuted by tracing it, recorded per adversarial duty either way. **Practical effect:** this favours the file driver's `GetState()` (already capable of holding a button continuously, no changes needed) over the Keystroke/400-100ms route, and makes `nav_repeat_and_b.py`'s null result more likely a **sampling artifact** (its ~4–5 fps screen-diff detector against a possibly-fast repeat) than a structural driver limit. Revised "what would close it" in the page: re-run the existing draw-log position-tracking instrument, properly gated on the menu, before reaching for a driver change. **Not found yet:** the actual producer writing into the ring each frame — narrowed to "reachable from the input-manager singleton fetch in `sub_821A9DC8`," not traced to completion. Still no number. `state/approved` stays.
Collaborator

Actually tried to run this measurement — first dynamic attempt on this
issue. docs/re/f1-hold-capture-harness-debugged.md,
in #23 (089aa8d). Still no number.

Built tools/re-capture/f1_hold_capture.py: boot to the settled menu via
the proven glyph gate, arm F10's UI-draw capture, hold a direction, read
cursor position per frame off the draw log. Four bugs across four boot
attempts, three fixed and confirmed:

  1. My own bugtap() shelled out to pad.py without this script's
    env, so the press silently went to the wrong pad file. Fixed, confirmed
    (title 154.5s, menu 163.0s next run).
  2. Persisted ui_draw_capture_frames/max at 3/20000 from a prior session
    log_ui_draws is now a documented no-op, command-line didn't visibly
    override these. Bumped in the container's xenia-canary.config.toml.
  3. The real one: this container has no signed-in profile at all (fresh
    after a restart), which reproduces structures/title-a-press-fault.md's
    already-diagnosed crash — verified byte-for-byte against that page's own
    addresses, looping continuously before F10 was ever pressed. boot_menu.sh
    already handles this; my script didn't. Fixed, confirmed (zero crashes
    with a profile signed in vs. dozens per run without).
  4. Found, not re-verified: no root-window blank before launch → a stale
    frame from a killed prior run gave a false "TITLE" read at 2.6s.
    Fixed in the script.

Stopped after 4 boot attempts (~9 min of emulator time) rather than chase a
fifth. The harness should be correct now; next attempt is the actual
measurement. Flagged loudly (REFUTED.md, HANDOFF) since bug 3 will hit any
bare run-canary call in any fresh container, not just mine. state/approved
stays — this is infrastructure progress, not an answer.

Actually tried to run this measurement — first dynamic attempt on this issue. [`docs/re/f1-hold-capture-harness-debugged.md`](../../docs/re/f1-hold-capture-harness-debugged.md), in #23 (089aa8d). Still no number. Built `tools/re-capture/f1_hold_capture.py`: boot to the settled menu via the proven glyph gate, arm F10's UI-draw capture, hold a direction, read cursor position per frame off the draw log. Four bugs across four boot attempts, three fixed and confirmed: 1. **My own bug** — `tap()` shelled out to `pad.py` without this script's env, so the press silently went to the wrong pad file. Fixed, confirmed (title 154.5s, menu 163.0s next run). 2. Persisted `ui_draw_capture_frames`/`max` at 3/20000 from a prior session — `log_ui_draws` is now a documented no-op, command-line didn't visibly override these. Bumped in the container's `xenia-canary.config.toml`. 3. **The real one:** this container has no signed-in profile at all (fresh after a restart), which reproduces `structures/title-a-press-fault.md`'s already-diagnosed crash — verified byte-for-byte against that page's own addresses, looping continuously before F10 was ever pressed. `boot_menu.sh` already handles this; my script didn't. Fixed, confirmed (zero crashes with a profile signed in vs. dozens per run without). 4. Found, not re-verified: no root-window blank before launch → a stale frame from a killed prior run gave a false "TITLE" read at 2.6s. Fixed in the script. Stopped after 4 boot attempts (~9 min of emulator time) rather than chase a fifth. The harness should be correct now; next attempt is the actual measurement. Flagged loudly (REFUTED.md, HANDOFF) since bug 3 will hit any bare `run-canary` call in any fresh container, not just mine. `state/approved` stays — this is infrastructure progress, not an answer.
Collaborator

Clean run landed — the harness debugged last iteration worked end to end.
docs/re/f1-held-down-measured-no-repeat-via-file-driver.md,
in #23 (2939fa2). Still no initial-delay/interval number, but now a sharp
reason why not.

Held ⬇ for 2.5s wall-clock (≈8s of guest time — this run went ~3.2× real
time, a cheap static menu with nothing pacing it to a display refresh).
Tracked one quad shape's position per frame off the draw log, not a screen
diff: one cursor step, caught within 133ms of arming, then nothing for
the remaining ~14s of guest time captured.
Broadened the match threshold
and found no other moving element.

This is a stronger negative than the 2026-08-30 result, not a different
one — it directly tests and refutes my own leading theory from last
iteration (C_PAD_RINGBUF's analog fields → coarse sampling was probably
hiding a fast polled-state repeat). A per-frame instrument over ~4× the
held-time still finds nothing. Refutation attempt against my own prior
lean, recorded — it doesn't survive.

Reconciled with the standing conflict rather than left hanging: the
human's play-test and pad.py's "auto-repeats" warning almost certainly
went through a real controller — Canary's SDL driver, which auto-repeats
keystrokes at a documented upstream 400ms delay / 100ms interval (guest
time) via the Keystroke REPEAT flag. Our scripted file driver
deliberately never emits that flag. So: the file driver structurally cannot
show repeat, at any hold length, through any measurement method — which is
exactly what both the 2026-08-30 result and this one show.

What would actually answer it: add opt-in REPEAT-flag support to the
file driver (matching the SDL driver's constants), gated so scripted work
elsewhere keeps today's one-event-per-press behavior, then re-run this exact
capture. Not attempted this iteration — a second unit on an already-complete
one. state/approved stays.

Clean run landed — the harness debugged last iteration worked end to end. [`docs/re/f1-held-down-measured-no-repeat-via-file-driver.md`](../../docs/re/f1-held-down-measured-no-repeat-via-file-driver.md), in #23 (2939fa2). Still no initial-delay/interval number, but now a sharp reason why not. Held ⬇ for 2.5s wall-clock (≈8s of *guest* time — this run went ~3.2× real time, a cheap static menu with nothing pacing it to a display refresh). Tracked one quad shape's position per frame off the draw log, not a screen diff: **one cursor step, caught within 133ms of arming, then nothing for the remaining ~14s of guest time captured.** Broadened the match threshold and found no other moving element. This is a *stronger* negative than the 2026-08-30 result, not a different one — it directly tests and refutes my own leading theory from last iteration (`C_PAD_RINGBUF`'s analog fields → coarse sampling was probably hiding a fast polled-state repeat). A per-frame instrument over ~4× the held-time still finds nothing. Refutation attempt against my own prior lean, recorded — it doesn't survive. **Reconciled with the standing conflict rather than left hanging:** the human's play-test and `pad.py`'s "auto-repeats" warning almost certainly went through a real controller — Canary's SDL driver, which auto-repeats keystrokes at a documented upstream 400ms delay / 100ms interval (guest time) via the Keystroke `REPEAT` flag. Our scripted `file` driver deliberately never emits that flag. So: the file driver structurally cannot show repeat, at any hold length, through any measurement method — which is exactly what both the 2026-08-30 result and this one show. **What would actually answer it:** add opt-in `REPEAT`-flag support to the file driver (matching the SDL driver's constants), gated so scripted work elsewhere keeps today's one-event-per-press behavior, then re-run this exact capture. Not attempted this iteration — a second unit on an already-complete one. `state/approved` stays.
Collaborator

Answered. docs/re/f1-repeat-measured-via-driver-patch.md,
in #23 (e61937f).

Built the fix the previous comment named: patched Canary's --hid=file
driver to emit Keystroke REPEAT at the SDL driver's own constants
(400ms delay / 100ms interval, guest time — opt-in --pad_file_repeat,
off by default, doesn't touch any other scripted script). Rebuilt
(incremental, ~1 minute), re-ran the identical held-⬇ capture that
previously showed one move and nothing else.

The cursor moved continuously this time — 19 distinct positions,
cycling and wrapping through the whole 5-item menu for the length of the
hold. At this run's achieved 29.87 fps guest rate:

  • initial delay: 12 frames (~402 ms)
  • steady-state interval: 4 frames (~133 ms, 13 of 15 gaps; 3 frames /
    ~100 ms for the other 2)

Use 12 and 4 frames. Flagging honestly rather than smoothing it over:
the interval is measurably slower than the raw 100 ms constant driving it
(100 ms ÷ a ~33.5 ms frame ≈ 3, not 4) — the game likely consumes drained
keystrokes at its own per-frame pace rather than instantly, and I haven't
traced exactly where the extra time goes. The 4-frame number is what
matters for the port: it's the visible on-screen cadence.

What this is not: a capture through an actual physical controller —
none exists in this container. This measures what the game does when fed
REPEAT events shaped like the SDL driver's, the closest available stand-in.
Classified measured, not decoded. One run only; the corpus's two-run
minimum isn't met, and the 3-vs-4-frame split is itself worth a second look
if anyone wants to firm this up further.

Moving to state/needs-human — this is as far as I can verify it myself,
and issue #2 (the Port's implementation) can use these numbers now.

Answered. [`docs/re/f1-repeat-measured-via-driver-patch.md`](../../docs/re/f1-repeat-measured-via-driver-patch.md), in #23 (e61937f). Built the fix the previous comment named: patched Canary's `--hid=file` driver to emit Keystroke `REPEAT` at the SDL driver's own constants (400ms delay / 100ms interval, guest time — opt-in `--pad_file_repeat`, off by default, doesn't touch any other scripted script). Rebuilt (incremental, ~1 minute), re-ran the identical held-⬇ capture that previously showed one move and nothing else. **The cursor moved continuously this time** — 19 distinct positions, cycling and wrapping through the whole 5-item menu for the length of the hold. At this run's achieved **29.87 fps** guest rate: * **initial delay: 12 frames** (~402 ms) * **steady-state interval: 4 frames** (~133 ms, 13 of 15 gaps; 3 frames / ~100 ms for the other 2) **Use 12 and 4 frames.** Flagging honestly rather than smoothing it over: the interval is measurably slower than the raw 100 ms constant driving it (100 ms ÷ a ~33.5 ms frame ≈ 3, not 4) — the game likely consumes drained keystrokes at its own per-frame pace rather than instantly, and I haven't traced exactly where the extra time goes. The 4-frame number is what matters for the port: it's the visible on-screen cadence. **What this is not:** a capture through an actual physical controller — none exists in this container. This measures what the game does when *fed* `REPEAT` events shaped like the SDL driver's, the closest available stand-in. Classified `measured`, not `decoded`. One run only; the corpus's two-run minimum isn't met, and the 3-vs-4-frame split is itself worth a second look if anyone wants to firm this up further. Moving to `state/needs-human` — this is as far as I can verify it myself, and issue #2 (the Port's implementation) can use these numbers now.
sylph-decoder added state/needs-human and removed state/approved labels 2026-09-12 12:31:34 +00:00
Collaborator

Small addendum, same PR (891c38e): found that HANDOFF.md's original
Q1–Q10 summary table (near the top of the file) still quoted the withdrawn
"no auto-repeat" finding as current, three commits after this session
measured the real numbers. A reader who only checks that summary table —
which a 6600+ line file trains you to do — would get the wrong answer.
Fixed with a pointer to the current entries rather than silently editing
the number in place. No new question here, just closing a gap I left in my
own prior edit.

Small addendum, same PR (891c38e): found that `HANDOFF.md`'s original Q1–Q10 summary table (near the top of the file) still quoted the withdrawn "no auto-repeat" finding as current, three commits after this session measured the real numbers. A reader who only checks that summary table — which a 6600+ line file trains you to do — would get the wrong answer. Fixed with a pointer to the current entries rather than silently editing the number in place. No new question here, just closing a gap I left in my own prior edit.
Author
Owner

Confirmed on a real controller, 2026-09-13, by the human who reported the original defect:

"Works now and speed is good."

That settles the question this issue could not answer from inside a container. REPEAT_INTERVAL = 0.134 — ~7.5 steps a second — reads as the "medium pace… slow enough to see which item is selected" of the 2026-09-02 play-test, from the same person, on the same hands.

📌 What that does and does not establish. It confirms the interval is not contradicted by the one oracle that matters. It does not promote the finding: still measured, not decoded; still one run against the corpus's two-run minimum; and the delay is still the constant that was fed in (Canary's SDL 400 ms), confirmed by the instrument rather than by the game. A human saying "good" cannot distinguish 0.402 from 0.38.

The port adopted it in #27, which also fixed a defect this measurement flushed out: held_direction() read the stick's position from the event history instead of polling the device, so the rate was correct and never ran.

Closing — the two numbers asked for exist, are cited, and are in use.

**Confirmed on a real controller, 2026-09-13, by the human who reported the original defect:** > *"Works now and speed is good."* That settles the question this issue could not answer from inside a container. `REPEAT_INTERVAL = 0.134` — ~7.5 steps a second — reads as the *"medium pace… slow enough to see which item is selected"* of the 2026-09-02 play-test, from the same person, on the same hands. 📌 **What that does and does not establish.** It confirms the interval is not contradicted by the one oracle that matters. It does **not** promote the finding: still `measured`, not `decoded`; still **one run** against the corpus's two-run minimum; and the **delay** is still the constant that was fed in (Canary's SDL 400 ms), confirmed by the instrument rather than by the game. A human saying "good" cannot distinguish 0.402 from 0.38. The port adopted it in #27, which also fixed a defect this measurement flushed out: `held_direction()` read the stick's position from the *event history* instead of polling the device, so the rate was correct and never ran. Closing — the two numbers asked for exist, are cited, and are in use.
fabi closed this issue 2026-09-13 12:45:24 +00:00
fabi removed the state/needs-human label 2026-09-17 19:02:18 +00:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Reference: fabi/Sylpheed#1