[HID] file-pad: implement GetKeystroke -- menus do not read GetState
Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Failing after 2m3s
Orchestrator / Windows (x86-64) (push) Has been skipped
Orchestrator / Linux (x86-64) (push) Has been skipped
Orchestrator / Create Release (push) Has been skipped

The pad looked correct and did nothing. Its own log showed A arriving, the
emulator sat on "PRESS (A) BUTTON", and the title never advanced.

Cause: 360 front-ends poll XamInputGetKeystrokeEx, not XamInputGetState. This
title imports both, and its menus use the keystroke path; the driver returned
X_ERROR_EMPTY there, so every scripted press went into the void while GetState
faithfully reported a button nobody asked about.

Implement it edge-triggered, one event per call: KEYUPs for everything released
first, then KEYDOWNs, matching the SDL driver's ordering (so a thumb transition
clears before it sets). Deliberately NO auto-repeat -- scripted input wants
exactly one event per press, and repeat is precisely what makes menu steps
overshoot. Bits without a virtual key (guide, unused) are swallowed rather than
re-offered forever.

Verified on the real game: title -> main menu -> EXTRAS driven entirely from the
pad file, with `[file-pad] keystroke vk=5800 down/up` in the log for each press.
This commit is contained in:
2026-08-13 20:27:38 +00:00
parent e3e17e4951
commit 15fe11d5d9

View File

@@ -49,6 +49,7 @@
#include "xenia/base/cvar.h" #include "xenia/base/cvar.h"
#include "xenia/base/logging.h" #include "xenia/base/logging.h"
#include "xenia/hid/input_driver.h" #include "xenia/hid/input_driver.h"
#include "xenia/ui/virtual_key.h"
DECLARE_string(pad_file); DECLARE_string(pad_file);
@@ -129,8 +130,79 @@ class FileInputDriver final : public InputDriver {
return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED; return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED;
} }
// Menus do NOT read the pad through GetState. "PRESS (A) BUTTON" and most
// 360 front-ends poll XamInputGetKeystrokeEx, so a driver that only answers
// GetState looks completely dead on a title screen while its own log happily
// shows the button arriving. Returning X_ERROR_EMPTY here is what made the
// first scripted run press A into the void.
//
// 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.
X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags, X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags,
X_INPUT_KEYSTROKE* out_keystroke) override { X_INPUT_KEYSTROKE* out_keystroke) override {
const bool user_any = user_index == 0xFF || user_index == 0xFFFFFFFFu;
if (!user_any && user_index != 0) {
return X_ERROR_DEVICE_NOT_CONNECTED;
}
if (!out_keystroke) {
return X_ERROR_BAD_ARGUMENTS;
}
Refresh();
// Bit index in X_INPUT_GAMEPAD::buttons -> virtual key. Order matters: it is
// the order multiple simultaneous changes are reported in.
static constexpr uint16_t kVk[16] = {
uint16_t(ui::VirtualKey::kXInputPadDpadUp),
uint16_t(ui::VirtualKey::kXInputPadDpadDown),
uint16_t(ui::VirtualKey::kXInputPadDpadLeft),
uint16_t(ui::VirtualKey::kXInputPadDpadRight),
uint16_t(ui::VirtualKey::kXInputPadStart),
uint16_t(ui::VirtualKey::kXInputPadBack),
uint16_t(ui::VirtualKey::kXInputPadLThumbPress),
uint16_t(ui::VirtualKey::kXInputPadRThumbPress),
uint16_t(ui::VirtualKey::kXInputPadLShoulder),
uint16_t(ui::VirtualKey::kXInputPadRShoulder),
0, /* guide */
0, /* unused */
uint16_t(ui::VirtualKey::kXInputPadA),
uint16_t(ui::VirtualKey::kXInputPadB),
uint16_t(ui::VirtualKey::kXInputPadX),
uint16_t(ui::VirtualKey::kXInputPadY),
};
const uint16_t changed = static_cast<uint16_t>(buttons_ ^ reported_);
if (!changed) {
return X_ERROR_EMPTY;
}
for (int pass = 0; pass < 2; ++pass) {
const bool clear_pass = pass == 0;
for (uint8_t i = 0; i < 16; ++i) {
const uint16_t bit = static_cast<uint16_t>(1u << i);
if (!(changed & bit) || kVk[i] == 0) {
continue;
}
const bool pressed = (buttons_ & bit) != 0;
if (clear_pass == pressed) {
continue;
}
reported_ = static_cast<uint16_t>(pressed ? (reported_ | bit)
: (reported_ & ~bit));
out_keystroke->virtual_key = kVk[i];
out_keystroke->unicode = 0;
out_keystroke->flags =
pressed ? X_INPUT_KEYSTROKE_KEYDOWN : X_INPUT_KEYSTROKE_KEYUP;
out_keystroke->user_index = 0;
out_keystroke->hid_code = 0;
XELOGI("[file-pad] keystroke vk={:04X} {}", kVk[i],
pressed ? "down" : "up");
return X_ERROR_SUCCESS;
}
}
// Only bits without a virtual key changed (guide/unused): swallow them so
// the caller is not asked again forever.
reported_ = buttons_;
return X_ERROR_EMPTY; return X_ERROR_EMPTY;
} }
@@ -249,6 +321,8 @@ class FileInputDriver final : public InputDriver {
uint8_t lt_ = 0, rt_ = 0; uint8_t lt_ = 0, rt_ = 0;
int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0; int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0;
uint32_t packet_ = 1; uint32_t packet_ = 1;
// Buttons already reported through GetKeystroke; the edge detector's memory.
uint16_t reported_ = 0;
bool present_ = false; bool present_ = false;
time_t mtime_sec_ = 0; time_t mtime_sec_ = 0;
long mtime_nsec_ = -1; long mtime_nsec_ = -1;