diff --git a/.gitmodules b/.gitmodules index 41022615bd..732123b804 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,6 +34,9 @@ [submodule "xbyak"] path = externals/xbyak url = https://github.com/herumi/xbyak.git +[submodule "externals/hidapi"] + path = externals/hidapi + url = https://github.com/german77/hidapi.git [submodule "opus"] path = externals/opus/opus url = https://github.com/xiph/opus.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 27aa567808..d757706d3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -384,6 +384,10 @@ if (NOT LIBUSB_FOUND) set(LIBUSB_LIBRARIES usb) endif() +find_package(HIDAPI) + add_subdirectory(externals/hidapi) + set(HIDAPI_LIBRARIES hidapi) + # Use system installed ffmpeg. if (NOT MSVC) find_package(FFmpeg REQUIRED) diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index 38ab318985..e5d909ff74 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -21,6 +21,10 @@ add_library(input_common STATIC mouse/mouse_input.h mouse/mouse_poller.cpp mouse/mouse_poller.h + joycon/jc_adapter.cpp + joycon/jc_adapter.h + joycon/jc_poller.cpp + joycon/jc_poller.h sdl/sdl.cpp sdl/sdl.h udp/client.cpp @@ -76,5 +80,7 @@ endif() target_include_directories(input_common SYSTEM PRIVATE ${LIBUSB_INCLUDE_DIR}) target_link_libraries(input_common PRIVATE ${LIBUSB_LIBRARIES}) +target_link_libraries(input_common PUBLIC ${HIDAPI_LIBRARIES}) + create_target_directory_groups(input_common) target_link_libraries(input_common PUBLIC core PRIVATE common Boost::boost) diff --git a/src/input_common/joycon/jc_adapter.cpp b/src/input_common/joycon/jc_adapter.cpp new file mode 100644 index 0000000000..3c615e2cf2 --- /dev/null +++ b/src/input_common/joycon/jc_adapter.cpp @@ -0,0 +1,974 @@ +// Copyright 2020 Yuzu Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include "common/logging/log.h" +#include "input_common/joycon/jc_adapter.h" + +namespace JCAdapter { + +/// Used to loop through and assign button in poller +constexpr std::array PadButtonArray{ + PadButton::BUTTON_DOWN, PadButton::BUTTON_UP, PadButton::BUTTON_RIGHT, + PadButton::BUTTON_LEFT, PadButton::BUTTON_L_SR, PadButton::BUTTON_L_SL, + PadButton::TRIGGER_L, PadButton::TRIGGER_ZL, PadButton::BUTTON_Y, + PadButton::BUTTON_X, PadButton::BUTTON_B, PadButton::BUTTON_A, + PadButton::BUTTON_R_SL, PadButton::BUTTON_R_SR, PadButton::TRIGGER_R, + PadButton::TRIGGER_ZR, PadButton::BUTTON_MINUS, PadButton::BUTTON_PLUS, + PadButton::STICK_R, PadButton::STICK_L, PadButton::BUTTON_HOME, + PadButton::BUTTON_CAPTURE, +}; + +Joycons::Joycons() { + LOG_INFO(Input, "JC Adapter Initialization started"); + global_counter = 0; + adapter_thread_running = false; + const int init_res = hid_init(); + if (init_res == 0) { + Setup(); + } else { + LOG_ERROR(Input, "HIDapi could not be initialized. failed with error = {}", init_res); + } +} + +void Joycons::SendWrite(hid_device* handle, std::vector buffer, int size) { + hid_write(handle, buffer.data(), size); +} + +u8 Joycons::GetCounter() { + const u8 counter = global_counter; + global_counter = (global_counter + 1) & 0x0F; + return counter; +} + +std::vector Joycons::SubCommand(hid_device* handle, SubComamnd sc, std::vector buffer, + int size) { + std::vector local_buffer(size + 11); + + local_buffer[0] = static_cast(Output::RUMBLE_AND_SUBCMD); + local_buffer[1] = GetCounter(); + for (int i = 0; i < 8; ++i) { + local_buffer[i + 2] = default_buffer[i]; + } + local_buffer[10] = static_cast(sc); + for (int i = 0; i < size; ++i) { + local_buffer[11 + i] = buffer[i]; + } + + SendWrite(handle, local_buffer, size + 11); + return GetResponse(handle, sc); +} + +std::vector Joycons::GetResponse(hid_device* handle, SubComamnd sc) { + int tries = 0; + std::vector buffer(max_resp_size); + do { + int result = hid_read_timeout(handle, buffer.data(), max_resp_size, 100); + if (result < 1) { + LOG_ERROR(Input, "No response from joystick"); + } + tries++; + } while (tries < 10 && buffer[0] != 0x21 && buffer[14] != static_cast(sc)); + return buffer; +} + +std::vector Joycons::ReadSPI(hid_device* handle, CalAddr addr, u8 size) { + std::vector buffer = {0x00, 0x00, 0x00, 0x00, size}; + std::vector local_buffer(size + 20); + + buffer[0] = static_cast(static_cast(addr) & 0x00FF); + buffer[1] = static_cast((static_cast(addr) & 0xFF00) >> 8); + for (int i = 0; i < 100; ++i) { + local_buffer = SubCommand(handle, SubComamnd::SPI_FLASH_READ, buffer, 5); + if (local_buffer[15] == buffer[0] && local_buffer[16] == buffer[1]) { + break; + } + } + return std::vector(local_buffer.begin() + 20, local_buffer.end()); +} + +void Joycons::SetJoyStickCal(std::vector buffer, JoyStick& axis1, JoyStick& axis2, bool left) { + if (left) { + axis1.max = (u16)(((buffer[1] & 0x0F) << 8) | buffer[0]); + axis2.max = (u16)((buffer[2] << 4) | (buffer[1] >> 4)); + axis1.center = (u16)(((buffer[4] & 0x0F) << 8) | buffer[3]); + axis2.center = (u16)((buffer[5] << 4) | (buffer[4] >> 4)); + axis1.min = (u16)(((buffer[7] & 0x0F) << 8) | buffer[6]); + axis2.min = (u16)((buffer[8] << 4) | (buffer[7] >> 4)); + } else { + axis1.center = (u16)(((buffer[1] & 0x0F) << 8) | buffer[0]); + axis2.center = (u16)((buffer[2] << 4) | (buffer[1] >> 4)); + axis1.min = (u16)(((buffer[4] & 0x0F) << 8) | buffer[3]); + axis2.min = (u16)((buffer[5] << 4) | (buffer[4] >> 4)); + axis1.max = (u16)(((buffer[7] & 0x0F) << 8) | buffer[6]); + axis2.max = (u16)((buffer[8] << 4) | (buffer[7] >> 4)); + } + + /* Nintendo fix for drifting stick + buffer = ReadSPI(0x60, (isLeft ? 0x86 : 0x98), 16); + joycon[0].joystick.deadzone = (u16)((buffer[4] << 8) & 0xF00 | buffer[3]);*/ +} + +void Joycons::SetImuCal(Joycon& jc, std::vector buffer) { + for (size_t i = 0; i < jc.imu.size(); ++i) { + jc.imu[i].acc.offset = (u16)(buffer[0 + (i * 2)] | (buffer[1 + (i * 2)] << 8)); + jc.imu[i].acc.scale = (u16)(buffer[0 + 6 + (i * 2)] | (buffer[1 + 6 + (i * 2)] << 8)); + jc.imu[i].gyr.offset = (u16)(buffer[0 + 12 + (i * 2)] | (buffer[1 + 12 + (i * 2)] << 8)); + jc.imu[i].gyr.scale = (u16)(buffer[0 + 18 + (i * 2)] | (buffer[1 + 18 + (i * 2)] << 8)); + } +} + +void Joycons::GetUserCalibrationData(Joycon& jc) { + std::vector buffer; + hid_set_nonblocking(jc.handle, 0); + + switch (jc.type) { + case JoyControllerTypes::Left: + buffer = ReadSPI(jc.handle, CalAddr::USER_LEFT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[0], jc.axis[1], true); + break; + case JoyControllerTypes::Right: + buffer = ReadSPI(jc.handle, CalAddr::USER_RIGHT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[2], jc.axis[3], false); + break; + case JoyControllerTypes::Pro: + buffer = ReadSPI(jc.handle, CalAddr::USER_LEFT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[0], jc.axis[1], true); + buffer = ReadSPI(jc.handle, CalAddr::USER_RIGHT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[2], jc.axis[3], false); + break; + } + + buffer = ReadSPI(jc.handle, CalAddr::USER_IMU_DATA, 24); + SetImuCal(jc, buffer); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::GetFactoryCalibrationData(Joycon& jc) { + std::vector buffer; + hid_set_nonblocking(jc.handle, 0); + + switch (jc.type) { + case JoyControllerTypes::Left: + buffer = ReadSPI(jc.handle, CalAddr::FACT_LEFT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[0], jc.axis[1], true); + break; + case JoyControllerTypes::Right: + buffer = ReadSPI(jc.handle, CalAddr::FACT_RIGHT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[2], jc.axis[3], false); + break; + case JoyControllerTypes::Pro: + buffer = ReadSPI(jc.handle, CalAddr::FACT_LEFT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[0], jc.axis[1], true); + buffer = ReadSPI(jc.handle, CalAddr::FACT_RIGHT_DATA, 9); + SetJoyStickCal(buffer, jc.axis[2], jc.axis[3], false); + break; + } + + buffer = ReadSPI(jc.handle, CalAddr::FACT_IMU_DATA, 24); + SetImuCal(jc, buffer); + hid_set_nonblocking(jc.handle, 1); +} + +s16 Joycons::GetRawIMUValues(size_t sensor, size_t axis, std::vector buffer) { + const size_t offset = (sensor * 6) + (axis * 2); + return static_cast(buffer[13 + offset] | (buffer[14 + offset] << 8)); +} + +f32 Joycons::TransformAccValue(s16 raw, ImuData cal, AccSensitivity sen) { + // const f32 value = (raw - cal.offset) * cal.scale / 65535.0f / 1000.0f; + const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4; + switch (sen) { + case AccSensitivity::G2: + return value / 4.0f; + case AccSensitivity::G4: + return value / 2.0f; + case AccSensitivity::G8: + return value; + case AccSensitivity::G16: + return value * 2.0f; + } + return value; +} + +f32 Joycons::TransformGyrValue(s16 raw, ImuData cal, GyrSensitivity sen) { + // const f32 value = (raw - cal.offset) * cal.scale / 65535.0f / 360.0f / 3.8f; + const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f; + switch (sen) { + case GyrSensitivity::DPS250: + return value / 8.0f; + case GyrSensitivity::DPS500: + return value / 4.0f; + case GyrSensitivity::DPS1000: + return value / 2.0f; + case GyrSensitivity::DPS2000: + return value; + } + return value; +} + +void Joycons::GetIMUValues(Joycon& jc, std::vector buffer) { + for (size_t i = 0; i < jc.imu.size(); ++i) { + jc.imu[i].gyr.value = 0; + jc.imu[i].acc.value = 0; + } + for (size_t i = 0; i < jc.imu.size(); ++i) { + for (size_t sample = 0; sample < 3; ++sample) { + const s16 raw_gyr = GetRawIMUValues((sample * 2) + 1, i, buffer); + const s16 raw_acc = GetRawIMUValues(sample * 2, i, buffer); + switch (i) { + case 0: + jc.gyro[sample].y = TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen); + jc.imu[1].gyr.value += TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen) * 0.3333f; + jc.imu[1].acc.value += TransformAccValue(raw_acc, jc.imu[i].acc, jc.asen) * 0.3333f; + break; + case 1: + jc.gyro[sample].x = TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen); + jc.imu[0].gyr.value += TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen) * 0.3333f; + jc.imu[0].acc.value += TransformAccValue(raw_acc, jc.imu[i].acc, jc.asen) * 0.3333f; + break; + case 2: + jc.gyro[sample].z = TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen); + jc.imu[2].gyr.value += TransformGyrValue(raw_gyr, jc.imu[i].gyr, jc.gsen) * 0.3333f; + jc.imu[2].acc.value += TransformAccValue(raw_acc, jc.imu[i].acc, jc.asen) * 0.3333f; + break; + } + } + } +} + +void Joycons::SetImuConfig(Joycon& jc, GyrSensitivity gsen, GyrPerformance gfrec, + AccSensitivity asen, AccPerformance afrec) { + hid_set_nonblocking(jc.handle, 0); + jc.gsen = gsen; + jc.gfrec = gfrec; + jc.asen = asen; + jc.afrec = afrec; + const std::vector buffer{static_cast(gsen), static_cast(asen), + static_cast(gfrec), static_cast(afrec)}; + SubCommand(jc.handle, SubComamnd::SET_IMU_SENSITIVITY, buffer, 4); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::GetLeftPadInput(Joycon& jc, std::vector buffer) { + jc.button = static_cast(buffer[5]); + jc.button |= static_cast((buffer[4] & 0b00101001) << 16); + jc.axis[0].value = static_cast(buffer[6] | ((buffer[7] & 0xf) << 8)); + jc.axis[1].value = static_cast((buffer[7] >> 4) | (buffer[8] << 4)); +} + +void Joycons::GetRightPadInput(Joycon& jc, std::vector buffer) { + jc.button = static_cast(buffer[3] << 8); + jc.button |= static_cast((buffer[4] & 0b00010110) << 16); + jc.axis[2].value = static_cast(buffer[9] | ((buffer[10] & 0xf) << 8)); + jc.axis[3].value = static_cast((buffer[10] >> 4) | (buffer[11] << 4)); +} + +void Joycons::GetProPadInput(Joycon& jc, std::vector buffer) { + jc.button = static_cast(buffer[5] & 0b11001111); + jc.button |= static_cast((buffer[3] & 0b11001111) << 8); + jc.button |= static_cast((buffer[4] & 0b00111111) << 16); + jc.axis[0].value = static_cast(buffer[6] | ((buffer[7] & 0xf) << 8)); + jc.axis[1].value = static_cast((buffer[7] >> 4) | (buffer[8] << 4)); + jc.axis[2].value = static_cast(buffer[9] | ((buffer[10] & 0xf) << 8)); + jc.axis[3].value = static_cast((buffer[10] >> 4) | (buffer[11] << 4)); +} + +void Joycons::SetRumble(int port, f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) { + if (!DeviceConnected(port) || !joycon[port].rumble_enabled) { + return; + } + freq_high = std::clamp(freq_high, 81.75177f, 1252.572266f); + freq_low = std::clamp(freq_low, 40.875885f, 626.286133f); + amp_high = std::clamp(amp_high, 0.0f, 1.0f); + amp_low = std::clamp(amp_low, 0.0f, 1.0f); + if (freq_high != joycon[port].hd_rumble.freq_high || + freq_low != joycon[port].hd_rumble.freq_low || + amp_high != joycon[port].hd_rumble.amp_high || amp_low != joycon[port].hd_rumble.amp_low) { + joycon[port].hd_rumble.freq_high = freq_high; + joycon[port].hd_rumble.freq_low = freq_low; + joycon[port].hd_rumble.amp_high = amp_high; + joycon[port].hd_rumble.amp_low = amp_low; + SendRumble(port); + } +} + +void Joycons::SendRumble(int port) { + std::vector buffer(max_resp_size); + + buffer[port] = static_cast(Output::RUMBLE_ONLY); + buffer[1] = GetCounter(); + for (int i = 0; i < 8; ++i) { + buffer[2 + i] = default_buffer[i]; + } + + if (joycon[port].hd_rumble.amp_low > 0 || joycon[port].hd_rumble.amp_high > 0) { + u16 encoded_hf = + static_cast(roundf(128 * log2f(joycon[port].hd_rumble.freq_high * 0.1f)) - 0x180); + u8 encoded_lf = + static_cast(roundf(32 * log2f(joycon[port].hd_rumble.freq_low * 0.1f)) - 0x40); + u8 encoded_hamp = + static_cast(EncodeRumbleAmplification(joycon[port].hd_rumble.amp_high)); + u8 encoded_lamp = + static_cast(EncodeRumbleAmplification(joycon[port].hd_rumble.amp_low)); + + for (int i = 0; i < 2; ++i) { + const u8 amplitude = i == 0 ? encoded_lamp : encoded_hamp; + const u8 offset = i * 4; + u32 encoded_amp = amplitude >> 1; + const u32 parity = (encoded_amp % 2) * 0x80; + encoded_amp >>= 1; + encoded_amp += 0x40; + + buffer[2 + offset] = static_cast(encoded_hf & 0xff); + buffer[3 + offset] = static_cast((encoded_hf >> 8) & 0xff); + buffer[4 + offset] = static_cast(encoded_lf & 0xff); + + buffer[3 + offset] |= static_cast(amplitude); + buffer[4 + offset] |= static_cast(parity); + buffer[5 + offset] = static_cast(encoded_amp); + } + } + + SendWrite(joycon[port].handle, buffer, max_resp_size); +} + +const f32 Joycons::EncodeRumbleAmplification(f32 amplification) { + if (amplification < 0.01182) { + return roundf(pow(amplification, 1.7f) * 7561); + } else if (amplification < 0.11249) { + return roundf((log(amplification) * 11.556f) + 55.3f); + } else if (amplification < 0.22498) { + return roundf((log2f(amplification) * 32) + 131); + } + + return roundf((log2f(amplification) * 64) + 200); +} + +const f32 Joycons::GetTemperatureCelcius(int port) { + if (!DeviceConnected(port)) { + return 0.0f; + } + return 25.0f + joycon[port].temperature * 0.0625f; +} + +const f32 Joycons::GetTemperatureFahrenheit(int port) { + if (!DeviceConnected(port)) { + return 0.0f; + } + return GetTemperatureCelcius(port) * 1.8f + 32; +} + +const u8 Joycons::GetBatteryLevel(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].battery; +} + +const std::array Joycons::GetSerialNumber(int port) { + if (!DeviceConnected(port)) { + return {}; + } + return joycon[port].serial_number; +} + +const f32 Joycons::GetVersion(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].version; +} + +const JoyControllerTypes Joycons::GetDeviceType(int port) { + if (!DeviceConnected(port)) { + return JoyControllerTypes::None; + } + return joycon[port].type; +} + +const std::array Joycons::GetMac(int port) { + if (!DeviceConnected(port)) { + return {}; + } + return joycon[port].mac; +} + +const u32 Joycons::GetBodyColor(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].color.body; +} + +const u32 Joycons::GetButtonColor(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].color.buttons; +} + +const u32 Joycons::GetLeftGripColor(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].color.left_grip; +} +const u32 Joycons::GetRightGripColor(int port) { + if (!DeviceConnected(port)) { + return 0x0; + } + return joycon[port].color.right_grip; +} + +void Joycons::SetSerialNumber(Joycon& jc) { + std::vector buffer; + hid_set_nonblocking(jc.handle, 0); + buffer = ReadSPI(jc.handle, CalAddr::SERIAL_NUMBER, 16); + for (int i = 0; i < 15; i++) { + jc.serial_number[i] = buffer[i + 1]; + } + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::SetDeviceType(Joycon& jc) { + std::vector buffer; + hid_set_nonblocking(jc.handle, 0); + buffer = ReadSPI(jc.handle, CalAddr::DEVICE_TYPE, 1); + jc.type = static_cast(buffer[0]); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::GetColor(Joycon& jc) { + std::vector buffer; + hid_set_nonblocking(jc.handle, 0); + buffer = ReadSPI(jc.handle, CalAddr::COLOR_DATA, 12); + jc.color.body = (buffer[2] << 16) | (buffer[1] << 8) | buffer[0]; + jc.color.buttons = (buffer[5] << 16) | (buffer[4] << 8) | buffer[3]; + jc.color.left_grip = (buffer[8] << 16) | (buffer[7] << 8) | buffer[6]; + jc.color.right_grip = (buffer[11] << 16) | (buffer[10] << 8) | buffer[9]; + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::SetMac(Joycon& jc) { + wchar_t mac[255]; + hid_get_serial_number_string(jc.handle, mac, std::size(mac)); + for (int i = 0; i < 6; ++i) { + wchar_t value[3] = {mac[i * 2], mac[(i * 2) + 1]}; + jc.mac[i] = std::stoi(value, 0, 16); + } +} + +void Joycons::SetVersionNumber(Joycon& jc) { + jc.version = 0.0f; +} + +void Joycons::SetLedConfig(Joycon& jc, u8 leds) { + hid_set_nonblocking(jc.handle, 0); + jc.leds = leds; + const std::vector buffer{leds}; + SubCommand(jc.handle, SubComamnd::SET_PLAYER_LIGHTS, buffer, 1); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::EnableImu(Joycon& jc, bool enable) { + hid_set_nonblocking(jc.handle, 0); + jc.imu_enabled = enable; + const std::vector buffer{static_cast(enable ? 1 : 0)}; + SubCommand(jc.handle, SubComamnd::ENABLE_IMU, buffer, 1); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::EnableRumble(Joycon& jc, bool enable) { + hid_set_nonblocking(jc.handle, 0); + jc.rumble_enabled = enable; + const std::vector buffer{static_cast(enable ? 1 : 0)}; + SubCommand(jc.handle, SubComamnd::ENABLE_VIBRATION, buffer, 1); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::SetReportMode(Joycon& jc, ReportMode mode) { + hid_set_nonblocking(jc.handle, 0); + jc.mode = mode; + const std::vector buffer{static_cast(mode)}; + SubCommand(jc.handle, SubComamnd::SET_REPORT_MODE, buffer, 1); + hid_set_nonblocking(jc.handle, 1); +} + +void Joycons::UpdateJoyconData(Joycon& jc, std::vector buffer) { + switch (jc.type) { + case JoyControllerTypes::Left: + GetLeftPadInput(jc, buffer); + if (jc.imu_enabled) { + GetIMUValues(jc, buffer); + jc.imu[1].acc.value = -jc.imu[1].acc.value; + jc.imu[0].gyr.value = -jc.imu[0].gyr.value; + jc.gyro[0].x = -jc.gyro[0].x; + jc.gyro[1].x = -jc.gyro[1].x; + jc.gyro[2].x = -jc.gyro[2].x; + + jc.imu[2].acc.value = -jc.imu[2].acc.value; + } + break; + case JoyControllerTypes::Right: + GetRightPadInput(jc, buffer); + if (jc.imu_enabled) { + GetIMUValues(jc, buffer); + jc.imu[0].acc.value = -jc.imu[0].acc.value; + jc.imu[1].acc.value = -jc.imu[1].acc.value; + jc.imu[2].gyr.value = -jc.imu[2].gyr.value; + jc.gyro[0].z = -jc.gyro[0].z; + jc.gyro[1].z = -jc.gyro[1].z; + jc.gyro[2].z = -jc.gyro[2].z; + } + break; + case JoyControllerTypes::Pro: + GetProPadInput(jc, buffer); + if (jc.imu_enabled) { + GetIMUValues(jc, buffer); + jc.imu[1].acc.value = -jc.imu[1].acc.value; + jc.imu[2].acc.value = -jc.imu[2].acc.value; + } + break; + } + const auto now = std::chrono::system_clock::now(); + u64 difference = + std::chrono::duration_cast(now - jc.last_motion_update).count(); + jc.last_motion_update = now; + Common::Vec3f acceleration = + Common::Vec3f(jc.imu[0].acc.value, jc.imu[1].acc.value, jc.imu[2].acc.value); + jc.motion->SetAcceleration(acceleration); + for (int i = 0; i < 3; i++) { + jc.motion->SetGyroscope(jc.gyro[i]); + jc.motion->UpdateRotation(difference / 3); + jc.motion->UpdateOrientation(difference / 3); + } + jc.battery = buffer[2] >> 4; +} + +void Joycons::UpdateYuzuSettings(Joycon& jc, int port) { + if (DeviceConnected(port) && configuring) { + JCPadStatus pad; + if (jc.button != 0) { + pad.button = jc.button; + pad_queue[port].Push(pad); + } + for (size_t i = 0; i < jc.axis.size(); ++i) { + const u16 value = jc.axis[i].value; + const u16 origin = jc.axis[i].center; + if (value != 0) { + if (value > origin + 500 || value < origin - 500) { + pad.axis = static_cast(i); + pad.axis_value = value - origin; + pad_queue[port].Push(pad); + } + } + } + for (size_t i = 0; i < jc.imu.size(); ++i) { + const f32 value = jc.imu[i].gyr.value; + const f32 value2 = jc.imu[i].acc.value; + if (value > 6.0f || value < -6.0f) { + pad.motion = static_cast(i); + pad.motion_value = value; + pad_queue[port].Push(pad); + } + if (value2 > 2.0f || value2 < -2.0f) { + pad.motion = static_cast(i + 3); + pad.motion_value = value; + pad_queue[port].Push(pad); + } + } + } +} + +void Joycons::JoyconToState(Joycon& jc, JCState& state) { + for (const auto& button : PadButtonArray) { + const u32 button_value = static_cast(button); + state.buttons.insert_or_assign(button_value, jc.button & button_value); + } + + for (size_t i = 0; i < jc.axis.size(); ++i) { + f32 axis_value = jc.axis[i].value - jc.axis[i].center; + if (axis_value > 0) { + axis_value = axis_value / jc.axis[i].max; + } else { + axis_value = axis_value / jc.axis[i].min; + } + axis_value = std::clamp(axis_value, -1.0f, 1.0f); + state.axes.insert_or_assign(static_cast(i), axis_value); + } + + Common::Vec3f gyroscope = jc.motion->GetGyroscope(); + Common::Vec3f accelerometer = jc.motion->GetAcceleration(); + Common::Vec3f rotation = jc.motion->GetRotations(); + std::array orientation = jc.motion->GetOrientation(); + + state.motion.insert_or_assign(static_cast(0), gyroscope.x); + state.motion.insert_or_assign(static_cast(1), gyroscope.y); + state.motion.insert_or_assign(static_cast(2), gyroscope.z); + + state.motion.insert_or_assign(static_cast(3), accelerometer.x); + state.motion.insert_or_assign(static_cast(4), accelerometer.y); + state.motion.insert_or_assign(static_cast(5), accelerometer.z); + + state.motion.insert_or_assign(static_cast(6), rotation.x); + state.motion.insert_or_assign(static_cast(7), rotation.y); + state.motion.insert_or_assign(static_cast(8), rotation.z); + + state.motion.insert_or_assign(static_cast(9), orientation[0].x); + state.motion.insert_or_assign(static_cast(10), orientation[0].y); + state.motion.insert_or_assign(static_cast(11), orientation[0].z); + state.motion.insert_or_assign(static_cast(12), orientation[1].x); + state.motion.insert_or_assign(static_cast(13), orientation[1].y); + state.motion.insert_or_assign(static_cast(14), orientation[1].z); + state.motion.insert_or_assign(static_cast(15), orientation[2].x); + state.motion.insert_or_assign(static_cast(16), orientation[2].y); + state.motion.insert_or_assign(static_cast(17), orientation[2].z); + + if (jc.button & 0x1 || jc.button & 0x100) { + jc.motion->ResetRotations(); + jc.motion->SetQuaternion({{0, 0, -1}, 0}); + } +} + +void Joycons::ReadLoop() { + LOG_DEBUG(Input, "JC Adapter Read() thread started"); + + std::vector buffer(max_resp_size); + + while (adapter_thread_running) { + for (int port = 0; port < joycon.size(); ++port) { + if (joycon[port].type != JoyControllerTypes::None) { + + const int status = + hid_read_timeout(joycon[port].handle, buffer.data(), max_resp_size, 15); + if (status > 0) { + if (buffer[0] == 0x00) { + // Invalid response + LOG_ERROR(Input, "error reading buffer"); + adapter_thread_running = false; + joycon[port].type = JoyControllerTypes::None; + break; + } + UpdateJoyconData(joycon[port], buffer); + UpdateYuzuSettings(joycon[port], port); + JoyconToState(joycon[port], joycon[port].state); + /* + printf("ctrl:%d\tbtn:%d\tx1:%d\ty1:%d\tx2:%d\ty2:%d\tax:%.3f\tay:%.3f\taz:%.3f\tgx:%.3f\tgy:%.3f, + " "gz:%.3f\tbuffer:",port, joycon[port].button, joycon[port].axis[0].value - + joycon[port].axis[0].center, joycon[port].axis[1].value - + joycon[port].axis[1].center, joycon[port].axis[2].value - + joycon[port].axis[2].center, joycon[port].axis[3].value - + joycon[port].axis[3].center, joycon[port].imu[0].acc.value, + joycon[port].imu[1].acc.value, joycon[port].imu[2].acc.value, + joycon[port].imu[0].gyr.value, joycon[port].imu[1].gyr.value, + joycon[port].imu[2].gyr.value); + for (int i = 0; i < 7; i++) { + printf("%02hx ", buffer[i]); + } + printf("\n");*/ + } + } + } + std::this_thread::yield(); + } +} + +void Joycons::Setup() { + // Initialize all controllers as unplugged + for (size_t port = 0; port < joycon.size(); ++port) { + joycon[port].type = JoyControllerTypes::None; + for (size_t i = 0; i < joycon[port].axis.size(); ++i) { + joycon[port].axis[i].value = 0; + } + for (size_t i = 0; i < joycon[port].imu.size(); ++i) { + joycon[port].imu[i].acc.value = 0; + joycon[port].imu[i].gyr.value = 0; + } + joycon[port].hd_rumble.amp_high = 0; + joycon[port].hd_rumble.amp_low = 0; + joycon[port].hd_rumble.freq_high = 160.0f; + joycon[port].hd_rumble.freq_low = 80.0f; + } + int port = 0; + + hid_device_info* devs = hid_enumerate(0x057e, 0x2006); + hid_device_info* cur_dev = devs; + + while (cur_dev) { + printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", + cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); + printf(" Product: %ls\n", cur_dev->product_string); + printf(" Release: %hx\n", cur_dev->release_number); + printf(" Interface: %d\n", cur_dev->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", cur_dev->usage, cur_dev->usage_page); + printf("\n"); + + if (CheckDeviceAccess(port, cur_dev)) { + // JC Adapter found and accessible, registering it + ++port; + } + cur_dev = cur_dev->next; + } + + devs = hid_enumerate(0x057e, 0x2007); + cur_dev = devs; + + while (cur_dev) { + printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", + cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); + printf(" Product: %ls\n", cur_dev->product_string); + printf(" Release: %hx\n", cur_dev->release_number); + printf(" Interface: %d\n", cur_dev->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", cur_dev->usage, cur_dev->usage_page); + printf("\n"); + + if (CheckDeviceAccess(port, cur_dev)) { + // JC Adapter found and accessible, registering it + ++port; + } + cur_dev = cur_dev->next; + } //*/ + /* + devs = hid_enumerate(0x057e, 0x2009); + cur_dev = devs; + + while (cur_dev) { + printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", + cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); + printf(" Product: %ls\n", cur_dev->product_string); + printf(" Release: %hx\n", cur_dev->release_number); + printf(" Interface: %d\n", cur_dev->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", cur_dev->usage, cur_dev->usage_page); + printf("\n"); + + if (CheckDeviceAccess(port, cur_dev)) { + // JC Adapter found and accessible, registering it + ++port; + } + cur_dev = cur_dev->next; + } + */ + GetJCEndpoint(); +} + +bool Joycons::CheckDeviceAccess(int port, hid_device_info* device) { + if (device->vendor_id != 0x057e || + !(device->product_id == 0x2006 || device->product_id == 0x2007 || + device->product_id == 0x2009)) { + // This isn't the device we are looking for. + return false; + } + joycon[port].handle = hid_open(device->vendor_id, device->product_id, device->serial_number); + if (!joycon[port].handle) { + LOG_ERROR(Input, "Yuzu can not gain access to this device: ID {:04X}:{:04X}.", + device->vendor_id, device->product_id); + return false; + } + hid_set_nonblocking(joycon[port].handle, 1); + switch (device->product_id) { + case 0x2006: + joycon[port].type = JoyControllerTypes::Left; + break; + case 0x2007: + joycon[port].type = JoyControllerTypes::Right; + break; + case 0x2009: + joycon[port].type = JoyControllerTypes::Pro; + break; + } + return true; +} + +void Joycons::GetJCEndpoint() { + // init joycons + for (u8 port = 0; port < joycon.size(); ++port) { + joycon[port].port = port; + if (joycon[port].type != JoyControllerTypes::None) { + SetMac(joycon[port]); + SetSerialNumber(joycon[port]); + SetVersionNumber(joycon[port]); + SetDeviceType(joycon[port]); + GetFactoryCalibrationData(joycon[port]); + GetColor(joycon[port]); + + SetLedConfig(joycon[port], port + 1); + EnableImu(joycon[port], true); + SetImuConfig(joycon[port], GyrSensitivity::DPS2000, GyrPerformance::HZ833, + AccSensitivity::G8, AccPerformance::HZ100); + EnableRumble(joycon[port], true); + SetReportMode(joycon[port], ReportMode::STANDARD_FULL_60HZ); + joycon[port].motion = new InputCommon::MotionInput(0.3f, 0.005f, 0.0f); + joycon[port].motion->SetGyroThreshold(0.001f); + // joycon[port].motion->EnableReset(false); + } + } + + adapter_thread_running = true; + adapter_input_thread = std::thread(&Joycons::ReadLoop, this); // Read input +} + +Joycons::~Joycons() { + Reset(); +} + +void Joycons::Reset() { + if (adapter_thread_running) { + adapter_thread_running = false; + } + if (adapter_input_thread.joinable()) { + adapter_input_thread.join(); + } + + for (u8 port = 0; port < joycon.size(); ++port) { + joycon[port].type = JoyControllerTypes::None; + if (joycon[port].handle) { + joycon[port].handle = nullptr; + } + } + + hid_exit(); +} +std::string Joycons::JoyconName(std::size_t port) const { + switch (joycon[port].type) { + case JoyControllerTypes::Left: + return "Left Joycon"; + case JoyControllerTypes::Right: + return "Right Joycon"; + case JoyControllerTypes::Pro: + return "Pro Controller"; + } + return "Unknow Joycon"; +} + +std::vector Joycons::GetInputDevices() const { + // std::scoped_lock lock(joystick_map_mutex); + std::vector devices; + for (const auto& controller : joycon) { + if (DeviceConnected(controller.port)) { + std::string name = fmt::format("{} {}", JoyconName(controller.port), controller.port); + devices.emplace_back(Common::ParamPackage{ + {"class", "jcpad"}, + {"display", std::move(name)}, + {"port", std::to_string(controller.port)}, + }); + } + } + return devices; +} + +InputCommon::ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) { + // This list is missing ZL/ZR since those are not considered buttons in SDL GameController. + // We will add those afterwards + // This list also excludes Screenshot since theres not really a mapping for that + static constexpr std::array, 20> + switch_to_jcadapter_button = { + std::pair{Settings::NativeButton::A, PadButton::BUTTON_B}, + {Settings::NativeButton::B, PadButton::BUTTON_A}, + {Settings::NativeButton::X, PadButton::BUTTON_Y}, + {Settings::NativeButton::Y, PadButton::BUTTON_X}, + {Settings::NativeButton::LStick, PadButton::STICK_L}, + {Settings::NativeButton::RStick, PadButton::STICK_R}, + {Settings::NativeButton::L, PadButton::TRIGGER_L}, + {Settings::NativeButton::R, PadButton::TRIGGER_R}, + {Settings::NativeButton::Plus, PadButton::BUTTON_PLUS}, + {Settings::NativeButton::Minus, PadButton::BUTTON_MINUS}, + {Settings::NativeButton::DLeft, PadButton::BUTTON_LEFT}, + {Settings::NativeButton::DUp, PadButton::BUTTON_UP}, + {Settings::NativeButton::DRight, PadButton::BUTTON_RIGHT}, + {Settings::NativeButton::DDown, PadButton::BUTTON_DOWN}, + {Settings::NativeButton::SL, PadButton::BUTTON_L_SL}, + {Settings::NativeButton::SR, PadButton::BUTTON_L_SR}, + {Settings::NativeButton::Screenshot, PadButton::BUTTON_CAPTURE}, + {Settings::NativeButton::Home, PadButton::BUTTON_HOME}, + {Settings::NativeButton::ZL, PadButton::TRIGGER_ZL}, + {Settings::NativeButton::ZR, PadButton::TRIGGER_ZR}, + }; + if (!params.Has("port")) { + return {}; + } + + InputCommon::ButtonMapping mapping{}; + for (const auto& [switch_button, jcadapter_button] : switch_to_jcadapter_button) { + Common::ParamPackage button_params({{"engine", "jcpad"}}); + button_params.Set("port", params.Get("port", 0)); + button_params.Set("button", static_cast(jcadapter_button)); + mapping[switch_button] = button_params; + } + + return mapping; +} + +InputCommon::AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) { + if (!params.Has("port")) { + return {}; + } + + InputCommon::AnalogMapping mapping = {}; + Common::ParamPackage left_analog_params; + left_analog_params.Set("engine", "jcpad"); + left_analog_params.Set("port", params.Get("port", 0)); + left_analog_params.Set("axis_x", 0); + left_analog_params.Set("axis_y", 1); + mapping[Settings::NativeAnalog::LStick] = left_analog_params; + Common::ParamPackage right_analog_params; + right_analog_params.Set("engine", "jcpad"); + right_analog_params.Set("port", params.Get("port", 0)); + right_analog_params.Set("axis_x", 2); + right_analog_params.Set("axis_y", 3); + mapping[Settings::NativeAnalog::RStick] = right_analog_params; + return mapping; +} + +bool Joycons::DeviceConnected(std::size_t port) const { + if (port > 4) { + return false; + } + return joycon[port].type != JoyControllerTypes::None; +} + +void Joycons::ResetDeviceType(std::size_t port) { + if (port > 4) { + return; + } + joycon[port].type = JoyControllerTypes::None; +} + +void Joycons::BeginConfiguration() { + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = true; +} + +void Joycons::EndConfiguration() { + for (auto& pq : pad_queue) { + pq.Clear(); + } + configuring = false; +} + +JCState& Joycons::GetPadState(std::size_t port) { + return joycon[port].state; +} + +const JCState& Joycons::GetPadState(std::size_t port) const { + return joycon[port].state; +} + +std::array, 4>& Joycons::GetPadQueue() { + return pad_queue; +} + +const std::array, 4>& Joycons::GetPadQueue() const { + return pad_queue; +} +} // namespace JCAdapter diff --git a/src/input_common/joycon/jc_adapter.h b/src/input_common/joycon/jc_adapter.h new file mode 100644 index 0000000000..478221b2d6 --- /dev/null +++ b/src/input_common/joycon/jc_adapter.h @@ -0,0 +1,373 @@ +// Copyright 2020 Yuzu Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "common/quaternion.h" +#include "common/param_package.h" +#include "common/threadsafe_queue.h" +#include "common/vector_math.h" +#include "input_common/motion_input.h" +#include "input_common/settings.h" +#include "input_common/main.h" + + +namespace JCAdapter { + +enum class PadButton { + BUTTON_DOWN = 0x000001, + BUTTON_UP = 0x000002, + BUTTON_RIGHT = 0x000004, + BUTTON_LEFT = 0x000008, + BUTTON_L_SR = 0x000010, + BUTTON_L_SL = 0x000020, + TRIGGER_L = 0x000040, + TRIGGER_ZL = 0x000080, + BUTTON_Y = 0x000100, + BUTTON_X = 0x000200, + BUTTON_B = 0x000400, + BUTTON_A = 0x000800, + BUTTON_R_SL = 0x001000, + BUTTON_R_SR = 0x002000, + TRIGGER_R = 0x004000, + TRIGGER_ZR = 0x008000, + BUTTON_MINUS = 0x010000, + BUTTON_PLUS = 0x020000, + STICK_R = 0x040000, + STICK_L = 0x080000, + BUTTON_HOME = 0x100000, + BUTTON_CAPTURE = 0x200000, + // Below is for compatibility with "AxisButton" and "MotionButton" type + STICK = 0x400000, + MOTION = 0x800000, +}; + +enum class Output { + RUMBLE_AND_SUBCMD = 0x01, + FW_UPDATE_PKT = 0x03, + RUMBLE_ONLY = 0x10, + MCU_DATA = 0x11, + USB_CMD = 0x80, +}; + +enum class SubComamnd { + STATE = 0x00, + MANUAL_BT_PAIRING = 0x01, + REQ_DEV_INFO = 0x02, + SET_REPORT_MODE = 0x03, + TRIGGERS_ELAPSED = 0x04, + GET_PAGE_LIST_STATE = 0x05, + SET_HCI_STATE = 0x06, + RESET_PAIRING_INFO = 0x07, + LOW_POWER_MODE = 0x08, + SPI_FLASH_READ = 0x10, + SPI_FLASH_WRITE = 0x11, + RESET_MCU = 0x20, + SET_MCU_CONFIG = 0x21, + SET_MCU_STATE = 0x22, + SET_PLAYER_LIGHTS = 0x30, + GET_PLAYER_LIGHTS = 0x31, + SET_HOME_LIGHT = 0x38, + ENABLE_IMU = 0x40, + SET_IMU_SENSITIVITY = 0x41, + WRITE_IMU_REG = 0x42, + READ_IMU_REG = 0x43, + ENABLE_VIBRATION = 0x48, + GET_REGULATED_VOLTAGE = 0x50, +}; + +enum class CalAddr { + USER_LEFT_MAGIC = 0X8010, + USER_LEFT_DATA = 0X8012, + USER_RIGHT_MAGIC = 0X801B, + USER_RIGHT_DATA = 0X801D, + USER_IMU_MAGIC = 0X8026, + USER_IMU_DATA = 0X8028, + SERIAL_NUMBER = 0X6000, + DEVICE_TYPE = 0X6012, + COLOR_EXIST = 0X601B, + FACT_LEFT_DATA = 0X603d, + FACT_RIGHT_DATA = 0X6046, + COLOR_DATA = 0X6050, + FACT_IMU_DATA = 0X6020, +}; + +enum class ReportMode { + ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00, + ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01, + ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02, + ACTIVE_POLLING_IR_CAMERA_DATA = 0x03, + MCU_UPDATE_STATE = 0x23, + STANDARD_FULL_60HZ = 0x30, + NFC_IR_MODE_60HZ = 0x31, + SIMPLE_HID_MODE = 0x3F, +}; + +enum class GyrSensitivity { + DPS250, + DPS500, + DPS1000, + DPS2000, +}; + +enum class AccSensitivity { + G8, + G4, + G2, + G16, +}; + +enum class GyrPerformance { + HZ833, + HZ208, +}; + +enum class AccPerformance { + HZ200, + HZ100, +}; + +enum class PadAxes { + StickX, + StickY, + SubstickX, + SubstickY, + Undefined, +}; + +enum class PadMotion { + GyrX, + GyrY, + GyrZ, + AccX, + AccY, + AccZ, + Undefined, +}; + +enum class JoyControllerTypes { + None, + Left, + Right, + Pro, +}; + +extern const std::array PadButtonArray; + +struct JCPadStatus { + u32 button{}; + + std::array axis_values{}; + std::array imu_values{}; + std::array orientation{}; + + u8 port{}; + PadAxes axis{PadAxes::Undefined}; + s16 axis_value{0}; + PadMotion motion{PadMotion::Undefined}; + f32 motion_value{0.0f}; +}; + +struct JCState { + std::unordered_map buttons; + std::unordered_map axes; + std::unordered_map motion; +}; + +class Joycons { +public: + /// Initialize the JC Adapter capture and read sequence + Joycons(); + + /// Close the adapter read thread and release the adapter + ~Joycons(); + /// Used for polling + void BeginConfiguration(); + void EndConfiguration(); + + std::vector GetInputDevices() const; + InputCommon::ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params); + InputCommon::AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params); + + bool DeviceConnected(std::size_t port) const; + + void SetRumble(int port, f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low); + + const f32 GetTemperatureCelcius(int port); + const f32 GetTemperatureFahrenheit(int port); + const u8 GetBatteryLevel(int port); + const std::array GetSerialNumber(int port); + const f32 GetVersion(int port); + const JoyControllerTypes GetDeviceType(int port); + const std::array GetMac(int port); + const u32 GetBodyColor(int port); + const u32 GetButtonColor(int port); + const u32 GetLeftGripColor(int port); + const u32 GetRightGripColor(int port); + + std::array, 4>& GetPadQueue(); + const std::array, 4>& GetPadQueue() const; + + JCState& GetPadState(std::size_t port); + const JCState& GetPadState(std::size_t port) const; + +private: + struct HDRumble { + f32 freq_high; + f32 freq_low; + f32 amp_high; + f32 amp_low; + }; + + struct ImuData { + s16 offset; + s16 scale; + f32 value; + }; + + struct Imu { + ImuData acc; + ImuData gyr; + }; + + struct JoyStick { + u16 max; + u16 min; + u16 center; + u16 deadzone; + u16 value; + }; + + struct Color { + u32 body; + u32 buttons; + u32 left_grip; + u32 right_grip; + }; + + struct Joycon { + hid_device* handle = nullptr; + JCState state; + + // Harware config + GyrSensitivity gsen; + GyrPerformance gfrec; + AccSensitivity asen; + AccPerformance afrec; + ReportMode mode; + bool imu_enabled; + bool rumble_enabled; + u8 leds; + + // Fixed value info + f32 version; + std::array mac; + JoyControllerTypes type; + std::array serial_number; + Color color; + u8 port; + + // Realtime values + InputCommon::MotionInput* motion = nullptr; + + std::array gyro; + std::array axis; + std::array imu; + HDRumble hd_rumble; + u32 button; + u32 temperature; + u8 battery; + + std::chrono::time_point last_motion_update; + }; + + // Low level commands for communication + void SendWrite(hid_device* handle, std::vector buffer, int size); + u8 GetCounter(); + std::vector SubCommand(hid_device* handle, SubComamnd sc, std::vector buffer, int size); + std::vector GetResponse(hid_device* handle, SubComamnd sc); + std::vector ReadSPI(hid_device* handle, CalAddr addr, u8 size); + + // Reads calibration values + void SetJoyStickCal(std::vector buffer, JoyStick& axis1, JoyStick& axis2, bool left); + void SetImuCal(Joycon& jc, std::vector buffer); + void GetUserCalibrationData(Joycon& jc); + void GetFactoryCalibrationData(Joycon& jc); + + // Reads buttons and stick values + void GetLeftPadInput(Joycon& jc, std::vector buffer); + void GetRightPadInput(Joycon& jc, std::vector buffer); + void GetProPadInput(Joycon& jc, std::vector buffer); + + // Reads gyro and accelerometer values + f32 TransformAccValue(s16 raw, ImuData cal, AccSensitivity sen); + f32 TransformGyrValue(s16 raw, ImuData cal, GyrSensitivity sen); + s16 GetRawIMUValues(size_t sensor, size_t axis, std::vector buffer); + void GetIMUValues(Joycon& jc, std::vector buffer); + void SetImuConfig(Joycon& jc, GyrSensitivity gsen, GyrPerformance gfrec, AccSensitivity asen, + AccPerformance afrec); + + // Sends rumble state + void SendRumble(int port); + const f32 EncodeRumbleAmplification(f32 amplification); + + // Reads color values + void GetColor(Joycon& jc); + + // Reads device hardware values + void SetMac(Joycon& jc); + void SetSerialNumber(Joycon& jc); + void SetDeviceType(Joycon& jc); + void SetVersionNumber(Joycon& jc); + + // Write device hardware configuration + void SetLedConfig(Joycon& jc, u8 leds); + void EnableImu(Joycon& jc, bool enable); + void EnableRumble(Joycon& jc, bool enable); + void SetReportMode(Joycon& jc, ReportMode mode); + + void UpdateJoyconData(Joycon& jc, std::vector buffer); + void UpdateYuzuSettings(Joycon& jc, int port); + void JoyconToState(Joycon& jc, JCState& state); + std::string JoyconName(std::size_t port) const; + void ReadLoop(); + + /// Resets status of device connected to port + void ResetDeviceType(std::size_t port); + + /// Returns true if we successfully gain access to JC Adapter + bool CheckDeviceAccess(int port, hid_device_info* device); + + /// Captures JC Adapter endpoint address, + void GetJCEndpoint(); + + /// For shutting down, clear all data, join all threads, release usb + void Reset(); + + /// For use in initialization, querying devices to find the adapter + void Setup(); + + // void UpdateOrientation(Joycon& jc, u64 time, std::size_t iteration); + + std::thread adapter_input_thread; + bool adapter_thread_running; + + bool configuring = false; + + std::array joycon; + std::array, 4> pad_queue; + + u8 global_counter; + static constexpr u32 max_resp_size = 49; + static constexpr std::array default_buffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40}; +}; + +} // namespace JCAdapter diff --git a/src/input_common/joycon/jc_poller.cpp b/src/input_common/joycon/jc_poller.cpp new file mode 100644 index 0000000000..edcc8bf739 --- /dev/null +++ b/src/input_common/joycon/jc_poller.cpp @@ -0,0 +1,445 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "common/assert.h" +#include "common/threadsafe_queue.h" +#include "input_common/joycon/jc_adapter.h" +#include "input_common/joycon/jc_poller.h" + +namespace InputCommon { + +class JCButton final : public Input::ButtonDevice { +public: + explicit JCButton(int port_, int button_, JCAdapter::Joycons* adapter) + : port(port_), button(button_), jcadapter(adapter) {} + + ~JCButton() override; + + bool GetStatus() const override { + if (jcadapter->DeviceConnected(port)) { + return jcadapter->GetPadState(port).buttons.at(button); + } + return false; + } + + bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const override { + if (jcadapter->DeviceConnected(port)) { + jcadapter->SetRumble(port, amp_high, amp_low, freq_high, freq_low); + return true; + } + return false; + } + +private: + const int port; + const int button; + JCAdapter::Joycons* jcadapter; +}; + +class JCAxisButton final : public Input::ButtonDevice { +public: + explicit JCAxisButton(int port_, int axis_, float threshold_, bool trigger_if_greater_, + JCAdapter::Joycons* adapter) + : port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), + jcadapter(adapter) {} + + bool GetStatus() const override { + if (jcadapter->DeviceConnected(port)) { + const float axis_value = jcadapter->GetPadState(port).axes.at(axis); + if (trigger_if_greater) { + // TODO: Might be worthwile to set a slider for the trigger threshold. It is + // currently always set to 0.5 in configure_input_player.cpp ZL/ZR HandleClick + return axis_value > threshold; + } + return axis_value < -threshold; + } + return false; + } + + bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const override { + if (jcadapter->DeviceConnected(port)) { + jcadapter->SetRumble(port, amp_high, amp_low, freq_high, freq_low); + return true; + } + return false; + } + +private: + const int port; + const int axis; + float threshold; + bool trigger_if_greater; + JCAdapter::Joycons* jcadapter; +}; +//Motion buttons are a temporary way to test motion +class JCMotionButton final : public Input::ButtonDevice { +public: + explicit JCMotionButton(int port_, int axis_, float threshold_, bool trigger_if_greater_, + JCAdapter::Joycons* adapter) + : port(port_), axis(axis_), threshold(threshold_), trigger_if_greater(trigger_if_greater_), + jcadapter(adapter) {} + + bool GetStatus() const override { + if (jcadapter->DeviceConnected(port)) { + const float axis_value = jcadapter->GetPadState(port).motion.at(axis); + if (trigger_if_greater) { + // TODO: Might be worthwile to set a slider for the trigger threshold. It is + // currently always set to 0.5 in configure_input_player.cpp ZL/ZR HandleClick + return axis_value > threshold; + } + return axis_value < -threshold; + } + return false; + } + + bool SetRumblePlay(f32 amp_high, f32 amp_low, f32 freq_high, f32 freq_low) const override { + if (jcadapter->DeviceConnected(port)) { + jcadapter->SetRumble(port, amp_high, amp_low, freq_high, freq_low); + return true; + } + return false; + } + +private: + const int port; + const int axis; + float threshold; + bool trigger_if_greater; + JCAdapter::Joycons* jcadapter; +}; + +JCButtonFactory::JCButtonFactory(std::shared_ptr adapter_) + : adapter(std::move(adapter_)) {} + +JCButton::~JCButton() = default; + +std::unique_ptr JCButtonFactory::Create(const Common::ParamPackage& params) { + const int button_id = params.Get("button", 0); + const int port = params.Get("port", 0); + + constexpr int PAD_STICK_ID = static_cast(JCAdapter::PadButton::STICK); + constexpr int PAD_MOTION_ID = static_cast(JCAdapter::PadButton::MOTION); + // button is not an axis/stick button + if (button_id != PAD_STICK_ID && button_id != PAD_MOTION_ID) { + auto button = std::make_unique(port, button_id, adapter.get()); + return std::move(button); + } + + // For Axis buttons, used by the binary sticks. + if (button_id == PAD_STICK_ID && button_id != PAD_MOTION_ID) { + const int axis = params.Get("axis", 0); + const float threshold = params.Get("threshold", 0.25f); + const std::string direction_name = params.Get("direction", ""); + bool trigger_if_greater; + if (direction_name == "+") { + trigger_if_greater = true; + } else if (direction_name == "-") { + trigger_if_greater = false; + } else { + trigger_if_greater = true; + LOG_ERROR(Input, "Unknown direction {}", direction_name); + } + return std::make_unique(port, axis, threshold, trigger_if_greater, + adapter.get()); + } + + // For Motion buttons, used by the binary motion. + if (button_id != PAD_STICK_ID && button_id == PAD_MOTION_ID) { + const int axis = params.Get("motion", 0); + const float threshold = params.Get("threshold", 0.5f); + const std::string direction_name = params.Get("direction", ""); + bool trigger_if_greater; + if (direction_name == "+") { + trigger_if_greater = true; + } else if (direction_name == "-") { + trigger_if_greater = false; + } else { + trigger_if_greater = true; + LOG_ERROR(Input, "Unknown direction {}", direction_name); + } + return std::make_unique(port, axis, threshold, trigger_if_greater, + adapter.get()); + } + + UNREACHABLE(); + return nullptr; +} + +Common::ParamPackage JCButtonFactory::GetNextInput() { + Common::ParamPackage params; + JCAdapter::JCPadStatus pad; + auto& queue = adapter->GetPadQueue(); + for (std::size_t port = 0; port < queue.size(); ++port) { + while (queue[port].Pop(pad)) { + // This while loop will break on the earliest detected button + params.Set("engine", "jcpad"); + params.Set("port", static_cast(port)); + for (const auto& button : JCAdapter::PadButtonArray) { + const int button_value = static_cast(button); + if (pad.button & button_value) { + params.Set("button", button_value); + break; + } + } + + // For Axis button implementation + if (pad.axis != JCAdapter::PadAxes::Undefined) { + params.Set("axis", static_cast(pad.axis)); + params.Set("button", static_cast(JCAdapter::PadButton::STICK)); + if (pad.axis_value > 0) { + params.Set("direction", "+"); + params.Set("threshold", "0.25"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "0.25"); + } + break; + } + + // For Motion button implementation + if (pad.motion != JCAdapter::PadMotion::Undefined) { + params.Set("motion", static_cast(pad.motion)); + params.Set("button", static_cast(JCAdapter::PadButton::MOTION)); + if (pad.motion_value > 0) { + params.Set("direction", "+"); + params.Set("threshold", "0.5"); + } else { + params.Set("direction", "-"); + params.Set("threshold", "0.5"); + } + break; + } + } + } + return params; +} + +void JCButtonFactory::BeginConfiguration() { + polling = true; + adapter->BeginConfiguration(); +} + +void JCButtonFactory::EndConfiguration() { + polling = false; + adapter->EndConfiguration(); +} + +class JCAnalog final : public Input::AnalogDevice { +public: + JCAnalog(int port_, int axis_x_, int axis_y_, float deadzone_, JCAdapter::Joycons* adapter) + : port(port_), axis_x(axis_x_), axis_y(axis_y_), deadzone(deadzone_), jcadapter(adapter) {} + + float GetAxis(int axis) const { + if (jcadapter->DeviceConnected(port)) { + std::lock_guard lock{mutex}; + if (axis < 10) { + return jcadapter->GetPadState(port).axes.at(axis); + } else { + return jcadapter->GetPadState(port).motion.at(axis-10); + } + } + return 0.0f; + } + + std::pair GetAnalog(int axis_x, int axis_y) const { + float x = GetAxis(axis_x); + float y = GetAxis(axis_y); + + // Make sure the coordinates are in the unit circle, + // otherwise normalize it. + float r = x * x + y * y; + if (r > 1.0f) { + r = std::sqrt(r); + x /= r; + y /= r; + } + + return {x, y}; + } + + std::tuple GetStatus() const override { + const auto [x, y] = GetAnalog(axis_x, axis_y); + const float r = std::sqrt((x * x) + (y * y)); + if (r > deadzone) { + return {x / r * (r - deadzone) / (1 - deadzone), + y / r * (r - deadzone) / (1 - deadzone)}; + } + return {0.0f, 0.0f}; + } + + bool GetAnalogDirectionStatus(Input::AnalogDirection direction) const override { + const auto [x, y] = GetStatus(); + const float directional_deadzone = 0.4f; + switch (direction) { + case Input::AnalogDirection::RIGHT: + return x > directional_deadzone; + case Input::AnalogDirection::LEFT: + return x < -directional_deadzone; + case Input::AnalogDirection::UP: + return y > directional_deadzone; + case Input::AnalogDirection::DOWN: + return y < -directional_deadzone; + } + return false; + } + +private: + const int port; + const int axis_x; + const int axis_y; + const float deadzone; + JCAdapter::Joycons* jcadapter; + mutable std::mutex mutex; +}; + +/// An analog device factory that creates analog devices from JC Adapter +JCAnalogFactory::JCAnalogFactory(std::shared_ptr adapter_) + : adapter(std::move(adapter_)) {} + +/** + * Creates analog device from joystick axes + * @param params contains parameters for creating the device: + * - "port": the nth jcpad on the adapter + * - "axis_x": the index of the axis to be bind as x-axis + * - "axis_y": the index of the axis to be bind as y-axis + */ +std::unique_ptr JCAnalogFactory::Create(const Common::ParamPackage& params) { + const int port = params.Get("port", 0); + const int axis_x = params.Get("axis_x", 0); + const int axis_y = params.Get("axis_y", 1); + const float deadzone = std::clamp(params.Get("deadzone", 0.0f), 0.0f, .99f); + + return std::make_unique(port, axis_x, axis_y, deadzone, adapter.get()); +} + +void JCAnalogFactory::BeginConfiguration() { + polling = true; + adapter->BeginConfiguration(); +} + +void JCAnalogFactory::EndConfiguration() { + polling = false; + adapter->EndConfiguration(); +} + +Common::ParamPackage JCAnalogFactory::GetNextInput() { + JCAdapter::JCPadStatus pad; + auto& queue = adapter->GetPadQueue(); + for (std::size_t port = 0; port < queue.size(); ++port) { + while (queue[port].Pop(pad)) { + if (pad.axis != JCAdapter::PadAxes::Undefined && std::abs(pad.axis_value) > 0.1) { + // An analog device needs two axes, so we need to store the axis for later and wait + // for a second input event. The axes also must be from the same joystick. + const u16 axis = static_cast(pad.axis); + if (analog_x_axis == -1) { + analog_x_axis = axis; + controller_number = static_cast(port); + } else if (analog_y_axis == -1 && analog_x_axis != axis && + controller_number == port) { + analog_y_axis = axis; + } + }else if (pad.motion != JCAdapter::PadMotion::Undefined && + std::abs(pad.motion_value) > 1) { + const u16 axis = 10 + static_cast(pad.motion); + if (analog_x_axis == -1) { + analog_x_axis = axis; + controller_number = static_cast(port); + } else if (analog_y_axis == -1 && analog_x_axis != axis && + controller_number == port) { + analog_y_axis = axis; + } + } + } + } + Common::ParamPackage params; + if (analog_x_axis != -1 && analog_y_axis != -1) { + params.Set("engine", "jcpad"); + params.Set("port", controller_number); + params.Set("axis_x", analog_x_axis); + params.Set("axis_y", analog_y_axis); + analog_x_axis = -1; + analog_y_axis = -1; + controller_number = -1; + return params; + } + return params; +} + +class JCMotion final : public Input::MotionDevice { +public: + JCMotion(int port_, JCAdapter::Joycons* adapter) : port(port_), jcadapter(adapter) {} + + std::tuple, Common::Vec3, Common::Vec3, + std::array> + GetStatus() + const override { + auto motion = jcadapter->GetPadState(port).motion; + if (motion.size() == 18) { + auto gyroscope = Common::MakeVec(motion.at(0), motion.at(1), motion.at(2)); + auto accelerometer = Common::MakeVec(motion.at(3), motion.at(4), motion.at(5)); + auto rotation = Common::MakeVec(motion.at(6), motion.at(7), motion.at(8)); + std::array orientation{}; + orientation[0] = Common::MakeVec(motion.at(9), motion.at(10), motion.at(11)); + orientation[1] = Common::MakeVec(motion.at(12), motion.at(13), motion.at(14)); + orientation[2] = Common::MakeVec(motion.at(15), motion.at(16), motion.at(17)); + return std::make_tuple(accelerometer, gyroscope, rotation, orientation); + } + return {}; + } + +private: + const int port; + JCAdapter::Joycons* jcadapter; + mutable std::mutex mutex; +}; + +/// A motion device factory that creates motion devices from JC Adapter +JCMotionFactory::JCMotionFactory(std::shared_ptr adapter_) + : adapter(std::move(adapter_)) {} + +/** + * Creates motion device + * @param params contains parameters for creating the device: + * - "port": the nth jcpad on the adapter + */ +std::unique_ptr JCMotionFactory::Create( + const Common::ParamPackage& params) { + const int port = params.Get("port", 0); + + return std::make_unique(port, adapter.get()); +} + +void JCMotionFactory::BeginConfiguration() { + polling = true; + adapter->BeginConfiguration(); +} + +void JCMotionFactory::EndConfiguration() { + polling = false; + adapter->EndConfiguration(); +} + +Common::ParamPackage JCMotionFactory::GetNextInput() { + Common::ParamPackage params; + JCAdapter::JCPadStatus pad; + auto& queue = adapter->GetPadQueue(); + for (std::size_t port = 0; port < queue.size(); ++port) { + while (queue[port].Pop(pad)) { + if (pad.motion == JCAdapter::PadMotion::Undefined || std::abs(pad.motion_value) < 1) { + continue; + } + params.Set("engine", "jcpad"); + params.Set("motion", static_cast(pad.motion)); + params.Set("port", static_cast(port)); + return params; + } + } + return params; +} + +} // namespace InputCommon \ No newline at end of file diff --git a/src/input_common/joycon/jc_poller.h b/src/input_common/joycon/jc_poller.h new file mode 100644 index 0000000000..bd69dd4ade --- /dev/null +++ b/src/input_common/joycon/jc_poller.h @@ -0,0 +1,89 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/frontend/input.h" +#include "input_common/joycon/jc_adapter.h" + +namespace InputCommon { + +/** + * A button device factory representing a jcpad. It receives jcpad events and forward them + * to all button devices it created. + */ +class JCButtonFactory final : public Input::Factory { +public: + explicit JCButtonFactory(std::shared_ptr adapter_); + + /** + * Creates a button device from a button press + * @param params contains parameters for creating the device: + * - "code": the code of the key to bind with the button + */ + std::unique_ptr Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + bool polling = false; +}; + +/// An analog device factory that creates analog devices from JC Adapter +class JCAnalogFactory final : public Input::Factory { +public: + explicit JCAnalogFactory(std::shared_ptr adapter_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + int analog_x_axis = -1; + int analog_y_axis = -1; + int controller_number = -1; + bool polling = false; +}; + +/// A motion device factory that creates motion devices from JC Adapter +class JCMotionFactory final : public Input::Factory { +public: + explicit JCMotionFactory(std::shared_ptr adapter_); + + std::unique_ptr Create(const Common::ParamPackage& params) override; + + Common::ParamPackage GetNextInput(); + + /// For device input configuration/polling + void BeginConfiguration(); + void EndConfiguration(); + + bool IsPolling() const { + return polling; + } + +private: + std::shared_ptr adapter; + bool polling = false; +}; + +} // namespace InputCommon diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index 7c4e7dd3bf..d7986ccf35 100644 --- a/src/input_common/main.cpp +++ b/src/input_common/main.cpp @@ -8,6 +8,8 @@ #include "input_common/analog_from_button.h" #include "input_common/gcadapter/gc_adapter.h" #include "input_common/gcadapter/gc_poller.h" +#include "input_common/joycon/jc_adapter.h" +#include "input_common/joycon/jc_poller.h" #include "input_common/keyboard.h" #include "input_common/main.h" #include "input_common/motion_from_button.h" @@ -32,6 +34,14 @@ struct InputSubsystem::Impl { gcvibration = std::make_shared(gcadapter); Input::RegisterFactory("gcpad", gcvibration); + jcadapter = std::make_shared(); + jcbuttons = std::make_shared(jcadapter); + Input::RegisterFactory("jcpad", jcbuttons); + jcanalog = std::make_shared(jcadapter); + Input::RegisterFactory("jcpad", jcanalog); + jcmotion = std::make_shared(jcadapter); + Input::RegisterFactory("jcpad", jcmotion); + keyboard = std::make_shared(); Input::RegisterFactory("keyboard", keyboard); Input::RegisterFactory("analog_from_button", @@ -79,6 +89,14 @@ struct InputSubsystem::Impl { gcanalog.reset(); gcvibration.reset(); + Input::UnregisterFactory("jcpad"); + Input::UnregisterFactory("jcpad"); + Input::UnregisterFactory("jcpad"); + + jcbuttons.reset(); + jcanalog.reset(); + jcmotion.reset(); + Input::UnregisterFactory("cemuhookudp"); Input::UnregisterFactory("cemuhookudp"); @@ -105,6 +123,8 @@ struct InputSubsystem::Impl { auto sdl_devices = sdl->GetInputDevices(); devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); #endif + auto jcpad_devices = jcadapter->GetInputDevices(); + devices.insert(devices.end(), jcpad_devices.begin(), jcpad_devices.end()); auto udp_devices = udp->GetInputDevices(); devices.insert(devices.end(), udp_devices.begin(), udp_devices.end()); auto gcpad_devices = gcadapter->GetInputDevices(); @@ -119,6 +139,9 @@ struct InputSubsystem::Impl { } if (params.Get("class", "") == "gcpad") { return gcadapter->GetAnalogMappingForDevice(params); + } + if (params.Get("class", "") == "jcpad") { + return jcadapter->GetAnalogMappingForDevice(params); } #ifdef HAVE_SDL2 if (params.Get("class", "") == "sdl") { @@ -135,6 +158,9 @@ struct InputSubsystem::Impl { } if (params.Get("class", "") == "gcpad") { return gcadapter->GetButtonMappingForDevice(params); + } + if (params.Get("class", "") == "jcpad") { + return jcadapter->GetButtonMappingForDevice(params); } #ifdef HAVE_SDL2 if (params.Get("class", "") == "sdl") { @@ -169,6 +195,10 @@ struct InputSubsystem::Impl { std::shared_ptr mouseanalog; std::shared_ptr mousemotion; std::shared_ptr mousetouch; + std::shared_ptr jcbuttons; + std::shared_ptr jcanalog; + std::shared_ptr jcmotion; + std::shared_ptr jcadapter; std::shared_ptr udp; std::shared_ptr gcadapter; std::shared_ptr mouse; @@ -234,6 +264,30 @@ const GCButtonFactory* InputSubsystem::GetGCButtons() const { return impl->gcbuttons.get(); } +JCAnalogFactory* InputSubsystem::GetJCAnalogs() { + return impl->jcanalog.get(); +} + +const JCAnalogFactory* InputSubsystem::GetJCAnalogs() const { + return impl->jcanalog.get(); +} + +JCButtonFactory* InputSubsystem::GetJCButtons() { + return impl->jcbuttons.get(); +} + +const JCButtonFactory* InputSubsystem::GetJCButtons() const { + return impl->jcbuttons.get(); +} + +JCMotionFactory* InputSubsystem::GetJCMotions() { + return impl->jcmotion.get(); +} + +const JCMotionFactory* InputSubsystem::GetJCMotions() const { + return impl->jcmotion.get(); +} + UDPMotionFactory* InputSubsystem::GetUDPMotions() { return impl->udpmotion.get(); } @@ -319,4 +373,4 @@ std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, }; return circle_pad_param.Serialize(); } -} // namespace InputCommon +} // namespace InputCommon \ No newline at end of file diff --git a/src/input_common/main.h b/src/input_common/main.h index 5d6f26385f..c4e9d54480 100644 --- a/src/input_common/main.h +++ b/src/input_common/main.h @@ -58,6 +58,9 @@ public: class GCAnalogFactory; class GCButtonFactory; +class JCAnalogFactory; +class JCButtonFactory; +class JCMotionFactory; class UDPMotionFactory; class UDPTouchFactory; class MouseButtonFactory; @@ -132,6 +135,24 @@ public: /// Retrieves the underlying GameCube button handler. [[nodiscard]] const GCButtonFactory* GetGCButtons() const; + /// Retrieves the underlying joycon analog handler. + [[nodiscard]] JCAnalogFactory* GetJCAnalogs(); + + /// Retrieves the underlying joycon analog handler. + [[nodiscard]] const JCAnalogFactory* GetJCAnalogs() const; + + /// Retrieves the underlying joycon button handler. + [[nodiscard]] JCButtonFactory* GetJCButtons(); + + /// Retrieves the underlying joycon button handler. + [[nodiscard]] const JCButtonFactory* GetJCButtons() const; + + /// Retrieves the underlying joycon motion handler. + [[nodiscard]] JCMotionFactory* GetJCMotions(); + + /// Retrieves the underlying joycon motion handler. + [[nodiscard]] const JCMotionFactory* GetJCMotions() const; + /// Retrieves the underlying udp motion handler. [[nodiscard]] UDPMotionFactory* GetUDPMotions(); diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index b40d7c5e25..7f49291958 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -17,6 +17,7 @@ #include "core/hle/service/hid/hid.h" #include "core/hle/service/sm/sm.h" #include "input_common/gcadapter/gc_poller.h" +#include "input_common/joycon/jc_poller.h" #include "input_common/main.h" #include "input_common/mouse/mouse_poller.h" #include "input_common/udp/udp.h" @@ -128,6 +129,25 @@ QString ButtonToText(const Common::ParamPackage& param) { return GetKeyName(param.Get("code", 0)); } + if (param.Get("engine", "") == "jcpad") { + if (param.Has("motion")) { + const QString motion_str = QString::fromStdString(param.Get("motion", "")); + const QString direction_str = QString::fromStdString(param.Get("direction", "")); + return QObject::tr("JC Motion %1%2").arg(motion_str, direction_str); + } + if (param.Has("axis")) { + const QString axis_str = QString::fromStdString(param.Get("axis", "")); + const QString direction_str = QString::fromStdString(param.Get("direction", "")); + + return QObject::tr("JC Axis %1%2").arg(axis_str, direction_str); + } + if (param.Has("button")) { + const QString button_str = QString::number(int(std::log2(param.Get("button", 0)))); + return QObject::tr("JC Button %1").arg(button_str); + } + return GetKeyName(param.Get("code", 0)); + } + if (param.Get("engine", "") == "sdl") { if (param.Has("hat")) { const QString hat_str = QString::fromStdString(param.Get("hat", "")); @@ -201,6 +221,26 @@ QString AnalogToText(const Common::ParamPackage& param, const std::string& dir) return {}; } + + if (param.Get("engine", "") == "jcpad") { + if (dir == "modifier") { + return QObject::tr("[unused]"); + } + + if (dir == "left" || dir == "right") { + const QString axis_x_str = QString::fromStdString(param.Get("axis_x", "")); + + return QObject::tr("JC Axis %1").arg(axis_x_str); + } + + if (dir == "up" || dir == "down") { + const QString axis_y_str = QString::fromStdString(param.Get("axis_y", "")); + + return QObject::tr("JC Axis %1").arg(axis_y_str); + } + + return {}; + } return QObject::tr("[unknown]"); } } // namespace @@ -529,6 +569,27 @@ ConfigureInputPlayer::ConfigureInputPlayer(QWidget* parent, std::size_t player_i return; } } + if (input_subsystem->GetJCAnalogs()->IsPolling()) { + params = input_subsystem->GetJCAnalogs()->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; + } + } + if (input_subsystem->GetJCButtons()->IsPolling()) { + params = input_subsystem->GetJCButtons()->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; + } + } + if (input_subsystem->GetJCMotions()->IsPolling()) { + params = input_subsystem->GetJCMotions()->GetNextInput(); + if (params.Has("engine")) { + SetPollingResult(params, false); + return; + } + } for (auto& poller : device_pollers) { params = poller->GetNextInput(); if (params.Has("engine") && IsInputAcceptable(params)) { @@ -847,7 +908,8 @@ void ConfigureInputPlayer::UpdateUI() { auto& param = analogs_param[analog_id]; const bool is_controller = param.Get("engine", "") == "sdl" || param.Get("engine", "") == "gcpad" || - param.Get("engine", "") == "mouse"; + param.Get("engine", "") == "mouse" || + param.Get("engine", "") == "jcpad"; if (is_controller) { if (!param.Has("deadzone")) { @@ -1173,6 +1235,14 @@ void ConfigureInputPlayer::HandleClick( input_subsystem->GetMouseTouch()->BeginConfiguration(); } + if (type == InputCommon::Polling::DeviceType::Button) { + input_subsystem->GetJCButtons()->BeginConfiguration(); + } else if (type == InputCommon::Polling::DeviceType::AnalogPreferred) { + input_subsystem->GetJCAnalogs()->BeginConfiguration(); + } else { + input_subsystem->GetJCMotions()->BeginConfiguration(); + } + timeout_timer->start(2500); // Cancel after 2.5 seconds poll_timer->start(50); // Check for new inputs every 50ms } @@ -1197,6 +1267,10 @@ void ConfigureInputPlayer::SetPollingResult(const Common::ParamPackage& params, input_subsystem->GetMouseMotions()->EndConfiguration(); input_subsystem->GetMouseTouch()->EndConfiguration(); + input_subsystem->GetJCButtons()->EndConfiguration(); + input_subsystem->GetJCAnalogs()->EndConfiguration(); + input_subsystem->GetJCMotions()->EndConfiguration(); + if (!abort) { (*input_setter)(params); }