From 8e63a9542582dfe0d29e46e6eaaa7bcbecd156e1 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Sun, 13 Sep 2026 19:17:17 +0200 Subject: [PATCH] [RE] file-pad: opt-in Keystroke REPEAT, at the SDL driver's own constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instrument behind the F1 menu-repeat measurement. It was uncommitted for a day while the numbers it produced were already shipping in the port. The file driver deliberately emits exactly one event per press -- repeat is what makes scripted menu steps overshoot -- so it structurally could not show whether the GAME repeats a held direction. Measured through it, a held (down) moved the cursor once and never again, at any hold length. That is a property of the driver, not of the game, and the page that recorded it said so. `--pad_file_repeat` opts in, off by default, so no existing script changes. It reuses the SDL driver's Waiting/Repeating state machine and its constants VERBATIM (HID_SDL_REPEAT_DELAY=400, HID_SDL_REPEAT_RATE=100, guest-time ms) rather than re-deriving them -- the point is to be a fair stand-in for a real controller, and a re-derived constant would only measure our own arithmetic. With it, the cursor cycled the whole 5-item menu for as long as the button was held: 19 distinct positions, 12 frames to the first repeat, 4 frames between the rest, at 29.87 fps guest. ⚠️ The 4-frame interval is NOT the 100ms constant that drives it (100ms is ~3 frames). The game consumes drained keystrokes at its own per-frame pace. The 400ms delay, by contrast, comes back as 402ms -- that confirms the instrument, not the game. docs/re/f1-repeat-measured-via-driver-patch.md in the Sylpheed repo. Co-Authored-By: Claude Opus 5 --- src/xenia/app/xenia_main.cc | 6 +++ src/xenia/hid/file/file_input_driver.h | 51 +++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index 258cbc35c..98ba2b009 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -93,6 +93,12 @@ DEFINE_string(pad_file, "/tmp/xenia_pad.txt", "as `press=A,START lt=0 rt=255 lx=0 ly=0`. Absent keys are " "neutral, a missing file means no input.", "HID"); +DEFINE_bool(pad_file_repeat, false, + "RE aid: opt in to Keystroke REPEAT events on `--hid=file`, using " + "the SDL driver's exact 400ms/100ms guest-time constants. Off by " + "default -- every existing scripted script wants one event per " + "press, and repeat is what makes menu steps overshoot.", + "HID"); DEFINE_path( storage_root, "", diff --git a/src/xenia/hid/file/file_input_driver.h b/src/xenia/hid/file/file_input_driver.h index 810a10e19..1e62389d1 100644 --- a/src/xenia/hid/file/file_input_driver.h +++ b/src/xenia/hid/file/file_input_driver.h @@ -46,12 +46,14 @@ #include #include +#include "xenia/base/clock.h" #include "xenia/base/cvar.h" #include "xenia/base/logging.h" #include "xenia/hid/input_driver.h" #include "xenia/ui/virtual_key.h" DECLARE_string(pad_file); +DECLARE_bool(pad_file_repeat); namespace xe { namespace hid { @@ -138,8 +140,9 @@ class FileInputDriver final : public InputDriver { // // One event per call, edge triggered: KEYUPs for everything released, then // KEYDOWNs for everything pressed, exactly as the SDL driver orders them. - // Deliberately NO auto-repeat — scripted input wants precisely one event per - // press, and repeat is what makes menu steps overshoot. + // NO auto-repeat by default — scripted input wants precisely one event per + // press, and repeat is what makes menu steps overshoot. `--pad_file_repeat` + // opts in, below, for the one experiment that specifically needs it. X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) override { const bool user_any = user_index == 0xFF || user_index == 0xFFFFFFFFu; @@ -172,6 +175,32 @@ class FileInputDriver final : public InputDriver { uint16_t(ui::VirtualKey::kXInputPadY), }; + // RE aid: opt-in REPEAT, matching the SDL driver's Waiting/Repeating + // state machine and its exact constants (sdl_input_driver.h: + // HID_SDL_REPEAT_DELAY=400, HID_SDL_REPEAT_RATE=100, guest-time + // milliseconds) verbatim -- not re-derived -- so a measurement through + // this driver is a fair stand-in for what a real controller produces. + // Checked BEFORE the edge scan below, same order the SDL driver uses. + if (cvars::pad_file_repeat) { + const int64_t guest_now = Clock::QueryGuestUptimeMillis(); + if (repeat_state_ == RepeatState::kWaiting && + repeat_time_ms_ + kRepeatDelayMs < guest_now) { + repeat_state_ = RepeatState::kRepeating; + } + if (repeat_state_ == RepeatState::kRepeating && + repeat_time_ms_ + kRepeatRateMs < guest_now) { + repeat_time_ms_ = guest_now; + out_keystroke->virtual_key = kVk[repeat_bit_]; + out_keystroke->unicode = 0; + out_keystroke->user_index = 0; + out_keystroke->hid_code = 0; + out_keystroke->flags = static_cast( + X_INPUT_KEYSTROKE_KEYDOWN | X_INPUT_KEYSTROKE_REPEAT); + XELOGI("[file-pad] keystroke vk={:04X} repeat", kVk[repeat_bit_]); + return X_ERROR_SUCCESS; + } + } + const uint16_t changed = static_cast(buttons_ ^ reported_); if (!changed) { return X_ERROR_EMPTY; @@ -189,6 +218,15 @@ class FileInputDriver final : public InputDriver { } reported_ = static_cast(pressed ? (reported_ | bit) : (reported_ & ~bit)); + if (cvars::pad_file_repeat) { + if (pressed) { + repeat_state_ = RepeatState::kWaiting; + repeat_bit_ = i; + repeat_time_ms_ = Clock::QueryGuestUptimeMillis(); + } else if (repeat_bit_ == i) { + repeat_state_ = RepeatState::kIdle; + } + } out_keystroke->virtual_key = kVk[i]; out_keystroke->unicode = 0; out_keystroke->flags = @@ -327,6 +365,15 @@ class FileInputDriver final : public InputDriver { time_t mtime_sec_ = 0; long mtime_nsec_ = -1; off_t size_ = -1; + + // RE aid: `--pad_file_repeat` state, unused unless that cvar is set. Single + // slot -- like the SDL driver, only the most recently pressed bit repeats. + enum class RepeatState { kIdle, kWaiting, kRepeating }; + static constexpr int64_t kRepeatDelayMs = 400; // == HID_SDL_REPEAT_DELAY + static constexpr int64_t kRepeatRateMs = 100; // == HID_SDL_REPEAT_RATE + RepeatState repeat_state_ = RepeatState::kIdle; + uint8_t repeat_bit_ = 0; + int64_t repeat_time_ms_ = 0; }; inline std::unique_ptr Create(xe::ui::Window* window,