From 58448d74c541d8081bafff4d35940281db88c610 Mon Sep 17 00:00:00 2001 From: Jordan Woyak Date: Tue, 21 Jan 2020 18:50:05 -0600 Subject: [PATCH] InputCommon: Add real Wii Remote support to ControllerInterface. Add option to connect additional Wii Remotes. --- Source/Core/Core/ConfigManager.cpp | 2 + Source/Core/Core/ConfigManager.h | 1 + Source/Core/Core/HW/WiimoteEmu/Camera.cpp | 22 +- Source/Core/Core/HW/WiimoteEmu/Camera.h | 13 + Source/Core/Core/HW/WiimoteEmu/Dynamics.cpp | 61 +- Source/Core/Core/HW/WiimoteEmu/Dynamics.h | 17 +- .../Core/HW/WiimoteEmu/EmuSubroutines.cpp | 4 - Source/Core/Core/HW/WiimoteReal/IOdarwin.mm | 17 +- .../Core/Core/HW/WiimoteReal/WiimoteReal.cpp | 289 +-- Source/Core/Core/HW/WiimoteReal/WiimoteReal.h | 18 +- .../DolphinQt/Config/ControllersWindow.cpp | 119 +- .../Core/DolphinQt/Config/ControllersWindow.h | 6 +- .../Config/Mapping/MappingIndicator.cpp | 5 +- Source/Core/InputCommon/CMakeLists.txt | 2 + .../ControllerEmu/ControlGroup/Cursor.cpp | 4 +- .../ControllerInterface.cpp | 4 +- .../ControllerInterface/Wiimote/Wiimote.cpp | 1631 +++++++++++++++++ .../ControllerInterface/Wiimote/Wiimote.h | 276 +++ Source/Core/InputCommon/InputCommon.vcxproj | 4 +- .../InputCommon/InputCommon.vcxproj.filters | 8 +- 20 files changed, 2267 insertions(+), 236 deletions(-) create mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp create mode 100644 Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 1e8272ad5e13..0d9821111d54 100644 --- a/Source/Core/Core/ConfigManager.cpp +++ b/Source/Core/Core/ConfigManager.cpp @@ -234,6 +234,7 @@ void SConfig::SaveCoreSettings(IniFile& ini) core->Set("WiiKeyboard", m_WiiKeyboard); core->Set("WiimoteContinuousScanning", m_WiimoteContinuousScanning); core->Set("WiimoteEnableSpeaker", m_WiimoteEnableSpeaker); + core->Set("WiimoteControllerInterface", connect_wiimotes_for_ciface); core->Set("RunCompareServer", bRunCompareServer); core->Set("RunCompareClient", bRunCompareClient); core->Set("MMU", bMMU); @@ -511,6 +512,7 @@ void SConfig::LoadCoreSettings(IniFile& ini) core->Get("WiiKeyboard", &m_WiiKeyboard, false); core->Get("WiimoteContinuousScanning", &m_WiimoteContinuousScanning, false); core->Get("WiimoteEnableSpeaker", &m_WiimoteEnableSpeaker, false); + core->Get("WiimoteControllerInterface", &connect_wiimotes_for_ciface, false); core->Get("RunCompareServer", &bRunCompareServer, false); core->Get("RunCompareClient", &bRunCompareClient, false); core->Get("MMU", &bMMU, bMMU); diff --git a/Source/Core/Core/ConfigManager.h b/Source/Core/Core/ConfigManager.h index d752010af506..60edd466b01b 100644 --- a/Source/Core/Core/ConfigManager.h +++ b/Source/Core/Core/ConfigManager.h @@ -72,6 +72,7 @@ struct SConfig bool m_WiiKeyboard; bool m_WiimoteContinuousScanning; bool m_WiimoteEnableSpeaker; + bool connect_wiimotes_for_ciface; // ISO folder std::vector m_ISOFolder; diff --git a/Source/Core/Core/HW/WiimoteEmu/Camera.cpp b/Source/Core/Core/HW/WiimoteEmu/Camera.cpp index 25b542bc8a33..32cc1752d1d3 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Camera.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Camera.cpp @@ -60,14 +60,6 @@ void CameraLogic::Update(const Common::Matrix44& transform) using Common::Vec3; using Common::Vec4; - constexpr int CAMERA_WIDTH = 1024; - constexpr int CAMERA_HEIGHT = 768; - - // Wiibrew claims the camera FOV is about 33 deg by 23 deg. - // Unconfirmed but it seems to work well enough. - constexpr int CAMERA_FOV_X_DEG = 33; - constexpr int CAMERA_FOV_Y_DEG = 23; - constexpr auto CAMERA_FOV_Y = float(CAMERA_FOV_Y_DEG * MathUtil::TAU / 360); constexpr auto CAMERA_ASPECT_RATIO = float(CAMERA_FOV_X_DEG) / CAMERA_FOV_Y_DEG; @@ -112,12 +104,12 @@ void CameraLogic::Update(const Common::Matrix44& transform) if (point.z > 0) { // FYI: Casting down vs. rounding seems to produce more symmetrical output. - const auto x = s32((1 - point.x / point.w) * CAMERA_WIDTH / 2); - const auto y = s32((1 - point.y / point.w) * CAMERA_HEIGHT / 2); + const auto x = s32((1 - point.x / point.w) * CAMERA_RES_X / 2); + const auto y = s32((1 - point.y / point.w) * CAMERA_RES_Y / 2); const auto point_size = std::lround(MAX_POINT_SIZE / point.w / 2); - if (x >= 0 && y >= 0 && x < CAMERA_WIDTH && y < CAMERA_HEIGHT) + if (x >= 0 && y >= 0 && x < CAMERA_RES_X && y < CAMERA_RES_Y) return CameraPoint{u16(x), u16(y), u8(point_size)}; } @@ -165,7 +157,7 @@ void CameraLogic::Update(const Common::Matrix44& transform) for (std::size_t i = 0; i != camera_points.size(); ++i) { const auto& p = camera_points[i]; - if (p.x < CAMERA_WIDTH) + if (p.x < CAMERA_RES_X) { IRExtended irdata = {}; @@ -186,7 +178,7 @@ void CameraLogic::Update(const Common::Matrix44& transform) for (std::size_t i = 0; i != camera_points.size(); ++i) { const auto& p = camera_points[i]; - if (p.x < CAMERA_WIDTH) + if (p.x < CAMERA_RES_X) { IRFull irdata = {}; @@ -203,8 +195,8 @@ void CameraLogic::Update(const Common::Matrix44& transform) irdata.xmin = std::max(p.x - p.size, 0); irdata.ymin = std::max(p.y - p.size, 0); - irdata.xmax = std::min(p.x + p.size, CAMERA_WIDTH); - irdata.ymax = std::min(p.y + p.size, CAMERA_HEIGHT); + irdata.xmax = std::min(p.x + p.size, CAMERA_RES_X); + irdata.ymax = std::min(p.y + p.size, CAMERA_RES_Y); // TODO: Is this maybe MSbs of the "intensity" value? irdata.zero = 0; diff --git a/Source/Core/Core/HW/WiimoteEmu/Camera.h b/Source/Core/Core/HW/WiimoteEmu/Camera.h index 29c3d9cd94a3..547610bd9b13 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Camera.h +++ b/Source/Core/Core/HW/WiimoteEmu/Camera.h @@ -20,6 +20,8 @@ namespace WiimoteEmu // Four bytes for two objects. Filled with 0xFF if empty struct IRBasic { + using IRObject = Common::TVec2; + u8 x1; u8 y1; u8 x2hi : 2; @@ -28,6 +30,9 @@ struct IRBasic u8 y1hi : 2; u8 x2; u8 y2; + + auto GetObject1() const { return IRObject(x1hi << 8 | x1, y1hi << 8 | y1); } + auto GetObject2() const { return IRObject(x2hi << 8 | x2, y2hi << 8 | y2); } }; static_assert(sizeof(IRBasic) == 5, "Wrong size"); @@ -62,6 +67,14 @@ static_assert(sizeof(IRFull) == 9, "Wrong size"); class CameraLogic : public I2CSlave { public: + static constexpr int CAMERA_RES_X = 1024; + static constexpr int CAMERA_RES_Y = 768; + + // Wiibrew claims the camera FOV is about 33 deg by 23 deg. + // Unconfirmed but it seems to work well enough. + static constexpr int CAMERA_FOV_X_DEG = 33; + static constexpr int CAMERA_FOV_Y_DEG = 23; + enum : u8 { IR_MODE_BASIC = 1, diff --git a/Source/Core/Core/HW/WiimoteEmu/Dynamics.cpp b/Source/Core/Core/HW/WiimoteEmu/Dynamics.cpp index 9c4fdb0d8f19..5ce129507bbd 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Dynamics.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Dynamics.cpp @@ -54,11 +54,15 @@ double CalculateStopDistance(double velocity, double max_accel) return velocity * velocity / (2 * std::copysign(max_accel, velocity)); } -// Note that 'gyroscope' is rotation of world around device. -Common::Matrix33 ComplementaryFilter(const Common::Vec3& accelerometer, - const Common::Matrix33& gyroscope, float accel_weight) +} // namespace + +namespace WiimoteEmu { - const auto gyro_vec = gyroscope * Common::Vec3{0, 0, 1}; +Common::Matrix33 ComplementaryFilter(const Common::Matrix33& gyroscope, + const Common::Vec3& accelerometer, float accel_weight, + const Common::Vec3& accelerometer_normal) +{ + const auto gyro_vec = gyroscope * accelerometer_normal; const auto normalized_accel = accelerometer.Normalized(); const auto cos_angle = normalized_accel.Dot(gyro_vec); @@ -76,10 +80,6 @@ Common::Matrix33 ComplementaryFilter(const Common::Vec3& accelerometer, } } -} // namespace - -namespace WiimoteEmu -{ IMUCursorState::IMUCursorState() : rotation{Common::Matrix33::Identity()} { } @@ -203,17 +203,17 @@ void EmulateSwing(MotionState* state, ControllerEmu::Force* swing_group, float t } } -WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g, - u16 one_g) +WiimoteCommon::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g, u16 one_g) { const auto scaled_accel = accel * (one_g - zero_g) / float(GRAVITY_ACCELERATION); // 10-bit integers. constexpr long MAX_VALUE = (1 << 10) - 1; - return {u16(std::clamp(std::lround(scaled_accel.x + zero_g), 0l, MAX_VALUE)), - u16(std::clamp(std::lround(scaled_accel.y + zero_g), 0l, MAX_VALUE)), - u16(std::clamp(std::lround(scaled_accel.z + zero_g), 0l, MAX_VALUE))}; + return WiimoteCommon::AccelData( + {u16(std::clamp(std::lround(scaled_accel.x + zero_g), 0l, MAX_VALUE)), + u16(std::clamp(std::lround(scaled_accel.y + zero_g), 0l, MAX_VALUE)), + u16(std::clamp(std::lround(scaled_accel.z + zero_g), 0l, MAX_VALUE))}); } void EmulateCursor(MotionState* state, ControllerEmu::Cursor* ir_group, float time_elapsed) @@ -311,28 +311,24 @@ void EmulateIMUCursor(IMUCursorState* state, ControllerEmu::IMUCursor* imu_ir_gr } // Apply rotation from gyro data. - const auto gyro_rotation = Common::Matrix33::FromQuaternion(ang_vel->x * time_elapsed / -2, - ang_vel->y * time_elapsed / -2, - ang_vel->z * time_elapsed / -2, 1); + const auto gyro_rotation = GetMatrixFromGyroscope(*ang_vel * -1 * time_elapsed); state->rotation = gyro_rotation * state->rotation; // If we have some non-zero accel data use it to adjust gyro drift. constexpr auto ACCEL_WEIGHT = 0.02f; auto const accel = imu_accelerometer_group->GetState().value_or(Common::Vec3{}); if (accel.LengthSquared()) - state->rotation = ComplementaryFilter(accel, state->rotation, ACCEL_WEIGHT); - - const auto inv_rotation = state->rotation.Inverted(); + state->rotation = ComplementaryFilter(state->rotation, accel, ACCEL_WEIGHT); // Clamp yaw within configured bounds. - const auto yaw = std::asin((inv_rotation * Common::Vec3{0, 1, 0}).x); + const auto yaw = GetYaw(state->rotation); const auto max_yaw = float(imu_ir_group->GetTotalYaw() / 2); auto target_yaw = std::clamp(yaw, -max_yaw, max_yaw); // Handle the "Recenter" button being pressed. if (imu_ir_group->controls[0]->GetState()) { - state->recentered_pitch = std::asin((inv_rotation * Common::Vec3{0, 1, 0}).z); + state->recentered_pitch = GetPitch(state->rotation); target_yaw = 0; } @@ -390,10 +386,33 @@ Common::Matrix33 GetMatrixFromAcceleration(const Common::Vec3& accel) axis.LengthSquared() ? axis.Normalized() : Common::Vec3{0, 1, 0}); } +Common::Matrix33 GetMatrixFromGyroscope(const Common::Vec3& gyro) +{ + return Common::Matrix33::FromQuaternion(gyro.x / 2, gyro.y / 2, gyro.z / 2, 1); +} + Common::Matrix33 GetRotationalMatrix(const Common::Vec3& angle) { return Common::Matrix33::RotateZ(angle.z) * Common::Matrix33::RotateY(angle.y) * Common::Matrix33::RotateX(angle.x); } +float GetPitch(const Common::Matrix33& world_rotation) +{ + const auto vec = world_rotation * Common::Vec3{0, 0, 1}; + return std::atan2(vec.y, Common::Vec2(vec.x, vec.z).Length()); +} + +float GetRoll(const Common::Matrix33& world_rotation) +{ + const auto vec = world_rotation * Common::Vec3{0, 0, 1}; + return std::atan2(vec.x, vec.z); +} + +float GetYaw(const Common::Matrix33& world_rotation) +{ + const auto vec = world_rotation.Inverted() * Common::Vec3{0, 1, 0}; + return std::atan2(vec.x, vec.y); +} + } // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/Dynamics.h b/Source/Core/Core/HW/WiimoteEmu/Dynamics.h index b72285a1590a..ca70f60ee442 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Dynamics.h +++ b/Source/Core/Core/HW/WiimoteEmu/Dynamics.h @@ -54,12 +54,26 @@ struct MotionState : PositionalState, RotationalState { }; +// Note that 'gyroscope' is rotation of world around device. +// Alternative accelerometer_normal can be supplied to correct from non-accelerometer data. +// e.g. Used for yaw/pitch correction with IR data. +Common::Matrix33 ComplementaryFilter(const Common::Matrix33& gyroscope, + const Common::Vec3& accelerometer, float accel_weight, + const Common::Vec3& accelerometer_normal = {0, 0, 1}); + // Estimate orientation from accelerometer data. Common::Matrix33 GetMatrixFromAcceleration(const Common::Vec3& accel); +// Get a rotation matrix from current gyro data. +Common::Matrix33 GetMatrixFromGyroscope(const Common::Vec3& gyro); + // Build a rotational matrix from euler angles. Common::Matrix33 GetRotationalMatrix(const Common::Vec3& angle); +float GetPitch(const Common::Matrix33& world_rotation); +float GetRoll(const Common::Matrix33& world_rotation); +float GetYaw(const Common::Matrix33& world_rotation); + void ApproachPositionWithJerk(PositionalState* state, const Common::Vec3& target, const Common::Vec3& max_jerk, float time_elapsed); @@ -75,7 +89,6 @@ void EmulateIMUCursor(IMUCursorState* state, ControllerEmu::IMUCursor* imu_ir_gr ControllerEmu::IMUGyroscope* imu_gyroscope_group, float time_elapsed); // Convert m/s/s acceleration data to the format used by Wiimote/Nunchuk (10-bit unsigned integers). -WiimoteCommon::DataReportBuilder::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g, - u16 one_g); +WiimoteCommon::AccelData ConvertAccelData(const Common::Vec3& accel, u16 zero_g, u16 one_g); } // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp b/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp index a3ea63161d87..478bc0f0f8a4 100644 --- a/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/EmuSubroutines.cpp @@ -236,10 +236,6 @@ void Wiimote::HandleRequestStatus(const OutputReportRequestStatus&) // Update status struct m_status.extension = m_extension_port.IsDeviceConnected(); - // Based on testing, old WiiLi.org docs, and WiiUse library: - // Max battery level seems to be 0xc8 (decimal 200) - constexpr u8 MAX_BATTERY_LEVEL = 0xc8; - m_status.battery = u8(std::lround(m_battery_setting.GetValue() / 100 * MAX_BATTERY_LEVEL)); if (Core::WantsDeterminism()) diff --git a/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm b/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm index 41a09ce363a3..de652db75614 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm +++ b/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm @@ -257,12 +257,13 @@ @interface ConnectBT : NSObject @implementation SearchBT - (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry*)sender error:(IOReturn)error - aborted:(BOOL)aborted { + aborted:(BOOL)aborted +{ done = true; } -- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender - device:(IOBluetoothDevice*)device { +- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender device:(IOBluetoothDevice*)device +{ NOTICE_LOG(WIIMOTE, "Discovered Bluetooth device at %s: %s", [[device addressString] UTF8String], [[device name] UTF8String]); @@ -274,11 +275,12 @@ - (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry*)sender @implementation ConnectBT - (void)l2capChannelData:(IOBluetoothL2CAPChannel*)l2capChannel data:(unsigned char*)data - length:(NSUInteger)length { + length:(NSUInteger)length +{ IOBluetoothDevice* device = [l2capChannel device]; WiimoteReal::WiimoteDarwin* wm = nullptr; - std::lock_guard lk(WiimoteReal::g_wiimotes_mutex); + std::lock_guard lk(WiimoteReal::g_wiimotes_mutex); for (int i = 0; i < MAX_WIIMOTES; i++) { @@ -314,11 +316,12 @@ - (void)l2capChannelData:(IOBluetoothL2CAPChannel*)l2capChannel CFRunLoopStop(CFRunLoopGetCurrent()); } -- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel { +- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel +{ IOBluetoothDevice* device = [l2capChannel device]; WiimoteReal::WiimoteDarwin* wm = nullptr; - std::lock_guard lk(WiimoteReal::g_wiimotes_mutex); + std::lock_guard lk(WiimoteReal::g_wiimotes_mutex); for (int i = 0; i < MAX_WIIMOTES; i++) { diff --git a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp index 8822621de48e..595bc5922c4e 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.cpp @@ -26,6 +26,7 @@ #include "Core/HW/WiimoteReal/IOWin.h" #include "Core/HW/WiimoteReal/IOdarwin.h" #include "Core/HW/WiimoteReal/IOhidapi.h" +#include "InputCommon/ControllerInterface/Wiimote/Wiimote.h" #include "InputCommon/InputConfig.h" #include "SFML/Network.hpp" @@ -35,7 +36,7 @@ namespace WiimoteReal using namespace WiimoteCommon; static void TryToConnectBalanceBoard(std::unique_ptr); -static void TryToConnectWiimote(std::unique_ptr); +static bool TryToConnectWiimoteToSlot(std::unique_ptr&, unsigned int); static void HandleWiimoteDisconnect(int index); static bool g_real_wiimotes_initialized = false; @@ -45,7 +46,7 @@ static bool g_real_wiimotes_initialized = false; static std::unordered_set s_known_ids; static std::mutex s_known_ids_mutex; -std::mutex g_wiimotes_mutex; +std::recursive_mutex g_wiimotes_mutex; // Real wii remotes assigned to a particular slot. std::unique_ptr g_wiimotes[MAX_BBMOTES]; @@ -72,22 +73,64 @@ std::vector g_wiimote_pool; WiimoteScanner g_wiimote_scanner; -static void ProcessWiimotePool() +// Attempt to fill a real wiimote slot from the pool or by stealing from ControllerInterface. +static void TryToFillWiimoteSlot(u32 index) { - std::lock_guard wm_lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); - for (auto it = g_wiimote_pool.begin(); it != g_wiimote_pool.end();) + if (g_wiimotes[index] || WiimoteCommon::GetSource(index) != WiimoteSource::Real) + return; + + // If the pool is empty, attempt to steal from ControllerInterface. + if (g_wiimote_pool.empty()) { - if (it->IsExpired()) - { - INFO_LOG(WIIMOTE, "Removing expired wiimote pool entry."); - it = g_wiimote_pool.erase(it); - } - else - { - ++it; - } + ciface::Wiimote::ReleaseDevices(1); + + // Still empty? + if (g_wiimote_pool.empty()) + return; } + + if (TryToConnectWiimoteToSlot(g_wiimote_pool.front().wiimote, index)) + g_wiimote_pool.erase(g_wiimote_pool.begin()); +} + +// Attempts to fill enabled real wiimote slots. +// Push/pull wiimotes to/from ControllerInterface as needed. +void ProcessWiimotePool() +{ + std::lock_guard lk(g_wiimotes_mutex); + + for (u32 index = 0; index != MAX_WIIMOTES; ++index) + TryToFillWiimoteSlot(index); + + if (SConfig::GetInstance().connect_wiimotes_for_ciface) + { + for (auto& entry : g_wiimote_pool) + ciface::Wiimote::AddDevice(std::move(entry.wiimote)); + + g_wiimote_pool.clear(); + } + else + { + ciface::Wiimote::ReleaseDevices(); + } +} + +void AddWiimoteToPool(std::unique_ptr wiimote) +{ + // Our real wiimote class requires an index. + // Within the pool it's only going to be used for logging purposes. + static constexpr int POOL_WIIMOTE_INDEX = 99; + + if (!wiimote->Connect(POOL_WIIMOTE_INDEX)) + { + ERROR_LOG(WIIMOTE, "Failed to connect real wiimote."); + return; + } + + std::lock_guard lk(g_wiimotes_mutex); + g_wiimote_pool.emplace_back(WiimotePoolEntry{std::move(wiimote)}); } Wiimote::Wiimote() = default; @@ -165,7 +208,7 @@ void Wiimote::ResetDataReporting() OutputReportMode rpt = {}; rpt.mode = InputReportID::ReportCore; rpt.continuous = 0; - QueueReport(OutputReportID::ReportMode, &rpt, sizeof(rpt)); + QueueReport(rpt); } void Wiimote::ClearReadQueue() @@ -241,11 +284,11 @@ void Wiimote::InterruptChannel(const u16 channel, const void* const data, const else if (rpt[1] == u8(OutputReportID::SpeakerData) && (!SConfig::GetInstance().m_WiimoteEnableSpeaker || !m_speaker_enable || m_speaker_mute)) { + rpt.resize(3); // Translate undesired speaker data reports into rumble reports. rpt[1] = u8(OutputReportID::Rumble); // Keep only the rumble bit. rpt[2] &= 0x1; - rpt.resize(3); } WriteReport(std::move(rpt)); @@ -380,11 +423,16 @@ static bool IsDataReport(const Report& rpt) return rpt.size() >= 2 && rpt[1] >= u8(InputReportID::ReportCore); } +bool Wiimote::GetNextReport(Report* report) +{ + return m_read_reports.Pop(*report); +} + // Returns the next report that should be sent Report& Wiimote::ProcessReadQueue() { // Pop through the queued reports - while (m_read_reports.Pop(m_last_input_report)) + while (GetNextReport(&m_last_input_report)) { if (!IsDataReport(m_last_input_report)) { @@ -452,26 +500,16 @@ void Wiimote::Prepare() bool Wiimote::PrepareOnThread() { - // core buttons, no continuous reporting - // TODO: use the structs.. - u8 static const mode_report[] = {WR_SET_REPORT | BT_OUTPUT, u8(OutputReportID::ReportMode), 0, + // Set reporting mode to non-continuous core buttons and turn on rumble. + u8 static const mode_report[] = {WR_SET_REPORT | BT_OUTPUT, u8(OutputReportID::ReportMode), 1, u8(InputReportID::ReportCore)}; - // Set the active LEDs and turn on rumble. - u8 static led_report[] = {WR_SET_REPORT | BT_OUTPUT, u8(OutputReportID::LED), 0}; - led_report[2] = u8(u8(LED::LED_1) << (m_index % WIIMOTE_BALANCE_BOARD) | 0x1); - - // Turn off rumble - u8 static const rumble_report[] = {WR_SET_REPORT | BT_OUTPUT, u8(OutputReportID::Rumble), 0}; - - // Request status report + // Request status and turn off rumble. u8 static const req_status_report[] = {WR_SET_REPORT | BT_OUTPUT, u8(OutputReportID::RequestStatus), 0}; - // TODO: check for sane response? - return (IOWrite(mode_report, sizeof(mode_report)) && IOWrite(led_report, sizeof(led_report)) && - (Common::SleepCurrentThread(200), IOWrite(rumble_report, sizeof(rumble_report))) && - IOWrite(req_status_report, sizeof(req_status_report))); + return IOWrite(mode_report, sizeof(mode_report)) && + (Common::SleepCurrentThread(200), IOWrite(req_status_report, sizeof(req_status_report))); } void Wiimote::EmuStart() @@ -499,32 +537,20 @@ void Wiimote::EmuPause() DisablePowerAssertionInternal(); } -static unsigned int CalculateConnectedWiimotes() -{ - std::lock_guard lk(g_wiimotes_mutex); - unsigned int connected_wiimotes = 0; - for (unsigned int i = 0; i < MAX_WIIMOTES; ++i) - if (g_wiimotes[i]) - ++connected_wiimotes; - - return connected_wiimotes; -} - static unsigned int CalculateWantedWiimotes() { - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); // Figure out how many real Wiimotes are required unsigned int wanted_wiimotes = 0; for (unsigned int i = 0; i < MAX_WIIMOTES; ++i) if (WiimoteCommon::GetSource(i) == WiimoteSource::Real && !g_wiimotes[i]) ++wanted_wiimotes; - return wanted_wiimotes; } static unsigned int CalculateWantedBB() { - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); unsigned int wanted_bb = 0; if (WiimoteCommon::GetSource(WIIMOTE_BALANCE_BOARD) == WiimoteSource::Real && !g_wiimotes[WIIMOTE_BALANCE_BOARD]) @@ -564,14 +590,63 @@ bool WiimoteScanner::IsReady() const static void CheckForDisconnectedWiimotes() { - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); for (unsigned int i = 0; i < MAX_BBMOTES; ++i) if (g_wiimotes[i] && !g_wiimotes[i]->IsConnected()) HandleWiimoteDisconnect(i); } +void WiimoteScanner::PoolThreadFunc() +{ + Common::SetCurrentThreadName("Wiimote Pool Thread"); + + // Toggle between 1010 and 0101. + u8 led_value = 0b1010; + + auto next_time = std::chrono::steady_clock::now(); + + while (m_scan_thread_running.IsSet()) + { + std::this_thread::sleep_until(next_time); + next_time += std::chrono::milliseconds(250); + + std::lock_guard lk(g_wiimotes_mutex); + + // Remove stale pool entries. + for (auto it = g_wiimote_pool.begin(); it != g_wiimote_pool.end();) + { + if (!it->wiimote->IsConnected()) + { + INFO_LOG(WIIMOTE, "Removing disconnected wiimote pool entry."); + it = g_wiimote_pool.erase(it); + } + else if (it->IsExpired()) + { + INFO_LOG(WIIMOTE, "Removing expired wiimote pool entry."); + it = g_wiimote_pool.erase(it); + } + else + { + ++it; + } + } + + // Make wiimote pool LEDs dance. + for (auto& wiimote : g_wiimote_pool) + { + OutputReportLeds leds = {}; + leds.leds = led_value; + wiimote.wiimote->QueueReport(leds); + } + + led_value ^= 0b1111; + } +} + void WiimoteScanner::ThreadFunc() { + std::thread pool_thread(&WiimoteScanner::PoolThreadFunc, this); + Common::SetCurrentThreadName("Wiimote Scanning Thread"); NOTICE_LOG(WIIMOTE, "Wiimote scanning thread has started."); @@ -594,22 +669,28 @@ void WiimoteScanner::ThreadFunc() { m_scan_mode_changed_event.WaitFor(std::chrono::milliseconds(500)); - ProcessWiimotePool(); + // Does stuff needed to detect disconnects on Windows + for (const auto& backend : m_backends) + backend->Update(); CheckForDisconnectedWiimotes(); if (m_scan_mode.load() == WiimoteScanMode::DO_NOT_SCAN) continue; - if (!g_real_wiimotes_initialized) - continue; - - // Does stuff needed to detect disconnects on Windows - for (const auto& backend : m_backends) - backend->Update(); - - if (0 == CalculateWantedWiimotes() && 0 == CalculateWantedBB()) - continue; + // If we don't want Wiimotes in ControllerInterface, we may not need them at all. + if (!SConfig::GetInstance().connect_wiimotes_for_ciface) + { + // We don't want any remotes in passthrough mode or running in GC mode. + const bool core_running = Core::GetState() != Core::State::Uninitialized; + if (SConfig::GetInstance().m_bt_passthrough_enabled || + (core_running && !SConfig::GetInstance().bWii)) + continue; + + // We don't want any remotes if we already connected everything we need. + if (0 == CalculateWantedWiimotes() && 0 == CalculateWantedBB()) + continue; + } for (const auto& backend : m_backends) { @@ -617,7 +698,7 @@ void WiimoteScanner::ThreadFunc() Wiimote* found_board = nullptr; backend->FindWiimotes(found_wiimotes, found_board); { - std::lock_guard wm_lk(g_wiimotes_mutex); + std::unique_lock wm_lk(g_wiimotes_mutex); for (auto* wiimote : found_wiimotes) { @@ -626,7 +707,8 @@ void WiimoteScanner::ThreadFunc() s_known_ids.insert(wiimote->GetId()); } - TryToConnectWiimote(std::unique_ptr(wiimote)); + AddWiimoteToPool(std::unique_ptr(wiimote)); + ProcessWiimotePool(); } if (found_board) @@ -641,32 +723,32 @@ void WiimoteScanner::ThreadFunc() } } - if (m_scan_mode.load() == WiimoteScanMode::SCAN_ONCE) - m_scan_mode.store(WiimoteScanMode::DO_NOT_SCAN); + // Stop scanning if not in continous mode. + auto scan_mode = WiimoteScanMode::SCAN_ONCE; + m_scan_mode.compare_exchange_strong(scan_mode, WiimoteScanMode::DO_NOT_SCAN); } { std::lock_guard lg(m_backends_mutex); m_backends.clear(); } + + pool_thread.join(); + NOTICE_LOG(WIIMOTE, "Wiimote scanning thread has stopped."); } bool Wiimote::Connect(int index) { m_index = index; - m_need_prepare.Set(); if (!m_run_thread.IsSet()) { + m_need_prepare.Set(); m_run_thread.Set(); StartThread(); m_thread_ready_event.Wait(); } - else - { - IOWakeup(); - } return IsConnected(); } @@ -729,6 +811,11 @@ int Wiimote::GetIndex() const return m_index; } +void Wiimote::SetChannel(u16 channel) +{ + m_channel = channel; +} + void LoadSettings() { std::string ini_filename = File::GetUserPath(D_CONFIG_IDX) + WIIMOTE_INI_NAME ".ini"; @@ -763,8 +850,7 @@ void Initialize(::Wiimote::InitializeMode init_mode) g_wiimote_scanner.StartThread(); } - if (SConfig::GetInstance().m_WiimoteContinuousScanning && - !SConfig::GetInstance().m_bt_passthrough_enabled) + if (SConfig::GetInstance().m_WiimoteContinuousScanning) g_wiimote_scanner.SetScanMode(WiimoteScanMode::CONTINUOUSLY_SCAN); else g_wiimote_scanner.SetScanMode(WiimoteScanMode::DO_NOT_SCAN); @@ -774,7 +860,7 @@ void Initialize(::Wiimote::InitializeMode init_mode) { int timeout = 100; g_wiimote_scanner.SetScanMode(WiimoteScanMode::SCAN_ONCE); - while (CalculateWantedWiimotes() > CalculateConnectedWiimotes() && timeout) + while (CalculateWantedWiimotes() && timeout) { Common::SleepCurrentThread(100); timeout--; @@ -805,9 +891,13 @@ void Shutdown() NOTICE_LOG(WIIMOTE, "WiimoteReal::Shutdown"); - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); for (unsigned int i = 0; i < MAX_BBMOTES; ++i) HandleWiimoteDisconnect(i); + + // Release remotes from ControllerInterface and empty the pool. + ciface::Wiimote::ReleaseDevices(); + g_wiimote_pool.clear(); } void Resume() @@ -836,6 +926,13 @@ static bool TryToConnectWiimoteToSlot(std::unique_ptr& wm, unsigned int return false; } + wm->Prepare(); + + // Set LEDs. + OutputReportLeds led_report = {}; + led_report.leds = u8(1 << (i % WIIMOTE_BALANCE_BOARD)); + wm->QueueReport(led_report); + g_wiimotes[i] = std::move(wm); Core::RunAsCPUThread([i] { ::Wiimote::Connect(i, true); }); @@ -844,22 +941,6 @@ static bool TryToConnectWiimoteToSlot(std::unique_ptr& wm, unsigned int return true; } -static void TryToConnectWiimote(std::unique_ptr wm) -{ - for (unsigned int i = 0; i < MAX_WIIMOTES; ++i) - { - if (TryToConnectWiimoteToSlot(wm, i)) - return; - } - - INFO_LOG(WIIMOTE, "No open slot for real wiimote, adding it to the pool."); - wm->Connect(0); - // Turn on LED 1 and 4 to make it apparant this remote is in the pool. - const u8 led_value = u8(LED::LED_1) | u8(LED::LED_4); - wm->QueueReport(OutputReportID::LED, &led_value, 1); - g_wiimote_pool.emplace_back(WiimotePoolEntry{std::move(wm)}); -} - static void TryToConnectBalanceBoard(std::unique_ptr wm) { if (TryToConnectWiimoteToSlot(wm, WIIMOTE_BALANCE_BOARD)) @@ -882,14 +963,14 @@ void Refresh() void InterruptChannel(int wiimote_number, u16 channel_id, const void* data, u32 size) { - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); if (g_wiimotes[wiimote_number]) g_wiimotes[wiimote_number]->InterruptChannel(channel_id, data, size); } void ControlChannel(int wiimote_number, u16 channel_id, const void* data, u32 size) { - std::lock_guard lk(g_wiimotes_mutex); + std::lock_guard lk(g_wiimotes_mutex); if (g_wiimotes[wiimote_number]) g_wiimotes[wiimote_number]->ControlChannel(channel_id, data, size); } @@ -946,25 +1027,17 @@ bool IsNewWiimote(const std::string& identifier) void HandleWiimoteSourceChange(unsigned int index) { - std::lock_guard wm_lk(g_wiimotes_mutex); + std::lock_guard wm_lk(g_wiimotes_mutex); - if (WiimoteCommon::GetSource(index) != WiimoteSource::Real) - { - if (auto removed_wiimote = std::move(g_wiimotes[index])) - { - removed_wiimote->EmuStop(); - // Try to use this removed wiimote in another slot. - TryToConnectWiimote(std::move(removed_wiimote)); - } - } - else if (WiimoteCommon::GetSource(index) == WiimoteSource::Real) - { - // Try to fill this slot from the pool. - if (!g_wiimote_pool.empty()) - { - if (TryToConnectWiimoteToSlot(g_wiimote_pool.front().wiimote, index)) - g_wiimote_pool.erase(g_wiimote_pool.begin()); - } - } + if (auto removed_wiimote = std::move(g_wiimotes[index])) + AddWiimoteToPool(std::move(removed_wiimote)); + + ProcessWiimotePool(); } -}; // namespace WiimoteReal + +void HandleWiimotesInControllerInterfaceSettingChange() +{ + ProcessWiimotePool(); +} + +} // namespace WiimoteReal diff --git a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h index 4c04a492ac80..5b20694ca2be 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h @@ -65,6 +65,7 @@ class Wiimote void Update(); bool CheckForButtonPress(); + bool GetNextReport(Report* report); Report& ProcessReadQueue(); void Read(); @@ -101,8 +102,16 @@ class Wiimote void QueueReport(WiimoteCommon::OutputReportID rpt_id, const void* data, unsigned int size); + template + void QueueReport(const T& report) + { + QueueReport(report.REPORT_ID, &report, sizeof(report)); + } + int GetIndex() const; + void SetChannel(u16 channel); + protected: Wiimote(); @@ -173,6 +182,7 @@ class WiimoteScanner private: void ThreadFunc(); + void PoolThreadFunc(); std::vector> m_backends; mutable std::mutex m_backends_mutex; @@ -183,10 +193,13 @@ class WiimoteScanner std::atomic m_scan_mode{WiimoteScanMode::DO_NOT_SCAN}; }; -extern std::mutex g_wiimotes_mutex; +// Mutex is recursive as ControllerInterface may call AddWiimoteToPool within ProcessWiimotePool. +extern std::recursive_mutex g_wiimotes_mutex; extern WiimoteScanner g_wiimote_scanner; extern std::unique_ptr g_wiimotes[MAX_BBMOTES]; +void AddWiimoteToPool(std::unique_ptr); + void InterruptChannel(int wiimote_number, u16 channel_id, const void* data, u32 size); void ControlChannel(int wiimote_number, u16 channel_id, const void* data, u32 size); void Update(int wiimote_number); @@ -202,4 +215,7 @@ void HandleWiimoteSourceChange(unsigned int wiimote_number); void InitAdapterClass(); #endif +void HandleWiimotesInControllerInterfaceSettingChange(); +void ProcessWiimotePool(); + } // namespace WiimoteReal diff --git a/Source/Core/DolphinQt/Config/ControllersWindow.cpp b/Source/Core/DolphinQt/Config/ControllersWindow.cpp index 1c60cd4682ed..65ab32cbe6c5 100644 --- a/Source/Core/DolphinQt/Config/ControllersWindow.cpp +++ b/Source/Core/DolphinQt/Config/ControllersWindow.cpp @@ -72,8 +72,6 @@ ControllersWindow::ControllersWindow(QWidget* parent) : QDialog(parent) CreateMainLayout(); LoadSettings(); ConnectWidgets(); - - OnEmulationStateChanged(Core::GetState() != Core::State::Uninitialized); } void ControllersWindow::CreateGamecubeLayout() @@ -157,6 +155,7 @@ void ControllersWindow::CreateWiimoteLayout() m_wiimote_continuous_scanning = new QCheckBox(tr("Continuous Scanning")); m_wiimote_real_balance_board = new QCheckBox(tr("Real Balance Board")); m_wiimote_speaker_data = new QCheckBox(tr("Enable Speaker Data")); + m_wiimote_ciface = new QCheckBox(tr("Connect Wii Remotes for Emulated Controllers")); m_wiimote_layout->setVerticalSpacing(7); m_wiimote_layout->setColumnMinimumWidth(0, GetRadioButtonIndicatorWidth() - @@ -192,12 +191,14 @@ void ControllersWindow::CreateWiimoteLayout() m_wiimote_layout->addWidget(wm_button, wm_row, 3); } - int continuous_scanning_row = m_wiimote_layout->rowCount(); - m_wiimote_layout->addWidget(m_wiimote_continuous_scanning, continuous_scanning_row, 1, 1, 2); - m_wiimote_layout->addWidget(m_wiimote_refresh, continuous_scanning_row, 3); - m_wiimote_layout->addWidget(m_wiimote_real_balance_board, m_wiimote_layout->rowCount(), 1, 1, -1); m_wiimote_layout->addWidget(m_wiimote_speaker_data, m_wiimote_layout->rowCount(), 1, 1, -1); + + m_wiimote_layout->addWidget(m_wiimote_ciface, m_wiimote_layout->rowCount(), 0, 1, -1); + + int continuous_scanning_row = m_wiimote_layout->rowCount(); + m_wiimote_layout->addWidget(m_wiimote_continuous_scanning, continuous_scanning_row, 0, 1, 3); + m_wiimote_layout->addWidget(m_wiimote_refresh, continuous_scanning_row, 3); } void ControllersWindow::CreateCommonLayout() @@ -232,10 +233,15 @@ void ControllersWindow::ConnectWidgets() { connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(&Settings::Instance(), &Settings::EmulationStateChanged, this, - [=](Core::State state) { OnEmulationStateChanged(state != Core::State::Uninitialized); }); + &ControllersWindow::UpdateDisabledWiimoteControls); connect(m_wiimote_passthrough, &QRadioButton::toggled, this, &ControllersWindow::OnWiimoteModeChanged); + connect(m_wiimote_ciface, &QCheckBox::toggled, this, &ControllersWindow::OnWiimoteModeChanged); + connect(m_wiimote_ciface, &QCheckBox::toggled, this, + &WiimoteReal::HandleWiimotesInControllerInterfaceSettingChange); + connect(m_wiimote_continuous_scanning, &QCheckBox::toggled, this, + &ControllersWindow::OnWiimoteModeChanged); connect(m_common_bg_input, &QCheckBox::toggled, this, &ControllersWindow::SaveSettings); connect(m_common_configure_controller_interface, &QPushButton::clicked, this, @@ -259,7 +265,7 @@ void ControllersWindow::ConnectWidgets() &ControllersWindow::SaveSettings); connect(m_wiimote_boxes[i], static_cast(&QComboBox::currentIndexChanged), this, - &ControllersWindow::OnWiimoteTypeChanged); + &ControllersWindow::OnWiimoteModeChanged); connect(m_wiimote_buttons[i], &QPushButton::clicked, this, &ControllersWindow::OnWiimoteConfigure); @@ -273,45 +279,50 @@ void ControllersWindow::ConnectWidgets() } } -void ControllersWindow::OnWiimoteModeChanged(bool passthrough) +void ControllersWindow::OnWiimoteModeChanged() { SaveSettings(); - m_wiimote_sync->setEnabled(passthrough); - m_wiimote_reset->setEnabled(passthrough); + // Make sure continuous scanning setting is applied. + WiimoteReal::Initialize(::Wiimote::InitializeMode::DO_NOT_WAIT_FOR_WIIMOTES); - for (size_t i = 0; i < m_wiimote_groups.size(); i++) - { - const int index = m_wiimote_boxes[i]->currentIndex(); + UpdateDisabledWiimoteControls(); +} - if (i < 2) - m_wiimote_pt_labels[i]->setEnabled(passthrough); +void ControllersWindow::UpdateDisabledWiimoteControls() +{ + const bool running = Core::GetState() != Core::State::Uninitialized; - m_wiimote_labels[i]->setEnabled(!passthrough); - m_wiimote_boxes[i]->setEnabled(!passthrough); - m_wiimote_buttons[i]->setEnabled(!passthrough && index != 0 && index != 2); - } + m_wiimote_emu->setEnabled(!running); + m_wiimote_passthrough->setEnabled(!running); - m_wiimote_refresh->setEnabled(!passthrough); - m_wiimote_real_balance_board->setEnabled(!passthrough); - m_wiimote_speaker_data->setEnabled(!passthrough); - m_wiimote_continuous_scanning->setEnabled(!passthrough); -} + const bool running_gc = running && !SConfig::GetInstance().bWii; + const bool enable_passthrough = m_wiimote_passthrough->isChecked() && !running_gc; + const bool enable_emu_bt = !m_wiimote_passthrough->isChecked() && !running_gc; + + m_wiimote_sync->setEnabled(enable_passthrough); + m_wiimote_reset->setEnabled(enable_passthrough); + + for (auto* pt_label : m_wiimote_pt_labels) + pt_label->setEnabled(enable_passthrough); -void ControllersWindow::OnWiimoteTypeChanged(int type) -{ - const auto* box = static_cast(QObject::sender()); for (size_t i = 0; i < m_wiimote_groups.size(); i++) { - if (m_wiimote_boxes[i] == box) - { - const int index = box->currentIndex(); - m_wiimote_buttons[i]->setEnabled(index != 0 && index != 2); - return; - } + m_wiimote_labels[i]->setEnabled(enable_emu_bt); + m_wiimote_boxes[i]->setEnabled(enable_emu_bt); + + const bool is_emu_wiimote = m_wiimote_boxes[i]->currentIndex() == 1; + m_wiimote_buttons[i]->setEnabled(enable_emu_bt && is_emu_wiimote); } - SaveSettings(); + m_wiimote_real_balance_board->setEnabled(enable_emu_bt); + m_wiimote_speaker_data->setEnabled(enable_emu_bt); + + const bool ciface_wiimotes = m_wiimote_ciface->isChecked(); + + m_wiimote_refresh->setEnabled((enable_emu_bt || ciface_wiimotes) && + !m_wiimote_continuous_scanning->isChecked()); + m_wiimote_continuous_scanning->setEnabled(enable_emu_bt || ciface_wiimotes); } void ControllersWindow::OnGCTypeChanged(int type) @@ -375,30 +386,6 @@ void ControllersWindow::OnWiimoteRefreshPressed() WiimoteReal::Refresh(); } -void ControllersWindow::OnEmulationStateChanged(bool running) -{ - const bool passthrough = SConfig::GetInstance().m_bt_passthrough_enabled; - - if (!SConfig::GetInstance().bWii) - { - m_wiimote_sync->setEnabled(!running && passthrough); - m_wiimote_reset->setEnabled(!running && passthrough); - - for (size_t i = 0; i < m_wiimote_groups.size(); i++) - m_wiimote_boxes[i]->setEnabled(!running && !passthrough); - } - - m_wiimote_emu->setEnabled(!running); - m_wiimote_passthrough->setEnabled(!running); - - if (!SConfig::GetInstance().bWii) - { - m_wiimote_real_balance_board->setEnabled(!running && !passthrough); - m_wiimote_continuous_scanning->setEnabled(!running && !passthrough); - m_wiimote_speaker_data->setEnabled(!running && !passthrough); - } -} - void ControllersWindow::OnGCPadConfigure() { size_t index; @@ -489,14 +476,12 @@ void ControllersWindow::LoadSettings() m_gc_controller_boxes[i]->setCurrentIndex(*gc_index); m_gc_buttons[i]->setEnabled(*gc_index != 0 && *gc_index != 6); } - - const WiimoteSource source = WiimoteCommon::GetSource(int(i)); - m_wiimote_boxes[i]->setCurrentIndex(int(source)); - m_wiimote_buttons[i]->setEnabled(source == WiimoteSource::Emulated); + m_wiimote_boxes[i]->setCurrentIndex(int(WiimoteCommon::GetSource(u32(i)))); } m_wiimote_real_balance_board->setChecked(WiimoteCommon::GetSource(WIIMOTE_BALANCE_BOARD) == WiimoteSource::Real); m_wiimote_speaker_data->setChecked(SConfig::GetInstance().m_WiimoteEnableSpeaker); + m_wiimote_ciface->setChecked(SConfig::GetInstance().connect_wiimotes_for_ciface); m_wiimote_continuous_scanning->setChecked(SConfig::GetInstance().m_WiimoteContinuousScanning); m_common_bg_input->setChecked(SConfig::GetInstance().m_BackgroundInput); @@ -506,12 +491,13 @@ void ControllersWindow::LoadSettings() else m_wiimote_emu->setChecked(true); - OnWiimoteModeChanged(SConfig::GetInstance().m_bt_passthrough_enabled); + OnWiimoteModeChanged(); } void ControllersWindow::SaveSettings() { SConfig::GetInstance().m_WiimoteEnableSpeaker = m_wiimote_speaker_data->isChecked(); + SConfig::GetInstance().connect_wiimotes_for_ciface = m_wiimote_ciface->isChecked(); SConfig::GetInstance().m_WiimoteContinuousScanning = m_wiimote_continuous_scanning->isChecked(); SConfig::GetInstance().m_bt_passthrough_enabled = m_wiimote_passthrough->isChecked(); SConfig::GetInstance().m_BackgroundInput = m_common_bg_input->isChecked(); @@ -522,9 +508,8 @@ void ControllersWindow::SaveSettings() for (size_t i = 0; i < m_wiimote_groups.size(); i++) { - const auto source = WiimoteSource(m_wiimote_boxes[i]->currentIndex()); - m_wiimote_buttons[i]->setEnabled(source == WiimoteSource::Emulated); - WiimoteCommon::SetSource(static_cast(i), source); + const int index = m_wiimote_boxes[i]->currentIndex(); + WiimoteCommon::SetSource(u32(i), WiimoteSource(index)); } UICommon::SaveWiimoteSources(); diff --git a/Source/Core/DolphinQt/Config/ControllersWindow.h b/Source/Core/DolphinQt/Config/ControllersWindow.h index 368ef36cdb48..93807be9b65b 100644 --- a/Source/Core/DolphinQt/Config/ControllersWindow.h +++ b/Source/Core/DolphinQt/Config/ControllersWindow.h @@ -27,9 +27,8 @@ class ControllersWindow final : public QDialog explicit ControllersWindow(QWidget* parent); private: - void OnEmulationStateChanged(bool running); - void OnWiimoteModeChanged(bool passthrough); - void OnWiimoteTypeChanged(int state); + void OnWiimoteModeChanged(); + void UpdateDisabledWiimoteControls(); void OnGCTypeChanged(int state); void SaveSettings(); void OnBluetoothPassthroughSyncPressed(); @@ -72,6 +71,7 @@ class ControllersWindow final : public QDialog QCheckBox* m_wiimote_continuous_scanning; QCheckBox* m_wiimote_real_balance_board; QCheckBox* m_wiimote_speaker_data; + QCheckBox* m_wiimote_ciface; QPushButton* m_wiimote_refresh; // Common diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp index 303060133102..e6549e48d66d 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -806,9 +806,8 @@ void GyroMappingIndicator::paintEvent(QPaintEvent*) const auto jitter = raw_gyro_state - m_previous_velocity; m_previous_velocity = raw_gyro_state; - m_state *= Common::Matrix33::FromQuaternion(angular_velocity.x / -INDICATOR_UPDATE_FREQ / 2, - angular_velocity.y / INDICATOR_UPDATE_FREQ / 2, - angular_velocity.z / -INDICATOR_UPDATE_FREQ / 2, 1); + m_state *= WiimoteEmu::GetMatrixFromGyroscope(angular_velocity * Common::Vec3(-1, +1, -1) / + INDICATOR_UPDATE_FREQ); // Reset orientation when stable for a bit: constexpr u32 STABLE_RESET_STEPS = INDICATOR_UPDATE_FREQ; diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index 30b2a4714c05..d5efe5824850 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -50,6 +50,8 @@ add_library(inputcommon ControllerInterface/ControllerInterface.h ControllerInterface/Device.cpp ControllerInterface/Device.h + ControllerInterface/Wiimote/Wiimote.cpp + ControllerInterface/Wiimote/Wiimote.h ControlReference/ControlReference.cpp ControlReference/ControlReference.h ControlReference/ExpressionParser.cpp diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp index b5a621e43bf3..382bd8c2a4c4 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/Cursor.cpp @@ -50,7 +50,7 @@ Cursor::Cursor(std::string name, std::string ui_name) _trans("°"), // i18n: Refers to emulated wii remote movements. _trans("Total rotation about the yaw axis.")}, - 15, 0, 180); + 15, 0, 360); AddSetting(&m_pitch_setting, // i18n: Refers to an amount of rotational movement about the "pitch" axis. @@ -59,7 +59,7 @@ Cursor::Cursor(std::string name, std::string ui_name) _trans("°"), // i18n: Refers to emulated wii remote movements. _trans("Total rotation about the pitch axis.")}, - 15, 0, 180); + 15, 0, 360); AddSetting(&m_relative_setting, {_trans("Relative Input")}, false); AddSetting(&m_autohide_setting, {_trans("Auto-Hide")}, false); diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp index faa6de185e52..833f288da5c1 100644 --- a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp +++ b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp @@ -7,6 +7,7 @@ #include #include "Common/Logging/Log.h" +#include "Core/HW/WiimoteReal/WiimoteReal.h" #ifdef CIFACE_USE_WIN32 #include "InputCommon/ControllerInterface/Win32/Win32.h" @@ -131,7 +132,8 @@ void ControllerInterface::RefreshDevices() #ifdef CIFACE_USE_DUALSHOCKUDPCLIENT ciface::DualShockUDPClient::PopulateDevices(); #endif - ciface::Wiimote::PopulateDevices(); + + WiimoteReal::ProcessWiimotePool(); m_is_populating_devices = false; InvokeDevicesChangedCallbacks(); diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp new file mode 100644 index 000000000000..c5d756fb1590 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp @@ -0,0 +1,1631 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include "InputCommon/ControllerInterface/Wiimote/Wiimote.h" + +#include "Common/BitUtils.h" +#include "Common/Logging/Log.h" +#include "Common/MathUtil.h" +#include "Core/Config/SYSCONFSettings.h" +#include "Core/HW/WiimoteEmu/ExtensionPort.h" +#include "Core/HW/WiimoteEmu/WiimoteEmu.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" +#include "InputCommon/ControllerInterface/ControllerInterface.h" + +namespace ciface::Wiimote +{ +static constexpr char SOURCE_NAME[] = "Bluetooth"; + +static constexpr size_t IR_SENSITIVITY_LEVEL_COUNT = 5; + +template +class Button final : public Core::Device::Input +{ +public: + Button(const T* value, std::common_type_t mask, std::string name) + : m_value(*value), m_mask(mask), m_name(std::move(name)) + { + } + + std::string GetName() const override { return m_name; } + + ControlState GetState() const override { return (m_value & m_mask) != 0; } + +private: + const T& m_value; + const T m_mask; + const std::string m_name; +}; + +// GetState returns value divided by supplied "extent". +template +class GenericInput : public Core::Device::Input +{ +public: + GenericInput(const T* value, std::string name, ControlState extent) + : m_value(*value), m_name(std::move(name)), m_extent(extent) + { + } + + bool IsDetectable() override { return Detectable; } + + std::string GetName() const override { return m_name; } + + ControlState GetState() const final override { return ControlState(m_value) / m_extent; } + +protected: + const T& m_value; + const std::string m_name; + const ControlState m_extent; +}; + +template +using AnalogInput = GenericInput; + +template +using UndetectableAnalogInput = GenericInput; + +// GetName() is appended with '-' or '+' based on sign of "extent" value. +template +class SignedInput final : public GenericInput +{ +public: + using GenericInput::GenericInput; + + std::string GetName() const override { return this->m_name + (this->m_extent < 0 ? '-' : '+'); } +}; + +using SignedAnalogInput = SignedInput; +using UndetectableSignedAnalogInput = SignedInput; + +class Motor final : public Core::Device::Output +{ +public: + Motor(ControlState* value) : m_value(*value) {} + + std::string GetName() const override { return "Motor"; } + + void SetState(ControlState state) override { m_value = state; } + +private: + ControlState& m_value; +}; + +template +void Device::QueueReport(T&& report, std::function ack_callback) +{ + // Maintain proper rumble state. + report.rumble = m_rumble; + + m_wiimote->QueueReport(std::forward(report)); + + if (ack_callback) + AddReportHandler(MakeAckHandler(report.REPORT_ID, std::move(ack_callback))); +} + +void AddDevice(std::unique_ptr wiimote) +{ + // Our real wiimote class requires an index. + // Within the pool it's only going to be used for logging purposes. + static constexpr int CIFACE_WIIMOTE_INDEX = 55; + + if (!wiimote->Connect(CIFACE_WIIMOTE_INDEX)) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to connect."); + return; + } + + wiimote->Prepare(); + + // Our silly real wiimote interface needs a non-zero "channel" to not drop input reports. + wiimote->SetChannel(26); + + g_controller_interface.AddDevice(std::make_shared(std::move(wiimote))); +} + +void ReleaseDevices(std::optional count) +{ + u32 removed_devices = 0; + + // Remove up to "count" remotes (or all of them if nullopt). + // Real wiimotes will be added to the pool. + g_controller_interface.RemoveDevice([&](const Core::Device* device) { + if (device->GetSource() != SOURCE_NAME || count == removed_devices) + return false; + + ++removed_devices; + return true; + }); +} + +Device::Device(std::unique_ptr wiimote) : m_wiimote(std::move(wiimote)) +{ + using EmuWiimote = WiimoteEmu::Wiimote; + + // Buttons. + static constexpr std::pair button_masks[] = { + {EmuWiimote::BUTTON_A, "A"}, {EmuWiimote::BUTTON_B, "B"}, + {EmuWiimote::BUTTON_ONE, "1"}, {EmuWiimote::BUTTON_TWO, "2"}, + {EmuWiimote::BUTTON_MINUS, "-"}, {EmuWiimote::BUTTON_PLUS, "+"}, + {EmuWiimote::BUTTON_HOME, "HOME"}, + }; + + for (auto& button : button_masks) + AddInput(new Button(&m_core_data.hex, button.first, button.second)); + + static constexpr u16 dpad_masks[] = { + EmuWiimote::PAD_UP, + EmuWiimote::PAD_DOWN, + EmuWiimote::PAD_LEFT, + EmuWiimote::PAD_RIGHT, + }; + + // Friendly orientation inputs. + static constexpr const char* const rotation_names[] = {"Pitch", "Roll", "Yaw"}; + for (std::size_t i = 0; i != std::size(rotation_names); ++i) + { + AddInput( + new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], -1.f)); + AddInput(new UndetectableSignedAnalogInput(&m_rotation_inputs.data[i], rotation_names[i], 1.f)); + } + + // Raw accelerometer. + for (std::size_t i = 0; i != std::size(dpad_masks); ++i) + AddInput(new Button(&m_core_data.hex, dpad_masks[i], named_directions[i])); + + static constexpr std::array, 3> accel_names = {{ + {"Accel Left", "Accel Right"}, + {"Accel Backward", "Accel Forward"}, + {"Accel Up", "Accel Down"}, + }}; + + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][0], 1)); + AddInput(new UndetectableAnalogInput(&m_accel_data.data[i], accel_names[i][1], -1)); + } + + // IR data. + static constexpr const char* const ir_names[] = {"IR Center X", "IR Center Y"}; + for (std::size_t i = 0; i != std::size(ir_names); ++i) + { + AddInput( + new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], -1.f)); + AddInput( + new UndetectableSignedAnalogInput(&m_ir_state.center_position.data[i], ir_names[i], 1.f)); + } + + AddInput(new UndetectableAnalogInput(&m_ir_state.is_hidden, "IR Hidden", 1)); + + // Raw gyroscope. + static constexpr std::array, 3> gyro_names = {{ + {"Gyro Pitch Down", "Gyro Pitch Up"}, + {"Gyro Roll Left", "Gyro Roll Right"}, + {"Gyro Yaw Left", "Gyro Yaw Right"}, + }}; + + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput( + new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][0], 1)); + AddInput( + new UndetectableAnalogInput(&m_mplus_state.gyro_data.data[i], gyro_names[i][1], -1)); + } + + using WiimoteEmu::Nunchuk; + const std::string nunchuk_prefix = "Nunchuk "; + + // Buttons. + AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_C, nunchuk_prefix + "C")); + AddInput(new Button(&m_nunchuk_state.buttons, Nunchuk::BUTTON_Z, nunchuk_prefix + "Z")); + + // Stick. + static constexpr const char* const nunchuk_stick_names[] = {"X", "Y"}; + for (std::size_t i = 0; i != std::size(nunchuk_stick_names); ++i) + { + AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], + nunchuk_prefix + nunchuk_stick_names[i], -1.f)); + AddInput(new SignedAnalogInput(&m_nunchuk_state.stick.data[i], + nunchuk_prefix + nunchuk_stick_names[i], 1.f)); + } + + // Raw accelerometer. + for (std::size_t i = 0; i != m_accel_data.data.size(); ++i) + { + AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], + nunchuk_prefix + accel_names[i][0], 1)); + AddInput(new UndetectableAnalogInput(&m_nunchuk_state.accel.data[i], + nunchuk_prefix + accel_names[i][1], -1)); + } + + using WiimoteEmu::Classic; + const std::string classic_prefix = "Classic "; + + // Buttons. + static constexpr u16 classic_dpad_masks[] = { + Classic::PAD_UP, + Classic::PAD_DOWN, + Classic::PAD_LEFT, + Classic::PAD_RIGHT, + }; + + for (std::size_t i = 0; i != std::size(classic_dpad_masks); ++i) + AddInput(new Button(&m_classic_state.buttons, classic_dpad_masks[i], + classic_prefix + named_directions[i])); + + static constexpr u16 classic_button_masks[] = { + Classic::BUTTON_A, Classic::BUTTON_B, Classic::BUTTON_X, Classic::BUTTON_Y, + Classic::TRIGGER_L, Classic::TRIGGER_R, Classic::BUTTON_ZL, Classic::BUTTON_ZR, + Classic::BUTTON_MINUS, Classic::BUTTON_PLUS, Classic::BUTTON_HOME, + }; + + static constexpr const char* const classic_button_names[] = { + "A", "B", "X", "Y", "L", "R", "ZL", "ZR", "-", "+", "HOME", + }; + + for (std::size_t i = 0; i != std::size(classic_button_masks); ++i) + AddInput(new Button(&m_classic_state.buttons, classic_button_masks[i], + classic_prefix + classic_button_names[i])); + + // Sticks. + static constexpr const char* const classic_stick_names[][2] = {{"Left X", "Left Y"}, + {"Right X", "Right Y"}}; + + for (std::size_t s = 0; s != std::size(m_classic_state.sticks); ++s) + { + for (std::size_t i = 0; i != std::size(m_classic_state.sticks[0].data); ++i) + { + AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], + classic_prefix + classic_stick_names[s][i], -1.f)); + AddInput(new SignedAnalogInput(&m_classic_state.sticks[s].data[i], + classic_prefix + classic_stick_names[s][i], 1.f)); + } + } + + // Triggers. + AddInput(new AnalogInput(&m_classic_state.triggers[0], classic_prefix + "L-Analog", 1.f)); + AddInput(new AnalogInput(&m_classic_state.triggers[1], classic_prefix + "R-Analog", 1.f)); + + // Specialty inputs: + AddInput(new UndetectableAnalogInput( + &m_battery, "Battery", WiimoteCommon::MAX_BATTERY_LEVEL / ciface::BATTERY_INPUT_MAX_VALUE)); + AddInput(new UndetectableAnalogInput( + &m_extension_number_input, "Attached Extension", WiimoteEmu::ExtensionNumber(1))); + AddInput(new UndetectableAnalogInput(&m_mplus_attached_input, "Attached MotionPlus", 1)); + + AddOutput(new Motor(&m_rumble_level)); +} + +Device::~Device() +{ + if (!m_wiimote->IsConnected()) + return; + + m_wiimote->EmuStop(); + + INFO_LOG(WIIMOTE, "WiiRemote: Returning remote to pool."); + WiimoteReal::AddWiimoteToPool(std::move(m_wiimote)); +} + +std::string Device::GetName() const +{ + return "Wii Remote"; +} + +std::string Device::GetSource() const +{ + return SOURCE_NAME; +} + +void Device::RunTasks() +{ + if (IsPerformingTask()) + return; + + // Request status. + if (Clock::now() >= m_status_outdated_time) + { + QueueReport(OutputReportRequestStatus()); + + AddReportHandler(std::function( + [this](const InputReportStatus& status) { + DEBUG_LOG(WIIMOTE, "WiiRemote: Received requested status."); + ProcessStatusReport(status); + })); + + return; + } + + // Set LEDs. + const auto desired_leds = GetDesiredLEDValue(); + if (m_leds != desired_leds) + { + OutputReportLeds rpt = {}; + rpt.ack = 1; + rpt.leds = desired_leds; + QueueReport(rpt, [this, desired_leds](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to set LEDs."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: Set LEDs."); + + m_leds = desired_leds; + }); + + return; + } + + // Set reporting mode to one that supports every component. + static constexpr auto desired_reporting_mode = InputReportID::ReportCoreAccelIR10Ext6; + if (m_reporting_mode != desired_reporting_mode) + { + OutputReportMode mode = {}; + mode.ack = 1; + mode.mode = desired_reporting_mode; + QueueReport(mode, [this](ErrorCode error) { + if (error != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to set reporting mode."); + return; + } + + m_reporting_mode = desired_reporting_mode; + + DEBUG_LOG(WIIMOTE, "WiiRemote: Set reporting mode."); + }); + + return; + } + + // Read accelerometer calibration. + if (!m_accel_calibration.has_value()) + { + static constexpr u16 ACCEL_CALIBRATION_ADDR = 0x16; + + ReadData(AddressSpace::EEPROM, 0, ACCEL_CALIBRATION_ADDR, sizeof(AccelCalibrationData), + [this](ReadResponse response) { + if (!response) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to read accelerometer calibration."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: Read accelerometer calibration."); + + auto& calibration_data = *response; + + const AccelCalibrationData accel_calibration = + Common::BitCastPtr(calibration_data.data()); + m_accel_calibration = accel_calibration.GetCalibration(); + + WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 1); + + // We could potentially try the second block at 0x26 if the checksum is bad. + if (accel_calibration.checksum != calibration_data.back()) + WARN_LOG(WIIMOTE, "WiiRemote: Bad accelerometer calibration checksum."); + }); + + return; + } + + if (!m_ir_state.IsFullyConfigured()) + { + ConfigureIRCamera(); + + return; + } + + if (!m_speaker_configured) + { + ConfigureSpeaker(); + + return; + } + + // Perform the following tasks only after M+ is settled. + if (IsWaitingForMotionPlus()) + return; + + // Read the "active" extension ID. (This also gives us the current M+ mode) + // This will fail on an un-intialized other extension. + // But extension initialization is the same as M+ de-activation so we must try this first. + if (m_extension_port == true && + (!IsMotionPlusStateKnown() || (!IsMotionPlusActive() && !m_extension_id.has_value()))) + { + static constexpr u16 ENCRYPTION_ADDR = 0xfb; + static constexpr u8 ENCRYPTION_VALUE = 0x00; + + // First disable encryption. Note this is a no-op when performed on the M+. + WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, ENCRYPTION_ADDR, + {ENCRYPTION_VALUE}, [this](ErrorCode error) { + if (error != ErrorCode::Success) + return; + + ReadActiveExtensionID(); + }); + + return; + } + + static constexpr u16 INIT_ADDR = 0xf0; + static constexpr u8 INIT_VALUE = 0x55; + + // Initialize "active" extension if ID was not recognized. + // Note this is done before M+ setup to determine the required passthrough mode. + if (m_extension_id == ExtensionID::Unsupported) + { + // Note that this signal also DE-activates a M+. + WriteData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, INIT_ADDR, + {INIT_VALUE}, [this](ErrorCode result) { + DEBUG_LOG(WIIMOTE, "WiiRemote: Initialized extension: %d.", int(result)); + + m_extension_id = std::nullopt; + }); + + return; + } + + // The following tasks require a known M+ state. + if (!IsMotionPlusStateKnown()) + return; + + // We now know the status of the M+. + // Updating it too frequently results off/on flashes on mode change. + m_mplus_attached_input = IsMotionPlusActive(); + + // Extension removal status is known here. Attachment status is updated after the ID is read. + if (m_extension_port != true) + m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + + // Periodically try to initialize and activate an inactive M+. + if (!IsMotionPlusActive() && m_mplus_desired_mode.has_value() && + m_mplus_state.current_mode != m_mplus_desired_mode) + { + static constexpr u16 MPLUS_POLL_ADDR = WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET; + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, MPLUS_POLL_ADDR, 1, + [this](ReadResponse response) { + if (!response) + { + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ poll failed."); + HandleMotionPlusNonResponse(); + return; + } + + WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR, + INIT_ADDR, {INIT_VALUE}, [this](ErrorCode result) { + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ initialization: %d.", int(result)); + if (result != ErrorCode::Success) + { + HandleMotionPlusNonResponse(); + return; + } + + TriggerMotionPlusModeChange(); + }); + }); + + return; + } + + // Change active M+ passthrough mode. + if (IsMotionPlusActive() && m_mplus_desired_mode.has_value() && + m_mplus_state.current_mode != m_mplus_desired_mode) + { + TriggerMotionPlusModeChange(); + + return; + } + + // Read passthrough extension ID. + // This will also give us a desired M+ passthrough mode. + if (IsMotionPlusActive() && m_mplus_state.passthrough_port == true && !m_extension_id.has_value()) + { + // The M+ reads the passthrough ext ID and stores it at 0xf6,f8,f9. + static constexpr u16 PASSTHROUGH_EXT_ID_ADDR = 0xf6; + + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + PASSTHROUGH_EXT_ID_ADDR, 4, [this](ReadResponse response) { + if (!response) + return; + + // Port status may have changed since the read was sent. + // In which case this data read would succeed but be useless. + if (m_mplus_state.passthrough_port != true) + return; + + auto& identifier = *response; + + ProcessExtensionID(identifier[2], identifier[0], identifier[3]); + }); + + return; + } + + // The following tasks require M+ configuration to be done. + if (!IsMotionPlusInDesiredMode()) + return; + + // Now that M+ config has settled we can update the extension number. + // Updating it too frequently results off/on flashes on M+ mode change. + UpdateExtensionNumberInput(); + + static constexpr u16 NORMAL_CALIBRATION_ADDR = 0x20; + + // Read M+ calibration. + if (IsMotionPlusActive() && !m_mplus_state.calibration.has_value()) + { + ReadData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + NORMAL_CALIBRATION_ADDR, sizeof(WiimoteEmu::MotionPlus::CalibrationData), + [this](ReadResponse response) { + if (!response) + return; + + DEBUG_LOG(WIIMOTE, "WiiRemote: Read M+ calibration."); + + WiimoteEmu::MotionPlus::CalibrationData calibration = + Common::BitCastPtr(response->data()); + + const auto read_checksum = std::pair(calibration.crc32_lsb, calibration.crc32_msb); + + calibration.UpdateChecksum(); + + m_mplus_state.SetCalibrationData(calibration); + + if (read_checksum != std::pair(calibration.crc32_lsb, calibration.crc32_msb)) + { + // We could potentially try another read or call the M+ unusable. + WARN_LOG(WIIMOTE, "WiiRemote: Bad M+ calibration checksum."); + } + }); + + return; + } + + // Read normal extension calibration. + if ((m_extension_id == ExtensionID::Nunchuk && !m_nunchuk_state.calibration) || + (m_extension_id == ExtensionID::Classic && !m_classic_state.calibration)) + { + // Extension calibration is normally at 0x20 but M+ reads and stores it at 0x40. + static constexpr u16 PASSTHROUGH_CALIBRATION_ADDR = 0x40; + + const u16 calibration_addr = + IsMotionPlusActive() ? PASSTHROUGH_CALIBRATION_ADDR : NORMAL_CALIBRATION_ADDR; + static constexpr u16 CALIBRATION_SIZE = 0x10; + + ReadData( + AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, calibration_addr, + CALIBRATION_SIZE, [this](ReadResponse response) { + if (!response) + return; + + DEBUG_LOG(WIIMOTE, "WiiRemote: Read extension calibration."); + + auto& calibration_data = *response; + + const auto read_checksum = std::pair(calibration_data[CALIBRATION_SIZE - 2], + calibration_data[CALIBRATION_SIZE - 1]); + + WiimoteEmu::UpdateCalibrationDataChecksum(calibration_data, 2); + + if (read_checksum != std::pair(calibration_data[CALIBRATION_SIZE - 2], + calibration_data[CALIBRATION_SIZE - 1])) + { + // We could potentially try another block or call the extension unusable. + WARN_LOG(WIIMOTE, "WiiRemote: Bad extension calibration checksum."); + } + + if (m_extension_id == ExtensionID::Nunchuk) + { + m_nunchuk_state.SetCalibrationData( + Common::BitCastPtr(calibration_data.data())); + } + else if (m_extension_id == ExtensionID::Classic) + { + m_classic_state.SetCalibrationData( + Common::BitCastPtr(calibration_data.data())); + } + }); + + return; + } +} + +void Device::HandleMotionPlusNonResponse() +{ + // No need for additional checks if an extension is attached. + // (not possible for M+ to become attached) + if (m_extension_port == true) + m_mplus_desired_mode = MotionPlusState::PassthroughMode{}; + else + WaitForMotionPlus(); +} + +// Produce LED bitmask for remotes. +// Remotes 1-4 are normal. Additional remotes LED labels will add up to their assigned ID. +u8 Device::GetDesiredLEDValue() const +{ + const auto index = GetId(); + + // Normal LED behavior for remotes 1-4. + if (index < 4) + return 1 << index; + + // Light LED 4 and LEDs 1 through 3 for remotes 5-7. (Add up the numbers on the remote) + if (index < 7) + return 1 << (index - 4) | 8; + + // Light LED 4+3 and LEDs 1 or 2 for remotes 8 or 9. (Add up the numbers on the remote) + if (index < 9) + return 1 << (index - 7) | 8 | 4; + + // For remotes 10 and up just light all LEDs. + return 0xf; +} + +void Device::UpdateExtensionNumberInput() +{ + switch (m_extension_id.value_or(ExtensionID::Unsupported)) + { + case ExtensionID::Nunchuk: + m_extension_number_input = WiimoteEmu::ExtensionNumber::NUNCHUK; + break; + case ExtensionID::Classic: + m_extension_number_input = WiimoteEmu::ExtensionNumber::CLASSIC; + break; + case ExtensionID::Unsupported: + default: + m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + break; + } +} + +void Device::ProcessExtensionEvent(bool connected) +{ + // Reset extension state. + m_nunchuk_state = {}; + m_classic_state = {}; + + m_extension_id = std::nullopt; + + // We won't know the desired mode until we get the extension ID. + if (connected) + m_mplus_desired_mode = std::nullopt; +} + +void Device::ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5) +{ + if (id_4 == 0x00 && id_5 == 0x00) + { + INFO_LOG(WIIMOTE, "WiiRemote: Nunchuk is attached."); + m_extension_id = ExtensionID::Nunchuk; + + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; + } + else if (id_4 == 0x01 && id_5 == 0x01) + { + INFO_LOG(WIIMOTE, "WiiRemote: Classic Controller is attached."); + m_extension_id = ExtensionID::Classic; + + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Classic; + } + else + { + // This is a normal occurance before extension initialization. + DEBUG_LOG(WIIMOTE, "WiiRemote: Unknown extension: %d %d %d.", id_0, id_4, id_5); + m_extension_id = ExtensionID::Unsupported; + } +} + +void Device::MotionPlusState::SetCalibrationData( + const WiimoteEmu::MotionPlus::CalibrationData& data) +{ + DEBUG_LOG(WIIMOTE, "WiiRemote: Set M+ calibration."); + + calibration.emplace(); + + calibration->fast = data.fast; + calibration->slow = data.slow; +} + +void Device::NunchukState::SetCalibrationData(const WiimoteEmu::Nunchuk::CalibrationData& data) +{ + DEBUG_LOG(WIIMOTE, "WiiRemote: Set Nunchuk calibration."); + + calibration.emplace(); + + calibration->stick = data.GetStick(); + calibration->accel = data.GetAccel(); +} + +void Device::ClassicState::SetCalibrationData(const WiimoteEmu::Classic::CalibrationData& data) +{ + DEBUG_LOG(WIIMOTE, "WiiRemote: Set Classic Controller calibration."); + + calibration.emplace(); + + calibration->left_stick = data.GetLeftStick(); + calibration->right_stick = data.GetRightStick(); + + calibration->left_trigger = data.GetLeftTrigger(); + calibration->right_trigger = data.GetRightTrigger(); +} + +void Device::ReadActiveExtensionID() +{ + static constexpr u16 EXT_ID_ADDR = 0xfa; + static constexpr u16 EXT_ID_SIZE = 6; + + ReadData(AddressSpace::I2CBus, WiimoteEmu::ExtensionPort::REPORT_I2C_SLAVE, EXT_ID_ADDR, + EXT_ID_SIZE, [this](ReadResponse response) { + if (!response) + return; + + auto& identifier = *response; + + // Check for M+ ID. + if (identifier[5] == 0x05) + { + const auto passthrough_mode = MotionPlusState::PassthroughMode(identifier[4]); + + m_mplus_state.current_mode = passthrough_mode; + + INFO_LOG(WIIMOTE, "WiiRemote: M+ is active in mode: %d.", int(passthrough_mode)); + } + else + { + m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; + + ProcessExtensionID(identifier[0], identifier[4], identifier[5]); + } + }); +} + +bool Device::IRState::IsFullyConfigured() const +{ + return enabled && mode_set && current_sensitivity == GetDesiredIRSensitivity(); +} + +u32 Device::IRState::GetDesiredIRSensitivity() +{ + // Wii stores values from 1 to 5. (subtract 1) + const u32 configured_level = Config::Get(Config::SYSCONF_SENSOR_BAR_SENSITIVITY) - 1; + + if (configured_level < IR_SENSITIVITY_LEVEL_COUNT) + return configured_level; + + // Default to middle level on bad value. + return 2; +} + +void Device::SetIRSensitivity(u32 level) +{ + struct IRSensitivityConfig + { + std::array block1; + std::array block2; + }; + + // Data for Wii levels 1 to 5. + static constexpr std::array sensitivity_configs = + {{ + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x64, 0x00, 0xfe}, {0xfd, 0x05}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0x96, 0x00, 0xb4}, {0xb3, 0x04}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xaa, 0x00, 0x64}, {0x63, 0x03}}, + {{0x02, 0x00, 0x00, 0x71, 0x01, 0x00, 0xc8, 0x00, 0x36}, {0x35, 0x03}}, + {{0x07, 0x00, 0x00, 0x71, 0x01, 0x00, 0x72, 0x00, 0x20}, {0x1f, 0x03}}, + }}; + + static constexpr u16 BLOCK1_ADDR = 0x00; + static constexpr u16 BLOCK2_ADDR = 0x1a; + + DEBUG_LOG(WIIMOTE, "WiiRemote: Setting IR sensitivity: %d.", level + 1); + + const auto& sensitivity_config = sensitivity_configs[level]; + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK1_ADDR, + sensitivity_config.block1, [&sensitivity_config, level, this](ErrorCode block_result) { + if (block_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to write IR block 1."); + return; + } + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, BLOCK2_ADDR, + sensitivity_config.block2, [&, level, this](ErrorCode block2_result) { + if (block2_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to write IR block 2."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: IR sensitivity set."); + + m_ir_state.current_sensitivity = level; + }); + }); +} + +void Device::ConfigureIRCamera() +{ + if (!m_ir_state.enabled) + { + OutputReportIRLogicEnable2 ir_logic2 = {}; + ir_logic2.ack = 1; + ir_logic2.enable = 1; + QueueReport(ir_logic2, [this](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to enable IR."); + return; + } + + OutputReportIRLogicEnable ir_logic = {}; + ir_logic.ack = 1; + ir_logic.enable = 1; + QueueReport(ir_logic, [this](ErrorCode ir_result) { + if (ir_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to enable IR."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: IR enabled."); + + m_ir_state.enabled = true; + }); + }); + + return; + } + + if (const u32 desired_level = IRState::GetDesiredIRSensitivity(); + desired_level != m_ir_state.current_sensitivity) + { + SetIRSensitivity(desired_level); + + return; + } + + if (!m_ir_state.mode_set) + { + static constexpr u16 MODE_ADDR = 0x33; + + // We only support "Basic" mode (it's all that fits in ReportCoreAccelIR10Ext6). + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, MODE_ADDR, + {WiimoteEmu::CameraLogic::IR_MODE_BASIC}, [this](ErrorCode mode_result) { + if (mode_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to set IR mode."); + return; + } + + // This seems to enable object tracking. + static constexpr u16 ENABLE_ADDR = 0x30; + static constexpr u8 ENABLE_VALUE = 0x08; + + WriteData(AddressSpace::I2CBus, WiimoteEmu::CameraLogic::I2C_ADDR, ENABLE_ADDR, + {ENABLE_VALUE}, [this](ErrorCode result) { + if (result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to enable object tracking."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: IR mode set."); + + m_ir_state.mode_set = true; + }); + }); + } +} + +void Device::ConfigureSpeaker() +{ + OutputReportSpeakerMute mute = {}; + mute.enable = 1; + mute.ack = 1; + QueueReport(mute, [this](ErrorCode mute_result) { + if (mute_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to mute speaker."); + return; + } + + OutputReportSpeakerEnable spkr = {}; + spkr.enable = 0; + spkr.ack = 1; + QueueReport(spkr, [this](ErrorCode enable_result) { + if (enable_result != ErrorCode::Success) + { + WARN_LOG(WIIMOTE, "WiiRemote: Failed to disable speaker."); + return; + } + + DEBUG_LOG(WIIMOTE, "WiiRemote: Speaker muted and disabled."); + + m_speaker_configured = true; + }); + }); +} + +void Device::TriggerMotionPlusModeChange() +{ + if (!m_mplus_desired_mode.has_value()) + return; + + const u8 passthrough_mode = u8(*m_mplus_desired_mode); + + const u8 device_addr = IsMotionPlusActive() ? WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR : + WiimoteEmu::MotionPlus::INACTIVE_DEVICE_ADDR; + + WriteData(AddressSpace::I2CBus, device_addr, WiimoteEmu::MotionPlus::PASSTHROUGH_MODE_OFFSET, + {passthrough_mode}, [this](ErrorCode activation_result) { + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ activation: %d.", int(activation_result)); + + WaitForMotionPlus(); + + // Normally M+ will be seen performing a reset here. (extension port events) + // But sometimes (rarely) M+ activation does not cause an extension port event. + // We'll consider the mode unknown. It will be read back after some time. + m_mplus_state.current_mode = std::nullopt; + }); +} + +void Device::TriggerMotionPlusCalibration() +{ + static constexpr u16 CALIBRATION_TRIGGER_ADDR = 0xf2; + static constexpr u8 CALIBRATION_TRIGGER_VALUE = 0x00; + + // This triggers a hardware "zero" calibration. + // The effect is notiecable but output still strays from calibration data. + // It seems we're better off just manually determining "zero". + WriteData(AddressSpace::I2CBus, WiimoteEmu::MotionPlus::ACTIVE_DEVICE_ADDR, + CALIBRATION_TRIGGER_ADDR, {CALIBRATION_TRIGGER_VALUE}, [](ErrorCode result) { + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ calibration trigger done: %d.", int(result)); + }); +} + +bool Device::IsMotionPlusStateKnown() const +{ + return m_mplus_state.current_mode.has_value(); +} + +bool Device::IsMotionPlusActive() const +{ + return m_mplus_state.current_mode != MotionPlusState::PassthroughMode{}; +} + +bool Device::IsMotionPlusInDesiredMode() const +{ + return m_mplus_state.current_mode.has_value() && + (m_mplus_state.current_mode == m_mplus_desired_mode); +} + +void Device::ProcessInputReport(WiimoteReal::Report& report) +{ + if (report.size() < WiimoteCommon::DataReportBuilder::HEADER_SIZE) + { + WARN_LOG(WIIMOTE, "WiiRemote: Bad report size."); + return; + } + + auto report_id = InputReportID(report[1]); + + for (auto it = m_report_handlers.begin(); true;) + { + if (it == m_report_handlers.end()) + { + if (report_id == InputReportID::Status) + { + if (report.size() < + sizeof(InputReportStatus) + WiimoteCommon::DataReportBuilder::HEADER_SIZE) + { + WARN_LOG(WIIMOTE, "WiiRemote: Bad report size."); + } + else + { + ProcessStatusReport(Common::BitCastPtr(report.data() + 2)); + } + } + else if (report_id < InputReportID::ReportCore) + { + WARN_LOG(WIIMOTE, "WiiRemote: Unhandled input report: %s.", + ArrayToString(report.data(), u32(report.size())).c_str()); + } + + break; + } + + if (it->IsExpired()) + { + WARN_LOG(WIIMOTE, "WiiRemote: Removing expired handler."); + it = m_report_handlers.erase(it); + continue; + } + + if (const auto result = it->TryToHandleReport(report); + result == ReportHandler::HandlerResult::Handled) + { + it = m_report_handlers.erase(it); + break; + } + + ++it; + } + + if (report_id < InputReportID::ReportCore) + { + // Normal input reports can be processed as "ReportCore". + report_id = InputReportID::ReportCore; + } + else + { + // We can assume the last received input report is the current reporting mode. + // FYI: This logic fails to properly handle the (never used) "interleaved" reports. + m_reporting_mode = InputReportID(report_id); + } + + auto manipulator = MakeDataReportManipulator( + report_id, report.data() + WiimoteCommon::DataReportBuilder::HEADER_SIZE); + + if (manipulator->GetDataSize() + WiimoteCommon::DataReportBuilder::HEADER_SIZE > report.size()) + { + WARN_LOG(WIIMOTE, "WiiRemote: Bad report size."); + return; + } + + // Read buttons. + manipulator->GetCoreData(&m_core_data); + + // Process accel data. + if (manipulator->HasAccel() && m_accel_calibration.has_value()) + { + // FYI: This logic fails to properly handle the (never used) "interleaved" reports. + AccelData accel_data = {}; + manipulator->GetAccelData(&accel_data); + + m_accel_data = + accel_data.GetNormalizedValue(*m_accel_calibration) * float(MathUtil::GRAVITY_ACCELERATION); + } + + // Process IR data. + if (manipulator->HasIR() && m_ir_state.IsFullyConfigured()) + { + m_ir_state.ProcessData( + Common::BitCastPtr>(manipulator->GetIRDataPtr())); + } + + // Process extension data. + if (IsMotionPlusStateKnown()) + { + const auto ext_data = manipulator->GetExtDataPtr(); + const auto ext_size = manipulator->GetExtDataSize(); + + if (IsMotionPlusActive()) + ProcessMotionPlusExtensionData(ext_data, ext_size); + else + ProcessNormalExtensionData(ext_data, ext_size); + } + + UpdateOrientation(); +} + +void Device::UpdateOrientation() +{ + const auto current_report_time = Clock::now(); + const auto elapsed_time = std::chrono::duration_cast>( + current_report_time - m_last_report_time); + m_last_report_time = current_report_time; + + // Apply M+ gyro data to our orientation. + m_orientation = + WiimoteEmu::GetMatrixFromGyroscope(m_mplus_state.gyro_data * -1 * elapsed_time.count()) * + m_orientation; + + // When M+ data is not available give accel/ir data more weight. + // ComplementaryFilter will then just smooth out our data a bit. + const bool is_mplus_active = IsMotionPlusStateKnown() && IsMotionPlusActive(); + + // With non-zero acceleration data we can perform pitch and roll correction. + if (m_accel_data.LengthSquared()) + { + const auto accel_weight = is_mplus_active ? 0.04 : 0.5f; + + m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, m_accel_data, accel_weight); + } + + // If IR objects are visible we can perform yaw and pitch correction. + if (!m_ir_state.is_hidden) + { + // FYI: We could do some roll correction from multiple IR objects. + + const auto ir_rotation = + Common::Vec3(m_ir_state.center_position.y * WiimoteEmu::CameraLogic::CAMERA_FOV_Y_DEG, 0, + m_ir_state.center_position.x * WiimoteEmu::CameraLogic::CAMERA_FOV_X_DEG) / + 2 * float(MathUtil::TAU) / 360; + const auto ir_normal = Common::Vec3(0, 1, 0); + const auto ir_vector = WiimoteEmu::GetMatrixFromGyroscope(-ir_rotation) * ir_normal; + + // Pitch correction will be slightly wrong based on sensorbar height. + // Keep weight below accelerometer weight for that reason. + // Correction will only happen near pitch zero when the sensorbar is actually in view. + const auto ir_weight = is_mplus_active ? 0.035 : 0.45f; + + m_orientation = WiimoteEmu::ComplementaryFilter(m_orientation, ir_vector, ir_weight, ir_normal); + } + + // Update our (pitch, roll, yaw) inputs now that orientation has been adjusted. + m_rotation_inputs = + Common::Vec3{WiimoteEmu::GetPitch(m_orientation), WiimoteEmu::GetRoll(m_orientation), + WiimoteEmu::GetYaw(m_orientation)} / + float(MathUtil::PI); +} + +void Device::IRState::ProcessData(const std::array& data) +{ + // A better implementation might extrapolate points when they fall out of camera view. + // But just averaging visible points actually seems to work very well. + + using IRObject = WiimoteEmu::IRBasic::IRObject; + + Common::Vec2 point_total; + int point_count = 0; + + const auto camera_max = IRObject(WiimoteEmu::CameraLogic::CAMERA_RES_X - 1, + WiimoteEmu::CameraLogic::CAMERA_RES_Y - 1); + + const auto add_point = [&](IRObject point) { + // Non-visible points are 0xFF-filled. + if (point.y > camera_max.y) + return; + + point_total += Common::Vec2(point); + ++point_count; + }; + + for (auto& block : data) + { + add_point(block.GetObject1()); + add_point(block.GetObject2()); + } + + is_hidden = !point_count; + + if (point_count) + { + center_position = + point_total / float(point_count) / Common::Vec2(camera_max) * 2.f - Common::Vec2(1, 1); + } + else + { + center_position = {}; + } +} + +void Device::ProcessMotionPlusExtensionData(const u8* ext_data, u32 ext_size) +{ + if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) + return; + + const WiimoteEmu::MotionPlus::DataFormat mplus_data = + Common::BitCastPtr(ext_data); + + const bool is_ext_connected = mplus_data.extension_connected; + + // Handle passthrough extension change. + if (is_ext_connected != m_mplus_state.passthrough_port) + { + m_mplus_state.passthrough_port = is_ext_connected; + + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ passthrough port event: %d.", is_ext_connected); + + // With no passthrough extension we'll be happy with the current mode. + if (!is_ext_connected) + m_mplus_desired_mode = m_mplus_state.current_mode; + + ProcessExtensionEvent(is_ext_connected); + } + + if (mplus_data.is_mp_data) + { + m_mplus_state.ProcessData(mplus_data); + return; + } + + if (!IsMotionPlusInDesiredMode()) + { + DEBUG_LOG(WIIMOTE, "WiiRemote: Ignoring unwanted passthrough data."); + return; + } + + std::array data; + std::copy_n(ext_data, ext_size, data.begin()); + + // Undo bit-hacks of M+ passthrough. + WiimoteEmu::MotionPlus::ReversePassthroughModifications(*m_mplus_state.current_mode, data.data()); + + ProcessNormalExtensionData(data.data(), u32(data.size())); +} + +void Device::ProcessNormalExtensionData(const u8* ext_data, u32 ext_size) +{ + if (m_extension_id == ExtensionID::Nunchuk) + { + if (ext_size < sizeof(WiimoteEmu::MotionPlus::DataFormat)) + return; + + const WiimoteEmu::Nunchuk::DataFormat nunchuk_data = + Common::BitCastPtr(ext_data); + + m_nunchuk_state.ProcessData(nunchuk_data); + } + else if (m_extension_id == ExtensionID::Classic) + { + if (ext_size < sizeof(WiimoteEmu::Classic::DataFormat)) + return; + + const WiimoteEmu::Classic::DataFormat cc_data = + Common::BitCastPtr(ext_data); + + m_classic_state.ProcessData(cc_data); + } +} + +void Device::UpdateRumble() +{ + static constexpr auto rumble_period = std::chrono::milliseconds(100); + + const auto on_time = std::chrono::duration_cast(rumble_period * m_rumble_level); + const auto off_time = rumble_period - on_time; + + const auto now = Clock::now(); + + if (m_rumble && (now < m_last_rumble_change + on_time || !off_time.count())) + return; + + if (!m_rumble && (now < m_last_rumble_change + off_time || !on_time.count())) + return; + + m_last_rumble_change = now; + m_rumble ^= true; + + // Rumble flag will be set within QueueReport. + QueueReport(OutputReportRumble{}); +} + +void Device::UpdateInput() +{ + if (!m_wiimote->IsConnected()) + { + g_controller_interface.RemoveDevice( + [this](const Core::Device* device) { return device == this; }); + return; + } + + UpdateRumble(); + RunTasks(); + + WiimoteReal::Report report; + while (m_wiimote->GetNextReport(&report)) + { + ProcessInputReport(report); + RunTasks(); + } +} + +void Device::MotionPlusState::ProcessData(const WiimoteEmu::MotionPlus::DataFormat& data) +{ + // We need the calibration block read to know the sensor orientations. + if (!calibration.has_value()) + return; + + // Unfortunately M+ calibration zero values are very poor. + // We calibrate when we receive a few seconds of stable data. + const auto unadjusted_gyro_data = data.GetData().GetAngularVelocity(*calibration); + + // Use zero-data calibration until acquired. + const auto adjusted_gyro_data = + unadjusted_gyro_data - m_dynamic_calibration.value_or(Common::Vec3{}); + + // We want quick calibration when remote is set down but not when held in the hand. + // This magic value seems to work well enough. + static constexpr auto UNSTABLE_ROTATION = float(MathUtil::TAU / 120); + + const bool is_stable = (adjusted_gyro_data - gyro_data).Length() < UNSTABLE_ROTATION; + + gyro_data = adjusted_gyro_data; + + // If we've yet to achieve calibration acquire one more quickly. + // This lessens the extreme drift on initial M+ activation. + const auto required_stable_frames = m_dynamic_calibration.has_value() ? 100u : 5u; + + if (is_stable) + { + if (++m_new_calibration_frames < required_stable_frames) + { + m_new_dynamic_calibration += unadjusted_gyro_data; + } + else + { + m_dynamic_calibration = m_new_dynamic_calibration / m_new_calibration_frames; + m_new_dynamic_calibration = {}; + m_new_calibration_frames = 0; + + DEBUG_LOG(WIIMOTE, "WiiRemote: M+ applied dynamic calibration."); + } + } + else + { + m_new_dynamic_calibration = {}; + m_new_calibration_frames = 0; + } +} + +bool Device::IsWaitingForMotionPlus() const +{ + return Clock::now() < m_mplus_wait_time; +} + +void Device::WaitForMotionPlus() +{ + DEBUG_LOG(WIIMOTE, "WiiRemote: Wait for M+."); + m_mplus_wait_time = Clock::now() + std::chrono::seconds{2}; +} + +void Device::NunchukState::ProcessData(const WiimoteEmu::Nunchuk::DataFormat& data) +{ + buttons = data.GetButtons(); + + // Stick/accel require calibration data. + if (!calibration.has_value()) + return; + + stick = data.GetStick().GetNormalizedValue(calibration->stick); + accel = data.GetAccel().GetNormalizedValue(calibration->accel) * + float(MathUtil::GRAVITY_ACCELERATION); +} + +void Device::ClassicState::ProcessData(const WiimoteEmu::Classic::DataFormat& data) +{ + buttons = data.GetButtons(); + + // Sticks/triggers require calibration data. + if (!calibration.has_value()) + return; + + sticks[0] = data.GetLeftStick().GetNormalizedValue(calibration->left_stick); + sticks[1] = data.GetRightStick().GetNormalizedValue(calibration->right_stick); + triggers[0] = data.GetLeftTrigger().GetNormalizedValue(calibration->left_trigger); + triggers[1] = data.GetRightTrigger().GetNormalizedValue(calibration->right_trigger); +} + +void Device::ReadData(AddressSpace space, u8 slave, u16 address, u16 size, + std::function callback) +{ + OutputReportReadData read_data{}; + read_data.space = u8(space); + read_data.slave_address = slave; + read_data.address[0] = u8(address >> 8); + read_data.address[1] = u8(address); + read_data.size[0] = u8(size >> 8); + read_data.size[1] = u8(size); + QueueReport(read_data); + + AddReadDataReplyHandler(space, slave, address, size, {}, std::move(callback)); +} + +void Device::AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, + std::vector starting_data, + std::function callback) +{ + // Data read may return a busy ack. + auto ack_handler = MakeAckHandler(OutputReportID::ReadData, [callback](ErrorCode result) { + DEBUG_LOG(WIIMOTE, "WiiRemote: Read ack error: %d.", int(result)); + callback(ReadResponse{}); + }); + + // Or more normally a "ReadDataReply". + auto read_handler = [this, space, slave, address, size, data = std::move(starting_data), + callback = + std::move(callback)](const InputReportReadDataReply& reply) mutable { + if (Common::swap16(reply.address) != address) + return ReportHandler::HandlerResult::NotHandled; + + if (reply.error != u8(ErrorCode::Success)) + { + DEBUG_LOG(WIIMOTE, "WiiRemote: Read reply error: %d.", int(reply.error)); + callback(ReadResponse{}); + + return ReportHandler::HandlerResult::Handled; + } + + const auto read_count = reply.size_minus_one + 1; + + data.insert(data.end(), reply.data, reply.data + read_count); + + if (read_count < size) + { + // We have more data to acquire. + AddReadDataReplyHandler(space, slave, address + read_count, size - read_count, + std::move(data), std::move(callback)); + } + else + { + // We have all the data. + callback(std::move(data)); + } + + return ReportHandler::HandlerResult::Handled; + }; + + AddReportHandler( + std::function( + std::move(read_handler)), + std::move(ack_handler)); +} + +template +void Device::WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback) +{ + OutputReportWriteData write_data = {}; + write_data.space = u8(space); + write_data.slave_address = slave; + write_data.address[0] = u8(address >> 8); + write_data.address[1] = u8(address); + + static constexpr auto MAX_DATA_SIZE = std::size(write_data.data); + write_data.size = u8(std::min(std::size(data), MAX_DATA_SIZE)); + + std::copy_n(std::begin(data), write_data.size, write_data.data); + + // Writes of more than 16 bytes must be split into multiple reports. + if (std::size(data) > MAX_DATA_SIZE) + { + auto next_write = [this, space, slave, address, + additional_data = + std::vector(std::begin(data) + MAX_DATA_SIZE, std::end(data)), + callback = std::forward(callback)](ErrorCode result) mutable { + if (result != ErrorCode::Success) + callback(result); + else + WriteData(space, slave, address + MAX_DATA_SIZE, additional_data, std::move(callback)); + }; + + QueueReport(write_data, std::move(next_write)); + } + else + { + QueueReport(write_data, std::forward(callback)); + } +} + +Device::ReportHandler::ReportHandler(Clock::time_point expired_time) : m_expired_time(expired_time) +{ +} + +template +void Device::AddReportHandler(T&&... callbacks) +{ + auto& handler = m_report_handlers.emplace_back(Clock::now() + std::chrono::seconds{5}); + (handler.AddHandler(std::forward(callbacks)), ...); +} + +template +void Device::ReportHandler::AddHandler(std::function handler) +{ + m_callbacks.emplace_back([handler = std::move(handler)](const WiimoteReal::Report& report) { + if (report[1] != u8(T::REPORT_ID)) + return ReportHandler::HandlerResult::NotHandled; + + T data; + + if (report.size() < sizeof(T) + WiimoteCommon::DataReportBuilder::HEADER_SIZE) + { + // Off-brand "NEW 2in1" Wii Remote likes to shorten read data replies. + WARN_LOG(WIIMOTE, "WiiRemote: Bad report size (%d) for report 0x%x. Zero-filling.", + int(report.size()), int(T::REPORT_ID)); + + data = {}; + std::memcpy(&data, report.data() + WiimoteCommon::DataReportBuilder::HEADER_SIZE, + report.size() - WiimoteCommon::DataReportBuilder::HEADER_SIZE); + } + else + { + data = Common::BitCastPtr(report.data() + WiimoteCommon::DataReportBuilder::HEADER_SIZE); + } + + if constexpr (std::is_same_v) + { + handler(data); + return ReportHandler::HandlerResult::Handled; + } + else + { + return handler(data); + } + }); +} + +auto Device::ReportHandler::TryToHandleReport(const WiimoteReal::Report& report) -> HandlerResult +{ + for (auto& callback : m_callbacks) + { + if (const auto result = callback(report); result != HandlerResult::NotHandled) + return result; + } + + return HandlerResult::NotHandled; +} + +bool Device::ReportHandler::IsExpired() const +{ + return Clock::now() >= m_expired_time; +} + +auto Device::MakeAckHandler(OutputReportID report_id, + std::function callback) + -> AckReportHandler +{ + return [report_id, callback = std::move(callback)](const InputReportAck& reply) { + if (reply.rpt_id != report_id) + return ReportHandler::HandlerResult::NotHandled; + + callback(reply.error_code); + return ReportHandler::HandlerResult::Handled; + }; +} + +bool Device::IsPerformingTask() const +{ + return !m_report_handlers.empty(); +} + +void Device::ProcessStatusReport(const InputReportStatus& status) +{ + // Update status periodically to keep battery level value up to date. + m_status_outdated_time = Clock::now() + std::chrono::seconds(10); + + m_battery = status.battery; + m_leds = status.leds; + + if (!status.ir) + m_ir_state = {}; + + const bool is_ext_connected = status.extension; + + // Handle extension port state change. + if (is_ext_connected != m_extension_port) + { + DEBUG_LOG(WIIMOTE, "WiiRemote: Extension port event: %d.", is_ext_connected); + + m_extension_port = is_ext_connected; + + // Data reporting stops on an extension port event. + m_reporting_mode = InputReportID::ReportDisabled; + + ProcessExtensionEvent(is_ext_connected); + + // The M+ is now in an unknown state. + m_mplus_state = {}; + + if (is_ext_connected) + { + // We can assume the M+ is settled on an attachment event. + m_mplus_wait_time = Clock::now(); + } + else + { + // "Nunchuk" will be the most used mode and also works with no passthrough extension. + m_mplus_desired_mode = MotionPlusState::PassthroughMode::Nunchuk; + + // If an extension is not connected the M+ is either disabled or resetting. + m_mplus_state.current_mode = MotionPlusState::PassthroughMode{}; + } + } +} + +} // namespace ciface::Wiimote diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h new file mode 100644 index 000000000000..e70ad0f2939e --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h @@ -0,0 +1,276 @@ +// Copyright 2020 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include "Core/HW/WiimoteCommon/DataReport.h" +#include "Core/HW/WiimoteCommon/WiimoteReport.h" +#include "Core/HW/WiimoteEmu/Camera.h" +#include "Core/HW/WiimoteEmu/Extension/Classic.h" +#include "Core/HW/WiimoteEmu/Extension/Nunchuk.h" +#include "Core/HW/WiimoteEmu/MotionPlus.h" +#include "Core/HW/WiimoteReal/WiimoteReal.h" +#include "InputCommon/ControllerInterface/Device.h" + +namespace ciface::Wiimote +{ +using namespace WiimoteCommon; + +void AddDevice(std::unique_ptr); +void ReleaseDevices(std::optional count = std::nullopt); + +class Device final : public Core::Device +{ +public: + Device(std::unique_ptr wiimote); + ~Device(); + + std::string GetName() const override; + std::string GetSource() const override; + + void UpdateInput() override; + +private: + using Clock = std::chrono::steady_clock; + + enum class ExtensionID + { + Nunchuk, + Classic, + Unsupported, + }; + + class MotionPlusState + { + public: + void SetCalibrationData(const WiimoteEmu::MotionPlus::CalibrationData&); + void ProcessData(const WiimoteEmu::MotionPlus::DataFormat&); + + using PassthroughMode = WiimoteEmu::MotionPlus::PassthroughMode; + + // State is unknown by default. + std::optional current_mode; + + // The last known state of the passthrough port flag. + // Used to detect passthrough extension port events. + std::optional passthrough_port; + + Common::Vec3 gyro_data = {}; + + std::optional calibration; + + private: + // Used to perform realtime calibration. + std::optional m_dynamic_calibration = {}; + Common::Vec3 m_new_dynamic_calibration = {}; + u32 m_new_calibration_frames = 0; + }; + + struct NunchukState + { + using CalibrationData = WiimoteEmu::Nunchuk::CalibrationData; + + void SetCalibrationData(const CalibrationData&); + void ProcessData(const WiimoteEmu::Nunchuk::DataFormat&); + + Common::Vec2 stick = {}; + Common::Vec3 accel = {}; + + u8 buttons = 0; + + struct Calibration + { + CalibrationData::AccelCalibration accel; + CalibrationData::StickCalibration stick; + }; + + std::optional calibration; + }; + + struct ClassicState + { + using CalibrationData = WiimoteEmu::Classic::CalibrationData; + + void SetCalibrationData(const CalibrationData&); + void ProcessData(const WiimoteEmu::Classic::DataFormat&); + + std::array sticks = {}; + std::array triggers = {}; + + u16 buttons = 0; + + struct Calibration + { + CalibrationData::StickCalibration left_stick; + CalibrationData::StickCalibration right_stick; + + CalibrationData::TriggerCalibration left_trigger; + CalibrationData::TriggerCalibration right_trigger; + }; + + std::optional calibration; + }; + + struct IRState + { + static u32 GetDesiredIRSensitivity(); + + void ProcessData(const std::array&); + bool IsFullyConfigured() const; + + u32 current_sensitivity = u32(-1); + bool enabled = false; + bool mode_set = false; + + // Average of visible IR "objects". + Common::Vec2 center_position = {}; + + bool is_hidden = true; + }; + + class ReportHandler + { + public: + enum class HandlerResult + { + Handled, + NotHandled, + }; + + ReportHandler(Clock::time_point expired_time); + + template + void AddHandler(std::function); + + HandlerResult TryToHandleReport(const WiimoteReal::Report& report); + + bool IsExpired() const; + + private: + const Clock::time_point m_expired_time; + std::vector> m_callbacks; + }; + + using AckReportHandler = std::function; + + static AckReportHandler MakeAckHandler(OutputReportID report_id, + std::function callback); + + // TODO: Make parameter const. (need to modify DataReportManipulator) + void ProcessInputReport(WiimoteReal::Report& report); + void ProcessMotionPlusExtensionData(const u8* data, u32 size); + void ProcessNormalExtensionData(const u8* data, u32 size); + void ProcessExtensionEvent(bool connected); + void ProcessExtensionID(u8 id_0, u8 id_4, u8 id_5); + void ProcessStatusReport(const InputReportStatus&); + + void RunTasks(); + + bool IsPerformingTask() const; + + template + void QueueReport(T&& report, std::function ack_callback = {}); + + template + void AddReportHandler(T&&... callbacks); + + using ReadResponse = std::optional>; + + void ReadData(AddressSpace space, u8 slave, u16 address, u16 size, + std::function callback); + + void AddReadDataReplyHandler(AddressSpace space, u8 slave, u16 address, u16 size, + std::vector starting_data, + std::function callback); + + template , typename C> + void WriteData(AddressSpace space, u8 slave, u16 address, T&& data, C&& callback); + + void ReadActiveExtensionID(); + void SetIRSensitivity(u32 level); + void ConfigureSpeaker(); + void ConfigureIRCamera(); + + u8 GetDesiredLEDValue() const; + + void TriggerMotionPlusModeChange(); + void TriggerMotionPlusCalibration(); + + bool IsMotionPlusStateKnown() const; + bool IsMotionPlusActive() const; + bool IsMotionPlusInDesiredMode() const; + + bool IsWaitingForMotionPlus() const; + void WaitForMotionPlus(); + void HandleMotionPlusNonResponse(); + + void UpdateRumble(); + void UpdateOrientation(); + void UpdateExtensionNumberInput(); + + std::unique_ptr m_wiimote; + + // Buttons. + DataReportManipulator::CoreData m_core_data = {}; + + // Accelerometer. + Common::Vec3 m_accel_data = {}; + std::optional m_accel_calibration; + + // Pitch, Roll, Yaw inputs. + Common::Vec3 m_rotation_inputs = {}; + + MotionPlusState m_mplus_state = {}; + NunchukState m_nunchuk_state = {}; + ClassicState m_classic_state = {}; + IRState m_ir_state = {}; + + // Used to poll for M+ periodically and wait for it to reset. + Clock::time_point m_mplus_wait_time = Clock::now(); + + // The desired mode is set based on the attached normal extension. + std::optional m_mplus_desired_mode; + + // Status report is requested every so often to update the battery level. + Clock::time_point m_status_outdated_time = Clock::now(); + u8 m_battery = 0; + u8 m_leds = 0; + + bool m_speaker_configured = false; + + // The last known state of the extension port status flag. + // Used to detect extension port events. + std::optional m_extension_port; + + // Note this refers to the passthrough extension when M+ is active. + std::optional m_extension_id; + + // Rumble state must be saved to set the proper flag in every output report. + bool m_rumble = false; + + // For pulse of rumble motor to simulate multiple levels. + ControlState m_rumble_level = 0; + Clock::time_point m_last_rumble_change = Clock::now(); + + // Assume mode is disabled so one gets set. + InputReportID m_reporting_mode = InputReportID::ReportDisabled; + + // Used only to provide a value for a specialty "input". (for attached extension passthrough) + WiimoteEmu::ExtensionNumber m_extension_number_input = WiimoteEmu::ExtensionNumber::NONE; + bool m_mplus_attached_input = false; + + // Holds callbacks for output report replies. + std::list m_report_handlers; + + // World rotation. (used to rotate IR data and provide pitch, roll, yaw inputs) + Common::Matrix33 m_orientation = Common::Matrix33::Identity(); + Clock::time_point m_last_report_time = Clock::now(); +}; + +} // namespace ciface::Wiimote diff --git a/Source/Core/InputCommon/InputCommon.vcxproj b/Source/Core/InputCommon/InputCommon.vcxproj index 1b0b42c7ab01..b5c9ef0bef5c 100644 --- a/Source/Core/InputCommon/InputCommon.vcxproj +++ b/Source/Core/InputCommon/InputCommon.vcxproj @@ -75,6 +75,7 @@ + @@ -122,6 +123,7 @@ + @@ -139,4 +141,4 @@ - \ No newline at end of file + diff --git a/Source/Core/InputCommon/InputCommon.vcxproj.filters b/Source/Core/InputCommon/InputCommon.vcxproj.filters index 0c8d79326fed..671c2b077daf 100644 --- a/Source/Core/InputCommon/InputCommon.vcxproj.filters +++ b/Source/Core/InputCommon/InputCommon.vcxproj.filters @@ -110,6 +110,9 @@ ControllerInterface\Win32 + + ControllerInterface\Wiimote + ControllerInterface @@ -218,6 +221,9 @@ ControllerInterface\Win32 + + ControllerInterface\Wiimote + ControllerInterface @@ -248,4 +254,4 @@ - \ No newline at end of file +