// Copyright 2021 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included #include #include "common/param_package.h" #include "common/settings_input.h" #include "common/thread.h" #include "input_common/drivers/joycon.h" namespace InputCommon { Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) { LOG_INFO(Input, "JC Adapter Initialization started"); const int init_res = SDL_hid_init(); if (init_res == 0) { Setup(); } else { LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res); } } Joycons::~Joycons() { Reset(); } void Joycons::UpdateJoyconData(JoyconData& jc, std::span buffer) { switch (jc.type) { case Joycon::ControllerType::Left: UpdateLeftPadInput(jc, buffer); break; case Joycon::ControllerType::Right: UpdateRightPadInput(jc, buffer); break; case Joycon::ControllerType::Pro: UpdateProPadInput(jc, buffer); break; case Joycon::ControllerType::None: break; } jc.raw_battery = buffer[2] >> 4; } std::string Joycons::JoyconName(std::size_t port) const { switch (joycon_data[port].type) { case Joycon::ControllerType::Left: return "Left Joycon"; case Joycon::ControllerType::Right: return "Right Joycon"; case Joycon::ControllerType::Pro: return "Pro Controller"; default: return "Unknow Joycon"; } } void Joycons::ResetDeviceType(std::size_t port) { if (port > joycon_data.size()) { return; } joycon_data[port].type = Joycon::ControllerType::None; } void Joycons::InputThread(std::stop_token stop_token) { LOG_INFO(Input, "JC Adapter scan thread started"); Common::SetCurrentThreadName("yuzu:input:JoyconInputThread"); input_thread_running = true; std::vector buffer(Joycon::max_resp_size); while (!stop_token.stop_requested()) { for (std::size_t port = 0; port < joycon_data.size(); ++port) { if (joycon_data[port].type == Joycon::ControllerType::None) { continue; } const int status = SDL_hid_read_timeout(joycon_data[port].joycon_handle.handle, buffer.data(), Joycon::max_resp_size, 15); if (IsPayloadCorrect(status, buffer)) { UpdateJoyconData(joycon_data[port], buffer); } } std::this_thread::yield(); } input_thread_running = false; } bool Joycons::IsPayloadCorrect(int status, std::span buffer) { if (status > 0) { if (buffer[0] == 0x00) { // Invalid response LOG_DEBUG(Input, "Error reading payload"); if (input_error_counter++ > 20) { LOG_ERROR(Input, "Timeout, Is the joycon connected?"); input_thread.request_stop(); } } return false; } input_error_counter = 0; return true; } void Joycons::Setup() { // Initialize all controllers as unplugged for (std::size_t port = 0; port < joycon_data.size(); ++port) { PreSetController(GetIdentifier(port)); joycon_data[port].type = Joycon::ControllerType::None; joycon_data[port].hd_rumble.high_amplitude = 0; joycon_data[port].hd_rumble.low_amplitude = 0; joycon_data[port].hd_rumble.high_frequency = 320.0f; joycon_data[port].hd_rumble.low_frequency = 160.0f; } if (!scan_thread_running) { scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); }); } } void Joycons::ScanThread(std::stop_token stop_token) { Common::SetCurrentThreadName("yuzu:input:JoyconScanThread"); scan_thread_running = true; std::size_t port = 0; while (!stop_token.stop_requested()) { LOG_INFO(Input, "Scanning for devices"); SDL_hid_device_info* devs = SDL_hid_enumerate(0x057e, 0x0); SDL_hid_device_info* cur_dev = devs; while (cur_dev) { LOG_WARNING(Input, "Device Found"); LOG_WARNING(Input, "type : {:04X} {:04X}", cur_dev->vendor_id, cur_dev->product_id); bool skip_device = false; for (std::size_t i = 0; i < joycon_data.size(); ++i) { if (Joycon::GetDeviceType(cur_dev) == joycon_data[i].type) { LOG_WARNING(Input, "Device already exist"); skip_device = true; break; } } if (!skip_device) { if (Joycon::CheckDeviceAccess(joycon_data[port].joycon_handle, cur_dev)) { LOG_WARNING(Input, "Device verified and accessible"); ++port; } } cur_dev = cur_dev->next; } GetJCEndpoint(); std::this_thread::sleep_for(std::chrono::seconds(2)); } scan_thread_running = false; } void Joycons::Reset() { scan_thread = {}; input_thread = {}; for (std::size_t port = 0; port < joycon_data.size(); ++port) { joycon_data[port].type = Joycon::ControllerType::None; if (joycon_data[port].joycon_handle.handle) { joycon_data[port].joycon_handle.handle = nullptr; joycon_data[port].joycon_handle.packet_counter = 0; } } SDL_hid_exit(); } void Joycons::GetJCEndpoint() { // init joycons for (std::size_t port = 0; port < joycon_data.size(); ++port) { auto& joycon = joycon_data[port]; auto& handle = joycon.joycon_handle; joycon.port = port; if (!joycon.joycon_handle.handle) { continue; } LOG_INFO(Input, "Initializing device {}", port); joycon.type = Joycon::GetDeviceType(handle); // joycon_data[port].mac= Joycon::GetMac(); // SetSerialNumber(joycon_data[port]); // SetVersionNumber(joycon_data[port]); // SetDeviceType(joycon_data[port]); joycon.calibration = Joycon::GetFactoryCalibrationData(handle, joycon.type); // GetColor(joycon_data[port]); joycon.rumble_enabled = true; joycon.imu_enabled = true; joycon.gyro_sensitivity = Joycon::GyroSensitivity::DPS2000; joycon.gyro_performance = Joycon::GyroPerformance::HZ833; joycon.accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8; joycon.accelerometer_performance = Joycon::AccelerometerPerformance::HZ100; Joycon::SetLedConfig(handle, static_cast(port + 1)); Joycon::EnableRumble(handle, joycon.rumble_enabled); Joycon::EnableImu(handle, joycon.imu_enabled); Joycon::SetImuConfig(handle, joycon.gyro_sensitivity, joycon.gyro_performance, joycon.accelerometer_sensitivity, joycon.accelerometer_performance); Joycon::SetReportMode(handle, Joycon::ReportMode::STANDARD_FULL_60HZ); } if (!input_thread_running) { input_thread = std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); }); } } bool Joycons::DeviceConnected(std::size_t port) const { if (port > joycon_data.size()) { return false; } return joycon_data[port].type != Joycon::ControllerType::None; } f32 Joycons::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const { const f32 value = static_cast(raw_value - calibration.center); if (value > 0.0f) { return value / calibration.max; } return value / calibration.min; } void Joycons::UpdateLeftPadInput(JoyconData& jc, std::span buffer) { static constexpr std::array left_buttons{ Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right, Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR, Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::StickL, }; const auto identifier = GetIdentifier(jc.port); const u32 raw_button = static_cast(buffer[5] | ((buffer[4] & 0b00101001) << 16)); for (std::size_t i = 0; i < left_buttons.size(); ++i) { const bool button_status = (raw_button & static_cast(left_buttons[i])) != 0; const int button = static_cast(left_buttons[i]); SetButton(identifier, button, button_status); } const u16 raw_left_axis_x = static_cast(buffer[6] | ((buffer[7] & 0xf) << 8)); const u16 raw_left_axis_y = static_cast((buffer[7] >> 4) | (buffer[8] << 4)); const f32 left_axis_x = GetAxisValue(raw_left_axis_x, jc.calibration.left_stick.x); const f32 left_axis_y = GetAxisValue(raw_left_axis_y, jc.calibration.left_stick.y); SetAxis(identifier, static_cast(PadAxes::LeftStickX), left_axis_x); SetAxis(identifier, static_cast(PadAxes::LeftStickY), left_axis_y); if (jc.imu_enabled) { auto left_motion = GetMotionInput(buffer, jc.calibration.imu, jc.accelerometer_sensitivity, jc.gyro_sensitivity); // Rotate motion axis to the correct direction left_motion.accel_y = -left_motion.accel_y; left_motion.accel_z = -left_motion.accel_z; left_motion.gyro_x = -left_motion.gyro_x; SetMotion(identifier, static_cast(PadMotion::LeftMotion), left_motion); } } void Joycons::UpdateRightPadInput(JoyconData& jc, std::span buffer) { static constexpr std::array right_buttons{ Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR, Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus, Joycon::PadButton::Home, Joycon::PadButton::StickR, }; const auto identifier = GetIdentifier(jc.port); const u32 raw_button = static_cast((buffer[3] << 8) | (buffer[4] << 16)); for (std::size_t i = 0; i < right_buttons.size(); ++i) { const bool button_status = (raw_button & static_cast(right_buttons[i])) != 0; const int button = static_cast(right_buttons[i]); SetButton(identifier, button, button_status); } const u16 raw_right_axis_x = static_cast(buffer[9] | ((buffer[10] & 0xf) << 8)); const u16 raw_right_axis_y = static_cast((buffer[10] >> 4) | (buffer[11] << 4)); const f32 right_axis_x = GetAxisValue(raw_right_axis_x, jc.calibration.right_stick.x); const f32 right_axis_y = GetAxisValue(raw_right_axis_y, jc.calibration.right_stick.y); SetAxis(identifier, static_cast(PadAxes::RightStickX), right_axis_x); SetAxis(identifier, static_cast(PadAxes::RightStickY), right_axis_y); if (jc.imu_enabled) { auto right_motion = GetMotionInput(buffer, jc.calibration.imu, jc.accelerometer_sensitivity, jc.gyro_sensitivity); // Rotate motion axis to the correct direction right_motion.accel_x = -right_motion.accel_x; right_motion.accel_y = -right_motion.accel_y; right_motion.gyro_z = -right_motion.gyro_z; SetMotion(identifier, static_cast(PadMotion::RightMotion), right_motion); } } void Joycons::UpdateProPadInput(JoyconData& jc, std::span buffer) { static constexpr std::array pro_buttons{ Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right, Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A, Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus, Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR, }; const auto identifier = GetIdentifier(jc.port); const u32 raw_button = static_cast(buffer[5] | (buffer[3] << 8) | (buffer[4] << 16)); for (std::size_t i = 0; i < pro_buttons.size(); ++i) { const bool button_status = (raw_button & static_cast(pro_buttons[i])) != 0; const int button = static_cast(pro_buttons[i]); SetButton(identifier, button, button_status); } const u16 raw_left_axis_x = static_cast(buffer[6] | ((buffer[7] & 0xf) << 8)); const u16 raw_left_axis_y = static_cast((buffer[7] >> 4) | (buffer[8] << 4)); const u16 raw_right_axis_x = static_cast(buffer[9] | ((buffer[10] & 0xf) << 8)); const u16 raw_right_axis_y = static_cast((buffer[10] >> 4) | (buffer[11] << 4)); const f32 left_axis_x = GetAxisValue(raw_left_axis_x, jc.calibration.left_stick.x); const f32 left_axis_y = GetAxisValue(raw_left_axis_y, jc.calibration.left_stick.y); const f32 right_axis_x = GetAxisValue(raw_right_axis_x, jc.calibration.right_stick.x); const f32 right_axis_y = GetAxisValue(raw_right_axis_y, jc.calibration.right_stick.y); SetAxis(identifier, static_cast(PadAxes::LeftStickX), left_axis_x); SetAxis(identifier, static_cast(PadAxes::LeftStickY), left_axis_y); SetAxis(identifier, static_cast(PadAxes::RightStickX), right_axis_x); SetAxis(identifier, static_cast(PadAxes::RightStickY), right_axis_y); if (jc.imu_enabled) { auto pro_motion = GetMotionInput(buffer, jc.calibration.imu, jc.accelerometer_sensitivity, jc.gyro_sensitivity); pro_motion.accel_y = -pro_motion.accel_y; pro_motion.accel_z = -pro_motion.accel_z; SetMotion(identifier, static_cast(PadMotion::ProMotion), pro_motion); } } f32 Joycons::GetAccelerometerValue(s16 raw, Joycon::ImuSensorCalibration cal, Joycon::AccelerometerSensitivity sen) const { const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4; switch (sen) { case Joycon::AccelerometerSensitivity::G2: return value / 4.0f; case Joycon::AccelerometerSensitivity::G4: return value / 2.0f; case Joycon::AccelerometerSensitivity::G8: return value; case Joycon::AccelerometerSensitivity::G16: return value * 2.0f; } return value; } f32 Joycons::GetGyroValue(s16 raw, Joycon::ImuSensorCalibration cal, Joycon::GyroSensitivity sen) const { const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f; switch (sen) { case Joycon::GyroSensitivity::DPS250: return value / 8.0f; case Joycon::GyroSensitivity::DPS500: return value / 4.0f; case Joycon::GyroSensitivity::DPS1000: return value / 2.0f; case Joycon::GyroSensitivity::DPS2000: return value; } return value; } s16 Joycons::GetRawIMUValues(std::size_t sensor, size_t axis, std::span buffer) const { const size_t offset = (sensor * 6) + (axis * 2); return static_cast(buffer[13 + offset] | (buffer[14 + offset] << 8)); } BasicMotion Joycons::GetMotionInput(std::span buffer, Joycon::ImuCalibration calibration, Joycon::AccelerometerSensitivity accel_sensitivity, Joycon::GyroSensitivity gyro_sensitivity) const { std::array motion_data{}; for (std::size_t sample = 0; sample < motion_data.size(); ++sample) { auto& motion = motion_data[sample]; const auto& accel_cal = calibration.accelerometer; const auto& gyro_cal = calibration.gyro; const std::size_t sample_index = sample * 2; const s16 raw_accel_x = GetRawIMUValues(sample_index, 1, buffer); const s16 raw_accel_y = GetRawIMUValues(sample_index, 0, buffer); const s16 raw_accel_z = GetRawIMUValues(sample_index, 2, buffer); const s16 raw_gyro_x = GetRawIMUValues(sample_index + 1, 1, buffer); const s16 raw_gyro_y = GetRawIMUValues(sample_index + 1, 0, buffer); const s16 raw_gyro_z = GetRawIMUValues(sample_index + 1, 2, buffer); motion.delta_timestamp = 15000; motion.accel_x = GetAccelerometerValue(raw_accel_x, accel_cal[1], accel_sensitivity); motion.accel_y = GetAccelerometerValue(raw_accel_y, accel_cal[0], accel_sensitivity); motion.accel_z = GetAccelerometerValue(raw_accel_z, accel_cal[2], accel_sensitivity); motion.gyro_x = GetGyroValue(raw_gyro_x, gyro_cal[1], gyro_sensitivity); motion.gyro_y = GetGyroValue(raw_gyro_y, gyro_cal[0], gyro_sensitivity); motion.gyro_z = GetGyroValue(raw_gyro_z, gyro_cal[2], gyro_sensitivity); } // TODO(German77): Return all three samples data return motion_data[0]; } Common::Input::VibrationError Joycons::SetRumble(const PadIdentifier& identifier, const Common::Input::VibrationStatus vibration) { const Joycon::VibrationValue native_vibration{ .low_amplitude = vibration.low_amplitude, .low_frequency = vibration.low_frequency, .high_amplitude = vibration.high_amplitude, .high_frequency = vibration.high_amplitude, }; Joycon::SetVibration(GetHandle(identifier), native_vibration); return Common::Input::VibrationError::None; } Joycon::JoyconHandle& Joycons::GetHandle(PadIdentifier identifier) { return joycon_data[identifier.port].joycon_handle; } PadIdentifier Joycons::GetIdentifier(std::size_t port) const { return { .guid = Common::UUID{Common::INVALID_UUID}, .port = port, .pad = 0, }; } std::vector Joycons::GetInputDevices() const { std::vector devices; for (const auto& controller : joycon_data) { if (DeviceConnected(controller.port)) { std::string name = fmt::format("{} {}", JoyconName(controller.port), controller.port); devices.emplace_back(Common::ParamPackage{ {"engine", GetEngineName()}, {"display", std::move(name)}, {"port", std::to_string(controller.port)}, }); } } return devices; } ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) { static constexpr std::array, 20> switch_to_joycon_button = { std::pair{Settings::NativeButton::A, Joycon::PadButton::A}, {Settings::NativeButton::B, Joycon::PadButton::B}, {Settings::NativeButton::X, Joycon::PadButton::X}, {Settings::NativeButton::Y, Joycon::PadButton::Y}, {Settings::NativeButton::DLeft, Joycon::PadButton::Left}, {Settings::NativeButton::DUp, Joycon::PadButton::Up}, {Settings::NativeButton::DRight, Joycon::PadButton::Right}, {Settings::NativeButton::DDown, Joycon::PadButton::Down}, {Settings::NativeButton::SL, Joycon::PadButton::LeftSL}, {Settings::NativeButton::SR, Joycon::PadButton::LeftSR}, {Settings::NativeButton::L, Joycon::PadButton::L}, {Settings::NativeButton::R, Joycon::PadButton::R}, {Settings::NativeButton::ZL, Joycon::PadButton::ZL}, {Settings::NativeButton::ZR, Joycon::PadButton::ZR}, {Settings::NativeButton::Plus, Joycon::PadButton::Plus}, {Settings::NativeButton::Minus, Joycon::PadButton::Minus}, {Settings::NativeButton::Home, Joycon::PadButton::Home}, {Settings::NativeButton::Screenshot, Joycon::PadButton::Capture}, {Settings::NativeButton::LStick, Joycon::PadButton::StickL}, {Settings::NativeButton::RStick, Joycon::PadButton::StickR}, }; if (!params.Has("port")) { return {}; } ButtonMapping mapping{}; for (const auto& [switch_button, joycon_button] : switch_to_joycon_button) { Common::ParamPackage button_params{}; button_params.Set("engine", GetEngineName()); button_params.Set("port", params.Get("port", 0)); button_params.Set("button", static_cast(joycon_button)); mapping.insert_or_assign(switch_button, std::move(button_params)); } return mapping; } AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) { if (!params.Has("port")) { return {}; } AnalogMapping mapping = {}; Common::ParamPackage left_analog_params; left_analog_params.Set("engine", GetEngineName()); left_analog_params.Set("port", params.Get("port", 0)); left_analog_params.Set("axis_x", static_cast(PadAxes::LeftStickX)); left_analog_params.Set("axis_y", static_cast(PadAxes::LeftStickY)); mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params)); Common::ParamPackage right_analog_params; right_analog_params.Set("engine", GetEngineName()); right_analog_params.Set("port", params.Get("port", 0)); right_analog_params.Set("axis_x", static_cast(PadAxes::RightStickX)); right_analog_params.Set("axis_y", static_cast(PadAxes::RightStickY)); mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params)); return mapping; } MotionMapping Joycons::GetMotionMappingForDevice(const Common::ParamPackage& params) { if (!params.Has("port")) { return {}; } MotionMapping mapping = {}; Common::ParamPackage left_motion_params; left_motion_params.Set("engine", GetEngineName()); left_motion_params.Set("port", params.Get("port", 0)); left_motion_params.Set("motion", 0); mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params)); Common::ParamPackage right_Motion_params; right_Motion_params.Set("engine", GetEngineName()); right_Motion_params.Set("port", params.Get("port", 0)); right_Motion_params.Set("motion", 1); mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params)); return mapping; } Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const { const auto button = static_cast(params.Get("button", 0)); switch (button) { case Joycon::PadButton::Left: return Common::Input::ButtonNames::ButtonLeft; case Joycon::PadButton::Right: return Common::Input::ButtonNames::ButtonRight; case Joycon::PadButton::Down: return Common::Input::ButtonNames::ButtonDown; case Joycon::PadButton::Up: return Common::Input::ButtonNames::ButtonUp; case Joycon::PadButton::LeftSL: case Joycon::PadButton::RightSL: return Common::Input::ButtonNames::TriggerSL; case Joycon::PadButton::LeftSR: case Joycon::PadButton::RightSR: return Common::Input::ButtonNames::TriggerSR; case Joycon::PadButton::L: return Common::Input::ButtonNames::TriggerL; case Joycon::PadButton::R: return Common::Input::ButtonNames::TriggerR; case Joycon::PadButton::ZL: return Common::Input::ButtonNames::TriggerZL; case Joycon::PadButton::ZR: return Common::Input::ButtonNames::TriggerZR; case Joycon::PadButton::A: return Common::Input::ButtonNames::ButtonA; case Joycon::PadButton::B: return Common::Input::ButtonNames::ButtonB; case Joycon::PadButton::X: return Common::Input::ButtonNames::ButtonX; case Joycon::PadButton::Y: return Common::Input::ButtonNames::ButtonY; case Joycon::PadButton::Plus: return Common::Input::ButtonNames::ButtonPlus; case Joycon::PadButton::Minus: return Common::Input::ButtonNames::ButtonMinus; case Joycon::PadButton::Home: return Common::Input::ButtonNames::ButtonHome; case Joycon::PadButton::Capture: return Common::Input::ButtonNames::ButtonCapture; case Joycon::PadButton::StickL: return Common::Input::ButtonNames::ButtonStickL; case Joycon::PadButton::StickR: return Common::Input::ButtonNames::ButtonStickR; default: return Common::Input::ButtonNames::Undefined; } } Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const { if (params.Has("button")) { return GetUIButtonName(params); } if (params.Has("axis")) { return Common::Input::ButtonNames::Value; } if (params.Has("motion")) { return Common::Input::ButtonNames::Engine; } return Common::Input::ButtonNames::Invalid; } } // namespace InputCommon