/** * @file src/platform/linux/input/inputtino_gamepad.cpp * @brief Definitions for inputtino gamepad input handling. */ // lib includes #include #include #include // local includes #include "inputtino_common.h" #include "inputtino_gamepad.h" #include "src/config.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" using namespace std::literals; namespace platf::gamepad { enum GamepadStatus { UHID_NOT_AVAILABLE = 0, ///< UHID is not available UINPUT_NOT_AVAILABLE, ///< UINPUT is not available XINPUT_NOT_AVAILABLE, ///< XINPUT is not available GAMEPAD_STATUS ///< Helper to indicate the number of status }; auto create_xbox_one() { return inputtino::XboxOneJoypad::create({.name = "Sunshine X-Box One (virtual) pad", // https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c#L147 .vendor_id = 0x045E, .product_id = 0x02EA, .version = 0x0408}); } auto create_switch() { return inputtino::SwitchJoypad::create({.name = "Sunshine Nintendo (virtual) pad", // https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h#L981 .vendor_id = 0x057e, .product_id = 0x2009, .version = 0x8111}); } auto create_ds5() { return inputtino::PS5Joypad::create({.name = "Sunshine DualSense (virtual) pad", .vendor_id = 0x054C, .product_id = 0x0CE6, .version = 0x8111}); } int alloc(input_raw_t *raw, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { ControllerType selectedGamepadType; if (config::input.gamepad == "xone"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (manual selection)"sv; selectedGamepadType = XboxOneWired; } else if (config::input.gamepad == "ds5"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualSense 5 controller (manual selection)"sv; selectedGamepadType = DualSenseWired; } else if (config::input.gamepad == "switch"sv) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (manual selection)"sv; selectedGamepadType = SwitchProWired; } else if (metadata.type == LI_CTYPE_XBOX) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (auto-selected by client-reported type)"sv; selectedGamepadType = XboxOneWired; } else if (metadata.type == LI_CTYPE_PS) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by client-reported type)"sv; selectedGamepadType = DualSenseWired; } else if (metadata.type == LI_CTYPE_NINTENDO) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Nintendo Pro controller (auto-selected by client-reported type)"sv; selectedGamepadType = SwitchProWired; } else if (config::input.motion_as_ds4 && (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by motion sensor presence)"sv; selectedGamepadType = DualSenseWired; } else if (config::input.touchpad_as_ds4 && (metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be DualShock 5 controller (auto-selected by touchpad presence)"sv; selectedGamepadType = DualSenseWired; } else { BOOST_LOG(info) << "Gamepad " << id.globalIndex << " will be Xbox One controller (default)"sv; selectedGamepadType = XboxOneWired; } if (selectedGamepadType == XboxOneWired || selectedGamepadType == SwitchProWired) { if (metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has motion sensors, but they are not usable when emulating a joypad different from DS5"sv; } if (metadata.capabilities & LI_CCAP_TOUCHPAD) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has a touchpad, but it is not usable when emulating a joypad different from DS5"sv; } if (metadata.capabilities & LI_CCAP_RGB_LED) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " has an RGB LED, but it is not usable when emulating a joypad different from DS5"sv; } } else if (selectedGamepadType == DualSenseWired) { if (!(metadata.capabilities & (LI_CCAP_ACCEL | LI_CCAP_GYRO))) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have motion sensors active"sv; } if (!(metadata.capabilities & LI_CCAP_TOUCHPAD)) { BOOST_LOG(warning) << "Gamepad " << id.globalIndex << " is emulating a DualShock 5 controller, but the client gamepad doesn't have a touchpad"sv; } } auto gamepad = std::make_shared(joypad_state {}); auto on_rumble_fn = [feedback_queue, idx = id.clientRelativeIndex, gamepad](int low_freq, int high_freq) { // Don't resend duplicate rumble data if (gamepad->last_rumble.type == platf::gamepad_feedback_e::rumble && gamepad->last_rumble.data.rumble.lowfreq == low_freq && gamepad->last_rumble.data.rumble.highfreq == high_freq) { return; } gamepad_feedback_msg_t msg = gamepad_feedback_msg_t::make_rumble(idx, low_freq, high_freq); feedback_queue->raise(msg); gamepad->last_rumble = msg; }; switch (selectedGamepadType) { case XboxOneWired: { auto xOne = create_xbox_one(); if (xOne) { (*xOne).set_on_rumble(on_rumble_fn); gamepad->joypad = std::make_unique(std::move(*xOne)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual Xbox One controller: " << xOne.getErrorMessage(); return -1; } } case SwitchProWired: { auto switchPro = create_switch(); if (switchPro) { (*switchPro).set_on_rumble(on_rumble_fn); gamepad->joypad = std::make_unique(std::move(*switchPro)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual Switch Pro controller: " << switchPro.getErrorMessage(); return -1; } } case DualSenseWired: { auto ds5 = create_ds5(); if (ds5) { (*ds5).set_on_rumble(on_rumble_fn); (*ds5).set_on_led([feedback_queue, idx = id.clientRelativeIndex, gamepad](int r, int g, int b) { // Don't resend duplicate LED data if (gamepad->last_rgb_led.type == platf::gamepad_feedback_e::set_rgb_led && gamepad->last_rgb_led.data.rgb_led.r == r && gamepad->last_rgb_led.data.rgb_led.g == g && gamepad->last_rgb_led.data.rgb_led.b == b) { return; } auto msg = gamepad_feedback_msg_t::make_rgb_led(idx, r, g, b); feedback_queue->raise(msg); gamepad->last_rgb_led = msg; }); // Activate the motion sensors feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100)); feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100)); gamepad->joypad = std::make_unique(std::move(*ds5)); raw->gamepads[id.globalIndex] = std::move(gamepad); return 0; } else { BOOST_LOG(warning) << "Unable to create virtual DualShock 5 controller: " << ds5.getErrorMessage(); return -1; } } } return -1; } void free(input_raw_t *raw, int nr) { // This will call the destructor which in turn will stop the background threads for rumble and LED (and ultimately remove the joypad device) raw->gamepads[nr]->joypad.reset(); raw->gamepads[nr].reset(); } void update(input_raw_t *raw, int nr, const gamepad_state_t &gamepad_state) { auto gamepad = raw->gamepads[nr]; if (!gamepad) { return; } std::visit([gamepad_state](inputtino::Joypad &gc) { gc.set_pressed_buttons(gamepad_state.buttonFlags); gc.set_stick(inputtino::Joypad::LS, gamepad_state.lsX, gamepad_state.lsY); gc.set_stick(inputtino::Joypad::RS, gamepad_state.rsX, gamepad_state.rsY); gc.set_triggers(gamepad_state.lt, gamepad_state.rt); }, *gamepad->joypad); } void touch(input_raw_t *raw, const gamepad_touch_t &touch) { auto gamepad = raw->gamepads[touch.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports touch input if (std::holds_alternative(*gamepad->joypad)) { if (touch.pressure > 0.5) { std::get(*gamepad->joypad).place_finger(touch.pointerId, touch.x * inputtino::PS5Joypad::touchpad_width, touch.y * inputtino::PS5Joypad::touchpad_height); } else { std::get(*gamepad->joypad).release_finger(touch.pointerId); } } } void motion(input_raw_t *raw, const gamepad_motion_t &motion) { auto gamepad = raw->gamepads[motion.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports motion if (std::holds_alternative(*gamepad->joypad)) { switch (motion.motionType) { case LI_MOTION_TYPE_ACCEL: std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::ACCELERATION, motion.x, motion.y, motion.z); break; case LI_MOTION_TYPE_GYRO: std::get(*gamepad->joypad).set_motion(inputtino::PS5Joypad::GYROSCOPE, deg2rad(motion.x), deg2rad(motion.y), deg2rad(motion.z)); break; } } } void battery(input_raw_t *raw, const gamepad_battery_t &battery) { auto gamepad = raw->gamepads[battery.id.globalIndex]; if (!gamepad) { return; } // Only the PS5 controller supports battery reports if (std::holds_alternative(*gamepad->joypad)) { inputtino::PS5Joypad::BATTERY_STATE state; switch (battery.state) { case LI_BATTERY_STATE_CHARGING: state = inputtino::PS5Joypad::BATTERY_CHARGHING; break; case LI_BATTERY_STATE_DISCHARGING: state = inputtino::PS5Joypad::BATTERY_DISCHARGING; break; case LI_BATTERY_STATE_FULL: state = inputtino::PS5Joypad::BATTERY_FULL; break; case LI_BATTERY_STATE_UNKNOWN: case LI_BATTERY_STATE_NOT_PRESENT: default: return; } if (battery.percentage != LI_BATTERY_PERCENTAGE_UNKNOWN) { std::get(*gamepad->joypad).set_battery(state, battery.percentage); } } } std::vector &supported_gamepads(input_t *input) { if (!input) { static std::vector gps { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"xone", false, ""}, supported_gamepad_t {"ds5", false, ""}, supported_gamepad_t {"switch", false, ""}, }; return gps; } auto ds5 = create_ds5(); auto switchPro = create_switch(); auto xOne = create_xbox_one(); static std::vector gps { supported_gamepad_t {"auto", true, ""}, supported_gamepad_t {"xone", static_cast(xOne), !xOne ? xOne.getErrorMessage() : ""}, supported_gamepad_t {"ds5", static_cast(ds5), !ds5 ? ds5.getErrorMessage() : ""}, supported_gamepad_t {"switch", static_cast(switchPro), !switchPro ? switchPro.getErrorMessage() : ""}, }; for (auto &[name, is_enabled, reason_disabled] : gps) { if (!is_enabled) { BOOST_LOG(warning) << "Gamepad " << name << " is disabled due to " << reason_disabled; } } return gps; } } // namespace platf::gamepad