[RE] file-pad: opt-in Keystroke REPEAT, at the SDL driver's own constants
Some checks failed
Orchestrator / Lint (push) Failing after 1m49s
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped

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 <noreply@anthropic.com>
This commit is contained in:
Sylpheed RE agent
2026-09-13 19:17:17 +02:00
parent ab3203f792
commit 8e63a95425
2 changed files with 55 additions and 2 deletions

View File

@@ -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, "",

View File

@@ -46,12 +46,14 @@
#include <string>
#include <sys/stat.h>
#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<uint16_t>(
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<uint16_t>(buttons_ ^ reported_);
if (!changed) {
return X_ERROR_EMPTY;
@@ -189,6 +218,15 @@ class FileInputDriver final : public InputDriver {
}
reported_ = static_cast<uint16_t>(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<InputDriver> Create(xe::ui::Window* window,