diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index bf176fad..1c9c82a2 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -6632,3 +6632,34 @@ now a clear reason no amount of re-testing through the file driver will ever produce one.** The next thing that could is patching repeat support into the file driver (proposed twice, not yet built) and re-running this exact capture. `-1.0` stays authored, not guessed, either way. + +## ✅ F1 — numbers, finally: 12 frames initial delay, 4 frames interval + +[`docs/re/f1-repeat-measured-via-driver-patch.md`](../re/f1-repeat-measured-via-driver-patch.md). +Built the fix the previous entry named: patched Canary's file driver to +emit `REPEAT` keystrokes using the SDL driver's own constants (400 ms +delay / 100 ms interval, guest time — opt-in, off by default, every other +scripted script unaffected), rebuilt, re-ran the identical held-⬇ capture. + +**The cursor moved continuously this time** — 19 distinct positions over +one hold, cycling and wrapping through the whole 5-item list. At this run's +achieved **29.87 fps** guest rate: + +* **initial delay: 12 frames** (~402 ms) from the ordinary press-triggered + step to the first repeat step; +* **steady-state interval: 4 frames** (~133 ms) for 13 of 15 gaps, 3 frames + (~100 ms) for the other 2. + +**Use 12 and 4.** ⚠️ The interval is measurably slower than the raw 100 ms +constant driving it (100 ms ÷ a ~33.5 ms frame is 2.99, not 4) — the game +appears to consume repeat events at its own per-frame pace rather than +instantly, and exactly why is not traced. The 4-frame figure is what +actually matters: it's what the cursor visibly does, which is what a port +needs to match. + +⚠️ **What this is not**: proof of what a *real controller* produces — it's +what the game does when fed `REPEAT` events shaped like the SDL driver's, +the closest thing to that oracle available in a container with no physical +pad. One run only; the corpus's two-run minimum isn't met, and the +3-vs-4-frame split in the interval is itself worth a second look. Good +enough to stop authoring `-1.0` against, not yet good enough to call final. diff --git a/docs/re/REFUTED.md b/docs/re/REFUTED.md index 25ce915c..18267209 100644 --- a/docs/re/REFUTED.md +++ b/docs/re/REFUTED.md @@ -1108,6 +1108,13 @@ attached to a right direction; only the reason is refuted here. Still 🟡, not ❌: only the file-driver path is tested; the SDL-driver path (what a real controller and the human's play-test went through) is not. [`f1-held-down-measured-no-repeat-via-file-driver.md`](f1-held-down-measured-no-repeat-via-file-driver.md) ⟨capture⟩ + ✅ **The other half done, same day — the Keystroke-`REPEAT` reading is + confirmed, not just favoured.** Patched the file driver to emit `REPEAT` + at the SDL driver's own constants (opt-in, off by default), rebuilt, + re-ran the identical capture: the cursor that moved once and stopped now + cycles continuously through the whole menu for as long as the button is + held. Measured: 12 frames initial delay, 4 frames steady interval, at + 29.87 fps guest. [`f1-repeat-measured-via-driver-patch.md`](f1-repeat-measured-via-driver-patch.md) ⟨capture⟩ * "F10-arming a UI draw capture is safe on any settled screen" — **implicit in every prior use of it, and false in a fresh container.** A `run-canary` launch with no signed-in profile (every container right after a restart — diff --git a/docs/re/data/f1-repeat-cursor-transitions.tsv b/docs/re/data/f1-repeat-cursor-transitions.tsv new file mode 100644 index 00000000..da6f4d6d --- /dev/null +++ b/docs/re/data/f1-repeat-cursor-transitions.tsv @@ -0,0 +1,23 @@ +# frame gtick gfreq_hz cursor_quad_y_ndc frames_since_prev_transition +# source: xenia_re_ui_draws_01.log, 2026-09-12, f1_hold_capture.py OUTDIR 2.5 repeat (run f1h) +# --pad_file_repeat=true: file_input_driver.h emits Keystroke REPEAT at SDL-driver constants (400ms delay/100ms interval, guest time) +# quad: page B5B1C73032BA3FA3, ~0.09x0.16 NDC (focus highlight) +1 8088762299 50000000 0.05 0 +3 8092093499 50000000 -0.16 2 +15 8112191520 50000000 -0.385 12 +19 8118864353 50000000 0.5025 4 +23 8125673641 50000000 0.28 4 +27 8132277847 50000000 0.055 4 +31 8138935070 50000000 -0.1625 4 +35 8145620699 50000000 -0.385 4 +39 8152349650 50000000 0.505 4 +43 8159014126 50000000 0.285 4 +46 8164059013 50000000 0.05 3 +50 8170775552 50000000 -0.1675 4 +54 8177444513 50000000 -0.3875 4 +58 8184235878 50000000 0.5025 4 +62 8190930524 50000000 0.28 4 +66 8197605724 50000000 0.05 4 +70 8204312489 50000000 -0.1675 4 +73 8209322148 50000000 -0.39 3 +77 8216032739 50000000 0.5 4 diff --git a/docs/re/f1-repeat-measured-via-driver-patch.md b/docs/re/f1-repeat-measured-via-driver-patch.md new file mode 100644 index 00000000..ee104985 --- /dev/null +++ b/docs/re/f1-repeat-measured-via-driver-patch.md @@ -0,0 +1,94 @@ +# F1 — measured: ~12 frames initial delay, ~4 frames steady interval, by giving the driver the repeat it lacked + +**Status:** ✅ measured (⟨capture⟩), via a purpose-built instrument, not the +game's own emulated hardware path. This is the direct follow-through on +[`f1-held-down-measured-no-repeat-via-file-driver.md`](f1-held-down-measured-no-repeat-via-file-driver.md)'s +named next step. 2026-09-12, same session. + +## What changed + +The prior page's conclusion: the file driver cannot show menu repeat because +its `GetKeystroke()` never emits a `REPEAT`-flagged event, by design, and the +menu's repeat is very likely driven by that flag rather than by polling raw +held state. That is a testable claim, so it was tested: patched +`/canary/src/xenia/hid/file/file_input_driver.h` to add opt-in repeat, +gated behind a new `--pad_file_repeat` cvar (off by default — every existing +scripted script keeps its one-event-per-press behaviour unchanged), using +the **exact same constants** as the SDL driver +(`HID_SDL_REPEAT_DELAY`/`_RATE` = 400/100, guest-time milliseconds via +`Clock::QueryGuestUptimeMillis()`) rather than re-deriving them. Rebuilt +Canary (`build-canary Release`, incremental, ~1 minute — only +`xenia_main.cc` and the header needed recompiling). Full patch in +`/canary`; not yet upstreamed into this repo's own tooling copy, since it's +Canary source, not `sylpheed-formats`. + +**Control:** the driver's own log confirms the mechanism fires as designed +— a held ⬇ produced repeated `[file-pad] keystroke vk=5811 repeat` lines at +the driver level, and zero crashes. + +## The result — the game DOES react to REPEAT, decisively + +Re-ran the exact same capture as the null result (boot to menu, hold ⬇ for +2.5 s wall-clock, draw-log per frame), this time with `--pad_file_repeat=true`. +Tracking the same focus-highlight quad (page `B5B1C73032BA3FA3`) that showed +exactly one move and then nothing in the prior run: **19 distinct positions +across the capture, spanning nearly the full NDC range** — the cursor +visibly cycled through the whole 5-item list multiple times, wrapping, for +as long as the button was held. The null result was real *for that driver +path*; giving the driver the one thing it lacked reverses it completely. + +Reference data: +[`data/f1-repeat-cursor-transitions.tsv`](data/f1-repeat-cursor-transitions.tsv) — +every transition's frame, guest tick and Y position, derived from the draw +log (log itself not committed, per the corpus's game-content rule). + +## The numbers, in frames at this run's achieved guest rate + +Achieved: **29.87 fps** (432 frames / 14.46 s guest time) — reported before +the numbers below, per `TEMPORAL-VERIFICATION.md`. + +| | frame | since previous | +|---|---|---| +| initial position | 1 | — | +| **press-triggered step** (not a repeat — the ordinary `KEYDOWN` edge) | 3 | 2 frames | +| **first repeat-driven step** | 15 | **12 frames** (402 ms) | +| every step after, ×15 | 19, 23, 27, 31, 35, 39, 43, 50, 54, 58, 62, 66, 70, 73, 77 | **4 frames** ×13, **3 frames** ×2 | + +**Initial delay: 12 frames (~402 ms guest) from the press-triggered step to +the first repeat.** Strikingly close to the SDL driver's own 400 ms +constant — expected, since that constant is what armed the timer, and the +closeness is a sanity check on the measurement more than a new fact. + +**Steady-state interval: predominantly 4 frames (~133 ms), with 2 of 16 +gaps at 3 frames (~100 ms).** This is *not* the same as the 100 ms constant +that drives the underlying `REPEAT` emission — it is measurably slower and +shows the frame-vs-100ms aliasing pattern you'd expect if the game consumes +repeat events at its own per-frame pace rather than reacting to every one +instantly (100 ms ÷ 33.5 ms/frame = 2.99, not 4). **The 4-frame figure is +the one that matters for the port**: it is what the cursor visibly does, +regardless of how the underlying keystroke stream is paced. The mismatch +against the raw driver constant is noted, not resolved — tracing exactly +where the extra ~30 ms per step goes (game-side frame batching of drained +keystrokes, most likely, given `sub_82457038` drains up to three +`XamInputGetKeystrokeEx` calls per poll) is future work, not needed to +answer what's asked here. + +## What this is not + +**Not proof this is what a real controller produces.** It is proof of what +the game does when *fed* `REPEAT` events at the same rate and shape the SDL +driver would produce — the closest thing to that oracle this container can +exercise, since no physical controller exists here. If a future session can +compare against an actual SDL-driver capture, this is the number to check it +against. Classified `measured`, not `decoded`, for exactly that reason: nothing +about this rate is decoded from the disc, and the delay/interval constants +were chosen by us (borrowed from Canary's own upstream driver, not the game). + +## Reach + +One run. The corpus's own two-run minimum for reproducibility is not met — +flagging rather than overclaiming. The steady-state interval's 3-vs-4-frame +split (14:2 across 16 gaps) is itself worth a second run to see if that +ratio holds or was a one-off aliasing artefact of this particular boot's +exact frame phase. The initial-delay figure rests on a single transition and +would benefit from the same repeat. diff --git a/tools/re-capture/f1_hold_capture.py b/tools/re-capture/f1_hold_capture.py index e3dbbd9d..edc71ef2 100644 --- a/tools/re-capture/f1_hold_capture.py +++ b/tools/re-capture/f1_hold_capture.py @@ -15,7 +15,13 @@ Reuses nav_repeat_and_b.py's proven boot-to-menu gate (glyph counting over a live x11grab pipe) verbatim in spirit -- that gate is the part of this apparatus already known to work -- and replaces its *measurement* half. - f1_hold_capture.py OUTDIR [hold_secs] + f1_hold_capture.py OUTDIR [hold_secs] [repeat] + +`repeat` passes --pad_file_repeat=true, which only exists on a Canary build +carrying the patch described in docs/re/f1-repeat-measured-via-driver-patch.md +(file_input_driver.h: opt-in Keystroke REPEAT at the SDL driver's own 400ms/ +100ms constants). Without that patch this flag is simply unrecognised -- +check `run-canary --help` output before relying on it after a rebuild. """ import os import subprocess @@ -26,6 +32,7 @@ import numpy as np OUT = sys.argv[1] HOLD_S = float(sys.argv[2]) if len(sys.argv) > 2 else 2.5 +REPEAT = len(sys.argv) > 3 and sys.argv[3] in ("1", "true", "repeat") os.makedirs(OUT, exist_ok=True) SD = os.path.dirname(os.path.abspath(__file__)) PAD = os.path.join(SD, "pad.py") @@ -123,10 +130,12 @@ def main(): "run: run-canary --create_profile_if_none=Tag, wait ~5s, kill it", flush=True) return - print(f"signing in profile {xuid}", flush=True) + print(f"signing in profile {xuid}, pad_file_repeat={REPEAT}", flush=True) + args = ["run-canary", f"--logged_profile_slot_0_xuid={xuid}"] + if REPEAT: + args.append("--pad_file_repeat=true") proc = subprocess.Popen( - ["run-canary", f"--logged_profile_slot_0_xuid={xuid}"], - cwd=OUT, env=env, stdout=canary_log, stderr=subprocess.STDOUT) + args, cwd=OUT, env=env, stdout=canary_log, stderr=subprocess.STDOUT) print(f"canary pid={proc.pid}, waiting for window", flush=True) T0 = time.time()