diff --git a/src/xenia/app/xenia_main.cc b/src/xenia/app/xenia_main.cc index ab9437330..258cbc35c 100644 --- a/src/xenia/app/xenia_main.cc +++ b/src/xenia/app/xenia_main.cc @@ -64,6 +64,12 @@ #include "xenia/hid/winkey/winkey_hid.h" #include "xenia/hid/xinput/xinput_hid.h" #endif // XE_PLATFORM_WIN32 +// RE aid: a controller driven by a text file instead of a kernel input device. +// A uinput pad created inside a container registers with the HOST's input stack +// (input devices are not namespaced), so scripted input leaks to the desktop. +// This one is visible only to whoever can read the file. Header-only on purpose: +// it adds no build target. +#include "xenia/hid/file/file_input_driver.h" #if XE_PLATFORM_WIN32 #define APU_OPTIONS "[any, nop, sdl, xaudio2]" @@ -72,7 +78,7 @@ #elif XE_PLATFORM_LINUX #define APU_OPTIONS "[any, alsa, nop, sdl]" #define GPU_OPTIONS "[any, vulkan, null]" -#define HID_OPTIONS "[any, nop, sdl]" +#define HID_OPTIONS "[any, file, nop, sdl]" #else #define APU_OPTIONS "[any, nop, sdl]" #define GPU_OPTIONS "[any, vulkan, null]" @@ -82,6 +88,11 @@ DEFINE_string(apu, "any", "Audio system. Use: " APU_OPTIONS, "APU"); DEFINE_string(gpu, "any", "Graphics system. Use: " GPU_OPTIONS, "GPU"); DEFINE_string(hid, "any", "Input system. Use: " HID_OPTIONS, "HID"); +DEFINE_string(pad_file, "/tmp/xenia_pad.txt", + "Controller state file read by `--hid=file`: key=value pairs such " + "as `press=A,START lt=0 rt=255 lx=0 ly=0`. Absent keys are " + "neutral, a missing file means no input.", + "HID"); DEFINE_path( storage_root, "", @@ -446,6 +457,10 @@ std::vector> EmulatorApp::CreateInputDrivers( if (cvars::hid.compare("nop") == 0) { drivers.emplace_back( xe::hid::nop::Create(window, EmulatorWindow::kZOrderHidInput)); + } else if (cvars::hid.compare("file") == 0) { + // Explicit, never part of "any": this pad must be asked for. + drivers.emplace_back( + xe::hid::filepad::Create(window, EmulatorWindow::kZOrderHidInput)); } else { Factory factory; #if XE_PLATFORM_WIN32 diff --git a/src/xenia/hid/file/file_input_driver.h b/src/xenia/hid/file/file_input_driver.h new file mode 100644 index 000000000..7e4eacb81 --- /dev/null +++ b/src/xenia/hid/file/file_input_driver.h @@ -0,0 +1,246 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * RE aid: a controller whose state comes from a FILE, not from a device. + ****************************************************************************** + */ + +#ifndef XENIA_HID_FILE_FILE_INPUT_DRIVER_H_ +#define XENIA_HID_FILE_FILE_INPUT_DRIVER_H_ + +// Why this exists +// --------------- +// The scripted-input tool used for reverse engineering created its pad through +// `/dev/uinput`. Input devices are NOT namespaced by the kernel, so a uinput +// device created inside a container is registered with the HOST's input stack: +// every trigger hold and button press is delivered to whatever on the host reads +// gamepads, not only to the emulator. That is a real leak, and it was noticed the +// hard way. +// +// This driver takes the kernel out of the loop entirely. The pad state lives in +// an ordinary text file that only this container can see, and `GetState` reads it +// (re-parsing only when the file changes). Nothing is registered with the host, +// no X server is involved either, and — a bonus for RE — the analogue values are +// exact rather than whatever a virtual stick quantises to. +// +// File format: one or more whitespace/newline separated `key=value` pairs. +// +// press=A,START buttons by name (see kButtonNames), comma separated +// buttons=0x1010 or the raw XINPUT mask, if you prefer +// lt=0 rt=255 triggers, 0..255 +// lx=0 ly=0 left thumb, -32768..32767 +// rx=0 ry=0 right thumb, -32768..32767 +// +// Anything absent is neutral, so `press=A` alone is a valid file. An empty or +// missing file means "no input" — which is also the safe default if the file is +// deleted mid-run. +// +// Path: `--pad_file=`, default `/tmp/xenia_pad.txt`. Only user 0 is +// connected; other slots report no device, as a single-pad console would. + +#include +#include +#include +#include +#include +#include + +#include "xenia/base/cvar.h" +#include "xenia/base/logging.h" +#include "xenia/hid/input_driver.h" + +DECLARE_string(pad_file); + +namespace xe { +namespace hid { +namespace filepad { + +struct NamedButton { + const char* name; + uint16_t mask; +}; + +static constexpr NamedButton kButtonNames[] = { + {"UP", X_INPUT_GAMEPAD_DPAD_UP}, + {"DOWN", X_INPUT_GAMEPAD_DPAD_DOWN}, + {"LEFT", X_INPUT_GAMEPAD_DPAD_LEFT}, + {"RIGHT", X_INPUT_GAMEPAD_DPAD_RIGHT}, + {"START", X_INPUT_GAMEPAD_START}, + {"BACK", X_INPUT_GAMEPAD_BACK}, + {"LS", X_INPUT_GAMEPAD_LEFT_THUMB}, + {"RS", X_INPUT_GAMEPAD_RIGHT_THUMB}, + {"LB", X_INPUT_GAMEPAD_LEFT_SHOULDER}, + {"RB", X_INPUT_GAMEPAD_RIGHT_SHOULDER}, + {"A", X_INPUT_GAMEPAD_A}, + {"B", X_INPUT_GAMEPAD_B}, + {"X", X_INPUT_GAMEPAD_X}, + {"Y", X_INPUT_GAMEPAD_Y}, +}; + +class FileInputDriver final : public InputDriver { + public: + FileInputDriver(xe::ui::Window* window, size_t window_z_order) + : InputDriver(window, window_z_order) {} + ~FileInputDriver() override = default; + + X_STATUS Setup() override { + XELOGI("[file-pad] reading controller state from {}", cvars::pad_file); + return X_STATUS_SUCCESS; + } + + X_RESULT GetCapabilities(uint32_t user_index, uint32_t flags, + X_INPUT_CAPABILITIES* out_caps) override { + if (user_index != 0) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + std::memset(out_caps, 0, sizeof(*out_caps)); + out_caps->type = 0x01; // XINPUT_DEVTYPE_GAMEPAD + out_caps->sub_type = 0x01; // XINPUT_DEVSUBTYPE_GAMEPAD + out_caps->flags = 0; + out_caps->gamepad.buttons = 0xFFFF; + out_caps->gamepad.left_trigger = 0xFF; + out_caps->gamepad.right_trigger = 0xFF; + out_caps->gamepad.thumb_lx = static_cast(0xFFFFu); + out_caps->gamepad.thumb_ly = static_cast(0xFFFFu); + out_caps->gamepad.thumb_rx = static_cast(0xFFFFu); + out_caps->gamepad.thumb_ry = static_cast(0xFFFFu); + return X_ERROR_SUCCESS; + } + + X_RESULT GetState(uint32_t user_index, X_INPUT_STATE* out_state) override { + if (user_index != 0) { + return X_ERROR_DEVICE_NOT_CONNECTED; + } + Refresh(); + std::memset(out_state, 0, sizeof(*out_state)); + out_state->packet_number = packet_; + out_state->gamepad.buttons = buttons_; + out_state->gamepad.left_trigger = lt_; + out_state->gamepad.right_trigger = rt_; + out_state->gamepad.thumb_lx = lx_; + out_state->gamepad.thumb_ly = ly_; + out_state->gamepad.thumb_rx = rx_; + out_state->gamepad.thumb_ry = ry_; + return X_ERROR_SUCCESS; + } + + X_RESULT SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) override { + return user_index == 0 ? X_ERROR_SUCCESS : X_ERROR_DEVICE_NOT_CONNECTED; + } + + X_RESULT GetKeystroke(uint32_t user_index, uint32_t flags, + X_INPUT_KEYSTROKE* out_keystroke) override { + return X_ERROR_EMPTY; + } + + InputType GetInputType() const override { return InputType::Controller; } + + private: + // Re-parse only when the file actually changed: `GetState` is polled every + // frame and a stat is far cheaper than a read+parse. + void Refresh() { + struct stat st; + if (::stat(cvars::pad_file.c_str(), &st) != 0) { + Neutral(); + return; + } + if (st.st_mtime == mtime_ && st.st_size == size_) { + return; + } + mtime_ = st.st_mtime; + size_ = st.st_size; + std::FILE* f = std::fopen(cvars::pad_file.c_str(), "rb"); + if (!f) { + Neutral(); + return; + } + char buf[512] = {0}; + size_t n = std::fread(buf, 1, sizeof(buf) - 1, f); + std::fclose(f); + buf[n] = '\0'; + Parse(buf); + ++packet_; + } + + void Neutral() { + buttons_ = 0; + lt_ = rt_ = 0; + lx_ = ly_ = rx_ = ry_ = 0; + } + + void Parse(const char* text) { + Neutral(); + std::string s(text); + size_t pos = 0; + while (pos < s.size()) { + size_t end = s.find_first_of(" \t\r\n", pos); + if (end == std::string::npos) { + end = s.size(); + } + std::string tok = s.substr(pos, end - pos); + pos = end + 1; + size_t eq = tok.find('='); + if (eq == std::string::npos) { + continue; + } + std::string key = tok.substr(0, eq), val = tok.substr(eq + 1); + if (key == "press") { + size_t p = 0; + while (p < val.size()) { + size_t c = val.find(',', p); + if (c == std::string::npos) { + c = val.size(); + } + std::string name = val.substr(p, c - p); + p = c + 1; + for (const auto& b : kButtonNames) { + if (name == b.name) { + buttons_ |= b.mask; + break; + } + } + } + } else if (key == "buttons") { + buttons_ |= static_cast(std::strtoul(val.c_str(), nullptr, 0)); + } else if (key == "lt") { + lt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0)); + } else if (key == "rt") { + rt_ = Clamp8(std::strtol(val.c_str(), nullptr, 0)); + } else if (key == "lx") { + lx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); + } else if (key == "ly") { + ly_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); + } else if (key == "rx") { + rx_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); + } else if (key == "ry") { + ry_ = Clamp16(std::strtol(val.c_str(), nullptr, 0)); + } + } + } + + static uint8_t Clamp8(long v) { + return static_cast(v < 0 ? 0 : (v > 255 ? 255 : v)); + } + static int16_t Clamp16(long v) { + return static_cast(v < -32768 ? -32768 : (v > 32767 ? 32767 : v)); + } + + uint16_t buttons_ = 0; + uint8_t lt_ = 0, rt_ = 0; + int16_t lx_ = 0, ly_ = 0, rx_ = 0, ry_ = 0; + uint32_t packet_ = 1; + time_t mtime_ = 0; + off_t size_ = -1; +}; + +inline std::unique_ptr Create(xe::ui::Window* window, + size_t window_z_order) { + return std::make_unique(window, window_z_order); +} + +} // namespace filepad +} // namespace hid +} // namespace xe + +#endif // XENIA_HID_FILE_FILE_INPUT_DRIVER_H_