/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2013 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** * * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr const char* kDefaultInputBackend = #if REX_PLATFORM_WIN32 "xinput"; #else "sdl"; #endif } // namespace REXCVAR_DEFINE_STRING(input_backend, kDefaultInputBackend, "Input", "Input backend: sdl, xinput") .allowed({"sdl", "xinput"}); REXCVAR_DEFINE_BOOL(guide_button, false, "Input", "Enable guide button pass-through"); // Back+Start is deliberately not the default: Steam Input uses View+Menu // (Back+Start) as its Guide-button chord on pads without a Guide button, so // opening our settings would also pop the Steam overlay. REXCVAR_DEFINE_STRING(menu_chord, "lb+rb+start", "Input", "Controller chord that toggles the settings menu. Buttons joined by '+': " "a, b, x, y, lb, rb, l3, r3, back, start, dpad_up, dpad_down, dpad_left, " "dpad_right. Empty disables the chord."); REXCVAR_DEFINE_BOOL(hid_rumble_enabled, true, "Input", "Enable controller vibration"); REXCVAR_DEFINE_UINT32(hid_rumble_min_motor_speed, 0x1000, "Input", "Minimum XInput motor speed forwarded to host controllers") .range(0, 0xFFFF); namespace rex::input { namespace { uint16_t ChordButtonFromToken(std::string_view token) { if (token == "a") return X_INPUT_GAMEPAD_A; if (token == "b") return X_INPUT_GAMEPAD_B; if (token == "x") return X_INPUT_GAMEPAD_X; if (token == "y") return X_INPUT_GAMEPAD_Y; if (token == "lb") return X_INPUT_GAMEPAD_LEFT_SHOULDER; if (token == "rb") return X_INPUT_GAMEPAD_RIGHT_SHOULDER; if (token == "l3") return X_INPUT_GAMEPAD_LEFT_THUMB; if (token == "r3") return X_INPUT_GAMEPAD_RIGHT_THUMB; if (token == "back" || token == "select" || token == "view") return X_INPUT_GAMEPAD_BACK; if (token == "start" || token == "menu") return X_INPUT_GAMEPAD_START; if (token == "dpad_up") return X_INPUT_GAMEPAD_DPAD_UP; if (token == "dpad_down") return X_INPUT_GAMEPAD_DPAD_DOWN; if (token == "dpad_left") return X_INPUT_GAMEPAD_DPAD_LEFT; if (token == "dpad_right") return X_INPUT_GAMEPAD_DPAD_RIGHT; return 0; } // "lb+rb+start" -> button mask; 0 when empty or no token parses (chord off). uint16_t ChordMaskFromSpec(std::string_view spec) { uint16_t mask = 0; std::string token; for (size_t i = 0; i <= spec.size(); ++i) { char c = i < spec.size() ? spec[i] : '+'; if (c == '+' || c == ',' || c == ' ') { if (!token.empty()) { mask |= ChordButtonFromToken(token); token.clear(); } continue; } token.push_back(char(std::tolower(static_cast(c)))); } return mask; } } // namespace InputSystem::InputSystem(rex::ui::Window* window) : window_(window) {} InputSystem::~InputSystem() = default; X_STATUS InputSystem::Setup() { return X_STATUS_SUCCESS; } void InputSystem::Shutdown() { if (window_) { rex::ui::UIEvent closing_event(window_); for (auto& driver : drivers_) { if (auto* window_listener = dynamic_cast(driver.get())) { window_listener->OnClosing(closing_event); } } window_ = nullptr; } drivers_.clear(); } void InputSystem::AddDriver(std::unique_ptr driver) { drivers_.push_back(std::move(driver)); } void InputSystem::AttachWindow(rex::ui::Window* window) { window_ = window; for (auto& driver : drivers_) { driver->OnWindowAvailable(window); } } void InputSystem::SetActiveCallback(std::function callback) { active_callback_ = callback; for (auto& driver : drivers_) { driver->set_is_active_callback(callback); } } void InputSystem::SetMenuChordCallback(std::function callback) { menu_chord_callback_ = std::move(callback); } X_RESULT InputSystem::GetCapabilities(uint32_t user_index, uint32_t flags, X_INPUT_CAPABILITIES* out_caps) { SCOPE_profile_cpu_f("hid"); bool any_connected = false; for (auto& driver : drivers_) { X_RESULT result = driver->GetCapabilities(user_index, flags, out_caps); if (result != X_ERROR_DEVICE_NOT_CONNECTED) { any_connected = true; } if (result == X_ERROR_SUCCESS) { return result; } } return any_connected ? X_ERROR_EMPTY : X_ERROR_DEVICE_NOT_CONNECTED; } X_RESULT InputSystem::GetState(uint32_t user_index, X_INPUT_STATE* out_state) { SCOPE_profile_cpu_f("hid"); bool any_connected = false; bool first_result = true; X_INPUT_STATE merged = {}; for (auto& driver : drivers_) { X_INPUT_STATE state = {}; X_RESULT result = driver->GetState(user_index, &state); if (result != X_ERROR_DEVICE_NOT_CONNECTED) { any_connected = true; } if (result == X_ERROR_SUCCESS) { if (first_result) { merged = state; first_result = false; } else { // Merge: OR buttons, max triggers, max-magnitude sticks merged.gamepad.buttons = static_cast(merged.gamepad.buttons) | static_cast(state.gamepad.buttons); merged.gamepad.left_trigger = std::max(merged.gamepad.left_trigger, state.gamepad.left_trigger); merged.gamepad.right_trigger = std::max(merged.gamepad.right_trigger, state.gamepad.right_trigger); auto merge_axis = [](int16_t a, int16_t b) -> int16_t { return (std::abs(static_cast(a)) >= std::abs(static_cast(b))) ? a : b; }; merged.gamepad.thumb_lx = merge_axis(merged.gamepad.thumb_lx, state.gamepad.thumb_lx); merged.gamepad.thumb_ly = merge_axis(merged.gamepad.thumb_ly, state.gamepad.thumb_ly); merged.gamepad.thumb_rx = merge_axis(merged.gamepad.thumb_rx, state.gamepad.thumb_rx); merged.gamepad.thumb_ry = merge_axis(merged.gamepad.thumb_ry, state.gamepad.thumb_ry); if (static_cast(state.packet_number) > static_cast(merged.packet_number)) { merged.packet_number = state.packet_number; } } } } if (first_result) { return any_connected ? X_ERROR_EMPTY : X_ERROR_DEVICE_NOT_CONNECTED; } const uint16_t menu_chord_buttons = ChordMaskFromSpec(REXCVAR_GET(menu_chord)); const bool menu_chord_down = menu_chord_buttons != 0 && (static_cast(merged.gamepad.buttons) & menu_chord_buttons) == menu_chord_buttons; if (menu_chord_down && !menu_chord_down_ && menu_chord_callback_) { menu_chord_callback_(); } menu_chord_down_ = menu_chord_down; if (active_callback_ && !active_callback_()) { std::memset(&merged.gamepad, 0, sizeof(merged.gamepad)); } if (out_state) { *out_state = merged; } return X_ERROR_SUCCESS; } bool InputSystem::GetUiGamepadState(X_INPUT_GAMEPAD* out_gamepad) { X_INPUT_GAMEPAD merged = {}; bool any = false; auto merge_axis = [](int16_t a, int16_t b) -> int16_t { return (std::abs(static_cast(a)) >= std::abs(static_cast(b))) ? a : b; }; for (auto& driver : drivers_) { for (uint32_t user_index = 0; user_index < 4; ++user_index) { X_INPUT_STATE state = {}; if (driver->GetStateUi(user_index, &state) != X_ERROR_SUCCESS) { continue; } any = true; merged.buttons = static_cast(merged.buttons) | static_cast(state.gamepad.buttons); merged.left_trigger = std::max(merged.left_trigger, state.gamepad.left_trigger); merged.right_trigger = std::max(merged.right_trigger, state.gamepad.right_trigger); merged.thumb_lx = merge_axis(merged.thumb_lx, state.gamepad.thumb_lx); merged.thumb_ly = merge_axis(merged.thumb_ly, state.gamepad.thumb_ly); merged.thumb_rx = merge_axis(merged.thumb_rx, state.gamepad.thumb_rx); merged.thumb_ry = merge_axis(merged.thumb_ry, state.gamepad.thumb_ry); } } if (out_gamepad) { *out_gamepad = merged; } return any; } X_RESULT InputSystem::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibration) { SCOPE_profile_cpu_f("hid"); if (!vibration) { return X_ERROR_BAD_ARGUMENTS; } X_INPUT_VIBRATION filtered_vibration = {}; if (REXCVAR_GET(hid_rumble_enabled)) { const uint32_t min_motor_speed = REXCVAR_GET(hid_rumble_min_motor_speed); const auto filter_motor = [min_motor_speed](uint16_t speed) -> uint16_t { return speed >= min_motor_speed ? speed : 0; }; filtered_vibration.left_motor_speed = filter_motor(static_cast(vibration->left_motor_speed)); filtered_vibration.right_motor_speed = filter_motor(static_cast(vibration->right_motor_speed)); } bool any_connected = false; for (auto& driver : drivers_) { X_RESULT result = driver->SetState(user_index, &filtered_vibration); if (result != X_ERROR_DEVICE_NOT_CONNECTED) { any_connected = true; } if (result == X_ERROR_SUCCESS) { return result; } } return any_connected ? X_ERROR_EMPTY : X_ERROR_DEVICE_NOT_CONNECTED; } X_RESULT InputSystem::GetKeystroke(uint32_t user_index, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) { SCOPE_profile_cpu_f("hid"); bool any_connected = false; for (auto& driver : drivers_) { X_RESULT result = driver->GetKeystroke(user_index, flags, out_keystroke); if (result != X_ERROR_DEVICE_NOT_CONNECTED) { any_connected = true; } if (result == X_ERROR_SUCCESS || result == X_ERROR_EMPTY) { return result; } } return any_connected ? X_ERROR_EMPTY : X_ERROR_DEVICE_NOT_CONNECTED; } std::unique_ptr CreateDefaultInputSystem(bool tool_mode) { auto input = std::make_unique(nullptr); if (!tool_mode) { #if REX_PLATFORM_WIN32 if (REXCVAR_GET(input_backend) == "xinput") { auto xinput_driver = std::make_unique(nullptr, 0); if (xinput_driver->Setup() == X_STATUS_SUCCESS) { input->AddDriver(std::move(xinput_driver)); } } #endif if (REXCVAR_GET(input_backend) == "sdl") { auto sdl_driver = std::make_unique(nullptr, 0); if (sdl_driver->Setup() == X_STATUS_SUCCESS) { input->AddDriver(std::move(sdl_driver)); } } // MnK driver (keyboard/mouse -> controller emulation) auto mnk_driver = std::make_unique(nullptr, 0); if (mnk_driver->Setup() == X_STATUS_SUCCESS) { input->AddDriver(std::move(mnk_driver)); } } // NOP driver (primary in tool mode, fallback otherwise) uint8_t nop_index = tool_mode ? 0 : 1; input->AddDriver(std::make_unique(nullptr, nop_index)); return input; } } // namespace rex::input