diff --git a/Source/Core/Common/BitUtils.h b/Source/Core/Common/BitUtils.h index 538718f3aa..7a1b65f587 100644 --- a/Source/Core/Common/BitUtils.h +++ b/Source/Core/Common/BitUtils.h @@ -300,6 +300,12 @@ void SetBit(T& value, size_t bit_number, bool bit_value) value &= ~(T{1} << bit_number); } +template +void SetBit(T& value, bool bit_value) +{ + SetBit(value, bit_number, bit_value); +} + template class FlagBit { @@ -340,4 +346,15 @@ public: std::underlying_type_t m_hex = 0; }; +// Left-shift a value and set new LSBs to that of the supplied LSB. +// Converts a value from a N-bit range to an (N+X)-bit range. e.g. 0x101 -> 0x10111 +template +T ExpandValue(T value, size_t left_shift_amount) +{ + static_assert(std::is_unsigned(), "ExpandValue is only sane on unsigned types."); + + return (value << left_shift_amount) | + (T(-ExtractBit<0>(value)) >> (BitSize() - left_shift_amount)); +} + } // namespace Common diff --git a/Source/Core/Common/MathUtil.h b/Source/Core/Common/MathUtil.h index 3ad01bc9fe..0e4b840524 100644 --- a/Source/Core/Common/MathUtil.h +++ b/Source/Core/Common/MathUtil.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include "Common/CommonTypes.h" @@ -93,6 +94,45 @@ struct Rectangle } }; +template +class RunningMean +{ +public: + constexpr void Clear() { *this = {}; } + + constexpr void Push(T x) { m_mean = m_mean + (x - m_mean) / ++m_count; } + + constexpr size_t Count() const { return m_count; } + constexpr T Mean() const { return m_mean; } + +private: + size_t m_count = 0; + T m_mean{}; +}; + +template +class RunningVariance +{ +public: + constexpr void Clear() { *this = {}; } + + constexpr void Push(T x) + { + const auto old_mean = m_running_mean.Mean(); + m_running_mean.Push(x); + m_variance += (x - old_mean) * (x - m_running_mean.Mean()); + } + + constexpr size_t Count() const { return m_running_mean.Count(); } + constexpr T Mean() const { return m_running_mean.Mean(); } + constexpr T Variance() const { return m_variance / (Count() - 1); } + constexpr T StandardDeviation() const { return std::sqrt(Variance()); } + +private: + RunningMean m_running_mean; + T m_variance{}; +}; + } // namespace MathUtil float MathFloatVectorSum(const std::vector&); diff --git a/Source/Core/Common/Matrix.h b/Source/Core/Common/Matrix.h index b47e24f03d..ff4ab71c73 100644 --- a/Source/Core/Common/Matrix.h +++ b/Source/Core/Common/Matrix.h @@ -20,6 +20,11 @@ union TVec3 TVec3() = default; TVec3(T _x, T _y, T _z) : data{_x, _y, _z} {} + template + explicit TVec3(const TVec3& other) : TVec3(other.x, other.y, other.z) + { + } + TVec3 Cross(const TVec3& rhs) const { return {(y * rhs.z) - (rhs.y * z), (z * rhs.x) - (rhs.z * x), (x * rhs.y) - (rhs.x * y)}; @@ -98,6 +103,11 @@ TVec3 operator<(const TVec3& lhs, const TVec3& rhs) return lhs.Map(std::less{}, rhs); } +inline TVec3 operator!(const TVec3& vec) +{ + return {!vec.x, !vec.y, !vec.z}; +} + template auto operator+(const TVec3& lhs, const TVec3& rhs) -> TVec3 { @@ -197,6 +207,11 @@ union TVec2 TVec2() = default; TVec2(T _x, T _y) : data{_x, _y} {} + template + explicit TVec2(const TVec2& other) : TVec2(other.x, other.y) + { + } + T Cross(const TVec2& rhs) const { return (x * rhs.y) - (y * rhs.x); } T Dot(const TVec2& rhs) const { return (x * rhs.x) + (y * rhs.y); } T LengthSquared() const { return Dot(*this); } @@ -217,6 +232,20 @@ union TVec2 return *this; } + TVec2& operator*=(const TVec2& rhs) + { + x *= rhs.x; + y *= rhs.y; + return *this; + } + + TVec2& operator/=(const TVec2& rhs) + { + x /= rhs.x; + y /= rhs.y; + return *this; + } + TVec2& operator*=(T scalar) { x *= scalar; @@ -242,6 +271,17 @@ union TVec2 }; }; +template +TVec2 operator<(const TVec2& lhs, const TVec2& rhs) +{ + return {lhs.x < rhs.x, lhs.y < rhs.y}; +} + +inline TVec2 operator!(const TVec2& vec) +{ + return {!vec.x, !vec.y}; +} + template TVec2 operator+(TVec2 lhs, const TVec2& rhs) { @@ -255,15 +295,27 @@ TVec2 operator-(TVec2 lhs, const TVec2& rhs) } template -TVec2 operator*(TVec2 lhs, T scalar) +TVec2 operator*(TVec2 lhs, const TVec2& rhs) { - return lhs *= scalar; + return lhs *= rhs; } template -TVec2 operator/(TVec2 lhs, T scalar) +TVec2 operator/(TVec2 lhs, const TVec2& rhs) { - return lhs /= scalar; + return lhs /= rhs; +} + +template +auto operator*(TVec2 lhs, T2 scalar) +{ + return TVec2(lhs) *= scalar; +} + +template +auto operator/(TVec2 lhs, T2 scalar) +{ + return TVec2(lhs) /= scalar; } using Vec2 = TVec2; diff --git a/Source/Core/Core/ConfigManager.cpp b/Source/Core/Core/ConfigManager.cpp index 1e8272ad5e..0d9821111d 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 d752010af5..60edd466b0 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/WiimoteCommon/DataReport.cpp b/Source/Core/Core/HW/WiimoteCommon/DataReport.cpp index 8f258665f0..0197553517 100644 --- a/Source/Core/Core/HW/WiimoteCommon/DataReport.cpp +++ b/Source/Core/Core/HW/WiimoteCommon/DataReport.cpp @@ -5,6 +5,7 @@ #include #include "Common/BitUtils.h" +#include "Common/MathUtil.h" #include "Core/HW/WiimoteCommon/DataReport.h" namespace WiimoteCommon @@ -75,40 +76,35 @@ struct IncludeAccel : virtual DataReportManipulator void GetAccelData(AccelData* result) const override { const AccelMSB accel = Common::BitCastPtr(data_ptr + 2); - result->x = accel.x << 2; - result->y = accel.y << 2; - result->z = accel.z << 2; - - // LSBs const CoreData core = Common::BitCastPtr(data_ptr); - result->x |= core.acc_bits & 0b11; - result->y |= (core.acc_bits2 & 0b1) << 1; - result->z |= core.acc_bits2 & 0b10; + + // X has 10 bits of precision. + result->value.x = accel.x << 2; + result->value.x |= core.acc_bits & 0b11; + + // Y and Z only have 9 bits of precision. (convert them to 10) + result->value.y = + Common::ExpandValue(accel.y << 1 | Common::ExtractBit<0>(core.acc_bits2), 1); + result->value.z = + Common::ExpandValue(accel.z << 1 | Common::ExtractBit<1>(core.acc_bits2), 1); } void SetAccelData(const AccelData& new_accel) override { - AccelMSB accel = {}; - accel.x = new_accel.x >> 2; - accel.y = new_accel.y >> 2; - accel.z = new_accel.z >> 2; - Common::BitCastPtr(data_ptr + 2) = accel; + Common::BitCastPtr(data_ptr + 2) = AccelMSB(new_accel.value / 4); // LSBs CoreData core = Common::BitCastPtr(data_ptr); - core.acc_bits = (new_accel.x >> 0) & 0b11; - core.acc_bits2 = (new_accel.y >> 1) & 0x1; - core.acc_bits2 |= (new_accel.z & 0xb10); + core.acc_bits = (new_accel.value.x >> 0) & 0b11; + core.acc_bits2 = (new_accel.value.y >> 1) & 0x1; + core.acc_bits2 |= (new_accel.value.z & 0xb10); Common::BitCastPtr(data_ptr) = core; } bool HasAccel() const override { return true; } private: - struct AccelMSB - { - u8 x, y, z; - }; + using AccelMSB = Common::TVec3; static_assert(sizeof(AccelMSB) == 3, "Wrong size"); }; @@ -195,26 +191,28 @@ struct ReportExt21 : NoCore, NoAccel, NoIR, IncludeExt<0, 21> struct ReportInterleave1 : IncludeCore, IncludeIR<3, 18, 0>, NoExt { // FYI: Only 8-bits of precision in this report, and no Y axis. - // Only contains 4 MSB of Z axis. - void GetAccelData(AccelData* accel) const override { - accel->x = data_ptr[2] << 2; + // X axis only has 8 bits of precision. (converted to 10) + accel->value.x = Common::ExpandValue(data_ptr[2], 2); - // Retain lower 6 bits. - accel->z &= 0b111111; + // Y axis is not contained in this report. (provided by "Interleave2") + // Clear upper bits, retain lower bits. (provided by "Interleave2") + accel->value.z &= 0b111111; + + // Report only contains 4 MSB of Z axis. const CoreData core = Common::BitCastPtr(data_ptr); - accel->z |= (core.acc_bits << 6) | (core.acc_bits2 << 8); + accel->value.z |= (core.acc_bits << 6) | (core.acc_bits2 << 8); } void SetAccelData(const AccelData& accel) override { - data_ptr[2] = accel.x >> 2; + data_ptr[2] = accel.value.x >> 2; CoreData core = Common::BitCastPtr(data_ptr); - core.acc_bits = (accel.z >> 6) & 0b11; - core.acc_bits2 = (accel.z >> 8) & 0b11; + core.acc_bits = (accel.value.z >> 6) & 0b11; + core.acc_bits2 = (accel.value.z >> 8) & 0b11; Common::BitCastPtr(data_ptr) = core; } @@ -226,26 +224,28 @@ struct ReportInterleave1 : IncludeCore, IncludeIR<3, 18, 0>, NoExt struct ReportInterleave2 : IncludeCore, IncludeIR<3, 18, 18>, NoExt { // FYI: Only 8-bits of precision in this report, and no X axis. - // Only contains 4 LSB of Z axis. - void GetAccelData(AccelData* accel) const override { - accel->y = data_ptr[2] << 2; + // X axis is not contained in this report. (provided by "Interleave1") - // Retain upper 4 bits. - accel->z &= ~0b111111; + // Y axis only has 8 bits of precision. (converted to 10) + accel->value.y = Common::ExpandValue(data_ptr[2], 2); + // Clear lower bits, retain upper bits. (provided by "Interleave1") + accel->value.z &= ~0b111111; + + // Report only contains 4 LSBs of Z axis. (converted to 6) const CoreData core = Common::BitCastPtr(data_ptr); - accel->z |= (core.acc_bits << 2) | (core.acc_bits2 << 4); + accel->value.z |= Common::ExpandValue(core.acc_bits | core.acc_bits2 << 2, 2); } void SetAccelData(const AccelData& accel) override { - data_ptr[2] = accel.y >> 2; + data_ptr[2] = accel.value.y >> 2; CoreData core = Common::BitCastPtr(data_ptr); - core.acc_bits = (accel.z >> 2) & 0b11; - core.acc_bits2 = (accel.z >> 4) & 0b11; + core.acc_bits = (accel.value.z >> 2) & 0b11; + core.acc_bits2 = (accel.value.z >> 4) & 0b11; Common::BitCastPtr(data_ptr) = core; } diff --git a/Source/Core/Core/HW/WiimoteCommon/DataReport.h b/Source/Core/Core/HW/WiimoteCommon/DataReport.h index dd2833728d..75b649d854 100644 --- a/Source/Core/Core/HW/WiimoteCommon/DataReport.h +++ b/Source/Core/Core/HW/WiimoteCommon/DataReport.h @@ -8,9 +8,11 @@ #include #include "Common/CommonTypes.h" +#include "Common/Matrix.h" #include "Core/HW/WiimoteCommon/WiimoteConstants.h" #include "Core/HW/WiimoteCommon/WiimoteHid.h" #include "Core/HW/WiimoteCommon/WiimoteReport.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" namespace WiimoteCommon { @@ -21,12 +23,6 @@ class DataReportManipulator public: virtual ~DataReportManipulator() = default; - // Accel data handled as if there were always 10 bits of precision. - struct AccelData - { - u16 x, y, z; - }; - using CoreData = ButtonData; virtual bool HasCore() const = 0; @@ -66,7 +62,6 @@ public: explicit DataReportBuilder(InputReportID rpt_id); using CoreData = ButtonData; - using AccelData = DataReportManipulator::AccelData; void SetMode(InputReportID rpt_id); InputReportID GetMode() const; @@ -99,11 +94,10 @@ public: u32 GetDataSize() const; -private: static constexpr int HEADER_SIZE = 2; - static constexpr int MAX_DATA_SIZE = MAX_PAYLOAD - 2; +private: TypedHIDInputData> m_data; std::unique_ptr m_manip; diff --git a/Source/Core/Core/HW/WiimoteCommon/WiimoteConstants.h b/Source/Core/Core/HW/WiimoteCommon/WiimoteConstants.h index 99cf0e04e7..2203eb3df2 100644 --- a/Source/Core/Core/HW/WiimoteCommon/WiimoteConstants.h +++ b/Source/Core/Core/HW/WiimoteCommon/WiimoteConstants.h @@ -10,6 +10,10 @@ namespace WiimoteCommon { constexpr u8 MAX_PAYLOAD = 23; +// 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; + enum class InputReportID : u8 { Status = 0x20, diff --git a/Source/Core/Core/HW/WiimoteCommon/WiimoteReport.h b/Source/Core/Core/HW/WiimoteCommon/WiimoteReport.h index 58332383d0..35c81518bb 100644 --- a/Source/Core/Core/HW/WiimoteCommon/WiimoteReport.h +++ b/Source/Core/Core/HW/WiimoteCommon/WiimoteReport.h @@ -7,7 +7,9 @@ #include #include "Common/CommonTypes.h" +#include "Common/Matrix.h" #include "Core/HW/WiimoteCommon/WiimoteConstants.h" +#include "InputCommon/ControllerEmu/ControllerEmu.h" #ifdef _MSC_VER #pragma warning(push) @@ -41,6 +43,8 @@ static_assert(sizeof(OutputReportGeneric) == 2, "Wrong size"); struct OutputReportRumble { + static constexpr OutputReportID REPORT_ID = OutputReportID::Rumble; + u8 rumble : 1; }; static_assert(sizeof(OutputReportRumble) == 1, "Wrong size"); @@ -55,8 +59,34 @@ struct OutputReportEnableFeature }; static_assert(sizeof(OutputReportEnableFeature) == 1, "Wrong size"); +struct OutputReportIRLogicEnable : OutputReportEnableFeature +{ + static constexpr OutputReportID REPORT_ID = OutputReportID::IRLogicEnable; +}; +static_assert(sizeof(OutputReportIRLogicEnable) == 1, "Wrong size"); + +struct OutputReportIRLogicEnable2 : OutputReportEnableFeature +{ + static constexpr OutputReportID REPORT_ID = OutputReportID::IRLogicEnable2; +}; +static_assert(sizeof(OutputReportIRLogicEnable2) == 1, "Wrong size"); + +struct OutputReportSpeakerEnable : OutputReportEnableFeature +{ + static constexpr OutputReportID REPORT_ID = OutputReportID::SpeakerEnable; +}; +static_assert(sizeof(OutputReportSpeakerEnable) == 1, "Wrong size"); + +struct OutputReportSpeakerMute : OutputReportEnableFeature +{ + static constexpr OutputReportID REPORT_ID = OutputReportID::SpeakerMute; +}; +static_assert(sizeof(OutputReportSpeakerMute) == 1, "Wrong size"); + struct OutputReportLeds { + static constexpr OutputReportID REPORT_ID = OutputReportID::LED; + u8 rumble : 1; u8 ack : 1; u8 : 2; @@ -66,6 +96,8 @@ static_assert(sizeof(OutputReportLeds) == 1, "Wrong size"); struct OutputReportMode { + static constexpr OutputReportID REPORT_ID = OutputReportID::ReportMode; + u8 rumble : 1; u8 ack : 1; u8 continuous : 1; @@ -76,6 +108,8 @@ static_assert(sizeof(OutputReportMode) == 2, "Wrong size"); struct OutputReportRequestStatus { + static constexpr OutputReportID REPORT_ID = OutputReportID::RequestStatus; + u8 rumble : 1; u8 : 7; }; @@ -83,6 +117,8 @@ static_assert(sizeof(OutputReportRequestStatus) == 1, "Wrong size"); struct OutputReportWriteData { + static constexpr OutputReportID REPORT_ID = OutputReportID::WriteData; + u8 rumble : 1; u8 : 1; u8 space : 2; @@ -100,6 +136,8 @@ static_assert(sizeof(OutputReportWriteData) == 21, "Wrong size"); struct OutputReportReadData { + static constexpr OutputReportID REPORT_ID = OutputReportID::ReadData; + u8 rumble : 1; u8 : 1; u8 space : 2; @@ -116,6 +154,8 @@ static_assert(sizeof(OutputReportReadData) == 6, "Wrong size"); struct OutputReportSpeakerData { + static constexpr OutputReportID REPORT_ID = OutputReportID::SpeakerData; + u8 rumble : 1; u8 : 2; u8 length : 5; @@ -157,6 +197,8 @@ static_assert(sizeof(ButtonData) == 2, "Wrong size"); struct InputReportStatus { + static constexpr InputReportID REPORT_ID = InputReportID::Status; + ButtonData buttons; u8 battery_low : 1; u8 extension : 1; @@ -170,6 +212,8 @@ static_assert(sizeof(InputReportStatus) == 6, "Wrong size"); struct InputReportAck { + static constexpr InputReportID REPORT_ID = InputReportID::Ack; + ButtonData buttons; OutputReportID rpt_id; ErrorCode error_code; @@ -178,6 +222,8 @@ static_assert(sizeof(InputReportAck) == 4, "Wrong size"); struct InputReportReadDataReply { + static constexpr InputReportID REPORT_ID = InputReportID::ReadDataReply; + ButtonData buttons; u8 error : 4; u8 size_minus_one : 4; @@ -187,6 +233,64 @@ struct InputReportReadDataReply }; static_assert(sizeof(InputReportReadDataReply) == 21, "Wrong size"); +// Accel data handled as if there were always 10 bits of precision. +using AccelType = Common::TVec3; +using AccelData = ControllerEmu::RawValue; + +// Found in Wiimote EEPROM and Nunchuk "register". +// 0g and 1g points exist. +struct AccelCalibrationPoint +{ + // All components have 10 bits of precision. + u16 GetX() const { return x2 << 2 | x1; } + u16 GetY() const { return y2 << 2 | y1; } + u16 GetZ() const { return z2 << 2 | z1; } + auto Get() const { return AccelType{GetX(), GetY(), GetZ()}; } + + void SetX(u16 x) + { + x2 = x >> 2; + x1 = x; + } + void SetY(u16 y) + { + y2 = y >> 2; + y1 = y; + } + void SetZ(u16 z) + { + z2 = z >> 2; + z1 = z; + } + void Set(AccelType accel) + { + SetX(accel.x); + SetY(accel.y); + SetZ(accel.z); + } + + u8 x2, y2, z2; + u8 z1 : 2; + u8 y1 : 2; + u8 x1 : 2; + u8 : 2; +}; + +// Located at 0x16 and 0x20 of Wii Remote EEPROM. +struct AccelCalibrationData +{ + using Calibration = ControllerEmu::TwoPointCalibration; + + auto GetCalibration() const { return Calibration(zero_g.Get(), one_g.Get()); } + + AccelCalibrationPoint zero_g; + AccelCalibrationPoint one_g; + + u8 volume : 7; + u8 motor : 1; + u8 checksum; +}; + } // namespace WiimoteCommon #pragma pack(pop) diff --git a/Source/Core/Core/HW/WiimoteEmu/Camera.cpp b/Source/Core/Core/HW/WiimoteEmu/Camera.cpp index 25b542bc8a..32cc1752d1 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 29c3d9cd94..547610bd9b 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 9c4fdb0d8f..5ce129507b 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 b72285a159..ca70f60ee4 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 a3ea63161d..478bc0f0f8 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/WiimoteEmu/Extension/Classic.cpp b/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.cpp index d7c34320c9..761950a24e 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.cpp @@ -114,8 +114,10 @@ void Classic::Update() { const ControllerEmu::AnalogStick::StateData left_stick_state = m_left_stick->GetState(); - classic_data.lx = static_cast(LEFT_STICK_CENTER + (left_stick_state.x * LEFT_STICK_RADIUS)); - classic_data.ly = static_cast(LEFT_STICK_CENTER + (left_stick_state.y * LEFT_STICK_RADIUS)); + const u8 x = static_cast(LEFT_STICK_CENTER + (left_stick_state.x * LEFT_STICK_RADIUS)); + const u8 y = static_cast(LEFT_STICK_CENTER + (left_stick_state.y * LEFT_STICK_RADIUS)); + + classic_data.SetLeftStick({x, y}); } // right stick @@ -125,10 +127,7 @@ void Classic::Update() const u8 x = static_cast(RIGHT_STICK_CENTER + (right_stick_data.x * RIGHT_STICK_RADIUS)); const u8 y = static_cast(RIGHT_STICK_CENTER + (right_stick_data.y * RIGHT_STICK_RADIUS)); - classic_data.rx1 = x; - classic_data.rx2 = x >> 1; - classic_data.rx3 = x >> 3; - classic_data.ry = y; + classic_data.SetRightStick({x, y}); } // triggers @@ -139,18 +138,15 @@ void Classic::Update() const u8 lt = static_cast(trigs[0] * TRIGGER_RANGE); const u8 rt = static_cast(trigs[1] * TRIGGER_RANGE); - classic_data.lt1 = lt; - classic_data.lt2 = lt >> 3; - classic_data.rt = rt; + classic_data.SetLeftTrigger(lt); + classic_data.SetRightTrigger(rt); } - // buttons - m_buttons->GetState(&classic_data.bt.hex, classic_button_bitmasks.data()); - // dpad - m_dpad->GetState(&classic_data.bt.hex, classic_dpad_bitmasks.data()); - - // flip button bits - classic_data.bt.hex ^= 0xFFFF; + // buttons and dpad + u16 buttons = 0; + m_buttons->GetState(&buttons, classic_button_bitmasks.data()); + m_dpad->GetState(&buttons, classic_dpad_bitmasks.data()); + classic_data.SetButtons(buttons); Common::BitCastPtr(&m_reg.controller_data) = classic_data; } diff --git a/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.h b/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.h index 124674783b..87397282c7 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.h +++ b/Source/Core/Core/HW/WiimoteEmu/Extension/Classic.h @@ -4,6 +4,9 @@ #pragma once +#include + +#include "Common/Matrix.h" #include "Core/HW/WiimoteCommon/WiimoteReport.h" #include "Core/HW/WiimoteEmu/Extension/Extension.h" @@ -56,12 +59,59 @@ public: }; static_assert(sizeof(ButtonFormat) == 2, "Wrong size"); + static constexpr int LEFT_STICK_BITS = 6; + static constexpr int RIGHT_STICK_BITS = 5; + static constexpr int TRIGGER_BITS = 5; + struct DataFormat { - // lx/ly/lz; left joystick - // rx/ry/rz; right joystick - // lt; left trigger - // rt; left trigger + using StickType = Common::TVec2; + using LeftStickRawValue = ControllerEmu::RawValue; + using RightStickRawValue = ControllerEmu::RawValue; + + using TriggerType = u8; + using TriggerRawValue = ControllerEmu::RawValue; + + // 6-bit X and Y values (0-63) + auto GetLeftStick() const { return LeftStickRawValue{StickType(lx, ly)}; }; + void SetLeftStick(const StickType& value) + { + lx = value.x; + ly = value.y; + } + // 5-bit X and Y values (0-31) + auto GetRightStick() const + { + return RightStickRawValue{StickType(rx1 | rx2 << 1 | rx3 << 3, ry)}; + }; + void SetRightStick(const StickType& value) + { + rx1 = value.x & 0b1; + rx2 = (value.x >> 1) & 0b11; + rx3 = (value.x >> 3) & 0b11; + ry = value.y; + } + // 5-bit values (0-31) + auto GetLeftTrigger() const { return TriggerRawValue(lt1 | lt2 << 3); } + void SetLeftTrigger(TriggerType value) + { + lt1 = value & 0b111; + lt2 = (value >> 3) & 0b11; + } + auto GetRightTrigger() const { return TriggerRawValue(rt); } + void SetRightTrigger(TriggerType value) { rt = value; } + + u16 GetButtons() const + { + // 0 == pressed. + return ~bt.hex; + } + + void SetButtons(u16 value) + { + // 0 == pressed. + bt.hex = ~value; + } u8 lx : 6; // byte 0 u8 rx3 : 2; @@ -80,6 +130,53 @@ public: }; static_assert(sizeof(DataFormat) == 6, "Wrong size"); + static constexpr int CAL_STICK_BITS = 8; + static constexpr int CAL_TRIGGER_BITS = 8; + + struct CalibrationData + { + using StickType = DataFormat::StickType; + using TriggerType = DataFormat::TriggerType; + + using StickCalibration = ControllerEmu::ThreePointCalibration; + using TriggerCalibration = ControllerEmu::TwoPointCalibration; + + static constexpr TriggerType TRIGGER_MAX = std::numeric_limits::max(); + + struct StickAxis + { + u8 max; + u8 min; + u8 center; + }; + + auto GetLeftStick() const + { + return StickCalibration{StickType{left_stick_x.min, left_stick_y.min}, + StickType{left_stick_x.center, left_stick_y.center}, + StickType{left_stick_x.max, left_stick_y.max}}; + } + auto GetRightStick() const + { + return StickCalibration{StickType{right_stick_x.min, right_stick_y.min}, + StickType{right_stick_x.center, right_stick_y.center}, + StickType{right_stick_x.max, right_stick_y.max}}; + } + auto GetLeftTrigger() const { return TriggerCalibration{left_trigger_zero, TRIGGER_MAX}; } + auto GetRightTrigger() const { return TriggerCalibration{right_trigger_zero, TRIGGER_MAX}; } + + StickAxis left_stick_x; + StickAxis left_stick_y; + StickAxis right_stick_x; + StickAxis right_stick_y; + + u8 left_trigger_zero; + u8 right_trigger_zero; + + std::array checksum; + }; + static_assert(sizeof(CalibrationData) == 16, "Wrong size"); + Classic(); void Update() override; @@ -110,13 +207,10 @@ public: static constexpr u8 CAL_STICK_CENTER = 0x80; static constexpr u8 CAL_STICK_RANGE = 0x7f; - static constexpr int CAL_STICK_BITS = 8; - static constexpr int LEFT_STICK_BITS = 6; static constexpr u8 LEFT_STICK_CENTER = CAL_STICK_CENTER >> (CAL_STICK_BITS - LEFT_STICK_BITS); static constexpr u8 LEFT_STICK_RADIUS = CAL_STICK_RANGE >> (CAL_STICK_BITS - LEFT_STICK_BITS); - static constexpr int RIGHT_STICK_BITS = 5; static constexpr u8 RIGHT_STICK_CENTER = CAL_STICK_CENTER >> (CAL_STICK_BITS - RIGHT_STICK_BITS); static constexpr u8 RIGHT_STICK_RADIUS = CAL_STICK_RANGE >> (CAL_STICK_BITS - RIGHT_STICK_BITS); diff --git a/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.cpp b/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.cpp index d1125ba05a..548d62638b 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.cpp @@ -87,10 +87,9 @@ void Nunchuk::Update() } // buttons - m_buttons->GetState(&nc_data.bt.hex, nunchuk_button_bitmasks.data()); - - // flip the button bits :/ - nc_data.bt.hex ^= 0x03; + u8 buttons = 0; + m_buttons->GetState(&buttons, nunchuk_button_bitmasks.data()); + nc_data.SetButtons(buttons); // Acceleration data: EmulateSwing(&m_swing_state, m_swing, 1.f / ::Wiimote::UPDATE_FREQ); @@ -109,13 +108,7 @@ void Nunchuk::Update() // Calibration values are 8-bit but we want 10-bit precision, so << 2. const auto acc = ConvertAccelData(accel, ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2); - - nc_data.ax = (acc.x >> 2) & 0xFF; - nc_data.ay = (acc.y >> 2) & 0xFF; - nc_data.az = (acc.z >> 2) & 0xFF; - nc_data.bt.acc_x_lsb = acc.x & 0x3; - nc_data.bt.acc_y_lsb = acc.y & 0x3; - nc_data.bt.acc_z_lsb = acc.z & 0x3; + nc_data.SetAccel(acc.value); Common::BitCastPtr(&m_reg.controller_data) = nc_data; } diff --git a/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.h b/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.h index af55a02b4b..19236e24f9 100644 --- a/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.h +++ b/Source/Core/Core/HW/WiimoteEmu/Extension/Nunchuk.h @@ -51,25 +51,103 @@ public: }; static_assert(sizeof(ButtonFormat) == 1, "Wrong size"); - union DataFormat + struct DataFormat { - struct + using StickType = Common::TVec2; + using StickRawValue = ControllerEmu::RawValue; + + using AccelType = WiimoteCommon::AccelType; + using AccelData = WiimoteCommon::AccelData; + + auto GetStick() const { return StickRawValue(StickType(jx, jy)); } + + // Components have 10 bits of precision. + u16 GetAccelX() const { return ax << 2 | bt.acc_x_lsb; } + u16 GetAccelY() const { return ay << 2 | bt.acc_y_lsb; } + u16 GetAccelZ() const { return az << 2 | bt.acc_z_lsb; } + auto GetAccel() const { return AccelData{AccelType{GetAccelX(), GetAccelY(), GetAccelZ()}}; } + + void SetAccelX(u16 val) { - // joystick x, y - u8 jx; - u8 jy; + ax = val >> 2; + bt.acc_x_lsb = val & 0b11; + } + void SetAccelY(u16 val) + { + ay = val >> 2; + bt.acc_y_lsb = val & 0b11; + } + void SetAccelZ(u16 val) + { + az = val >> 2; + bt.acc_z_lsb = val & 0b11; + } + void SetAccel(const AccelType& accel) + { + SetAccelX(accel.x); + SetAccelY(accel.y); + SetAccelZ(accel.z); + } - // accelerometer - u8 ax; - u8 ay; - u8 az; + u8 GetButtons() const + { + // 0 == pressed. + return ~bt.hex & (BUTTON_C | BUTTON_Z); + } + void SetButtons(u8 value) + { + // 0 == pressed. + bt.hex |= (BUTTON_C | BUTTON_Z); + bt.hex ^= value & (BUTTON_C | BUTTON_Z); + } - // buttons + accelerometer LSBs - ButtonFormat bt; - }; + // joystick x, y + u8 jx; + u8 jy; + + // accelerometer + u8 ax; + u8 ay; + u8 az; + + // buttons + accelerometer LSBs + ButtonFormat bt; }; static_assert(sizeof(DataFormat) == 6, "Wrong size"); + struct CalibrationData + { + using StickType = DataFormat::StickType; + using StickCalibration = ControllerEmu::ThreePointCalibration; + + using AccelType = WiimoteCommon::AccelType; + using AccelCalibration = ControllerEmu::TwoPointCalibration; + + struct Stick + { + u8 max; + u8 min; + u8 center; + }; + + auto GetStick() const + { + return StickCalibration(StickType{stick_x.min, stick_y.min}, + StickType{stick_x.center, stick_y.center}, + StickType{stick_x.max, stick_y.max}); + } + auto GetAccel() const { return AccelCalibration(accel_zero_g.Get(), accel_one_g.Get()); } + + WiimoteCommon::AccelCalibrationPoint accel_zero_g; + WiimoteCommon::AccelCalibrationPoint accel_one_g; + + Stick stick_x; + Stick stick_y; + + std::array checksum; + }; + static_assert(sizeof(CalibrationData) == 16, "Wrong size"); + Nunchuk(); void Update() override; diff --git a/Source/Core/Core/HW/WiimoteEmu/MotionPlus.cpp b/Source/Core/Core/HW/WiimoteEmu/MotionPlus.cpp index a9ec9cb6cd..ab8be7c19c 100644 --- a/Source/Core/Core/HW/WiimoteEmu/MotionPlus.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/MotionPlus.cpp @@ -16,7 +16,6 @@ #include "Common/Logging/Log.h" #include "Common/MathUtil.h" #include "Common/MsgHandler.h" -#include "Common/Swap.h" #include "Core/HW/Wiimote.h" #include "Core/HW/WiimoteEmu/Dynamics.h" @@ -56,6 +55,41 @@ struct MPI : mbedtls_mpi namespace WiimoteEmu { +Common::Vec3 MotionPlus::DataFormat::Data::GetAngularVelocity(const CalibrationBlocks& blocks) const +{ + // Each axis may be using either slow or fast calibration. + const auto calibration = blocks.GetRelevantCalibration(is_slow); + + // It seems M+ calibration data does not follow the "right-hand rule". + const auto sign_fix = Common::Vec3(-1, +1, -1); + + // Adjust deg/s to rad/s. + constexpr auto scalar = float(MathUtil::TAU / 360); + + return gyro.GetNormalizedValue(calibration.value) * sign_fix * Common::Vec3(calibration.degrees) * + scalar; +} + +auto MotionPlus::CalibrationBlocks::GetRelevantCalibration(SlowType is_slow) const + -> RelevantCalibration +{ + RelevantCalibration result; + + const auto& pitch_block = is_slow.x ? slow : fast; + const auto& roll_block = is_slow.y ? slow : fast; + const auto& yaw_block = is_slow.z ? slow : fast; + + result.value.max = {pitch_block.pitch_scale, roll_block.roll_scale, yaw_block.yaw_scale}; + + result.value.zero = {pitch_block.pitch_zero, roll_block.roll_zero, yaw_block.yaw_zero}; + + result.degrees.x = pitch_block.degrees_div_6 * 6; + result.degrees.y = roll_block.degrees_div_6 * 6; + result.degrees.z = yaw_block.degrees_div_6 * 6; + + return result; +} + MotionPlus::MotionPlus() : Extension("MotionPlus") { } @@ -82,35 +116,20 @@ void MotionPlus::Reset() constexpr u16 ROLL_SCALE = CALIBRATION_ZERO + CALIBRATION_SCALE_OFFSET; constexpr u16 PITCH_SCALE = CALIBRATION_ZERO - CALIBRATION_SCALE_OFFSET; -#pragma pack(push, 1) - struct CalibrationBlock - { - u16 yaw_zero = Common::swap16(CALIBRATION_ZERO); - u16 roll_zero = Common::swap16(CALIBRATION_ZERO); - u16 pitch_zero = Common::swap16(CALIBRATION_ZERO); - u16 yaw_scale = Common::swap16(YAW_SCALE); - u16 roll_scale = Common::swap16(ROLL_SCALE); - u16 pitch_scale = Common::swap16(PITCH_SCALE); - u8 degrees_div_6; - }; - - struct CalibrationData - { - CalibrationBlock fast; - u8 uid_1; - Common::BigEndianValue crc32_msb; - CalibrationBlock slow; - u8 uid_2; - Common::BigEndianValue crc32_lsb; - }; -#pragma pack(pop) - static_assert(sizeof(CalibrationData) == 0x20, "Bad size."); static_assert(CALIBRATION_FAST_SCALE_DEGREES % 6 == 0, "Value should be divisible by 6."); static_assert(CALIBRATION_SLOW_SCALE_DEGREES % 6 == 0, "Value should be divisible by 6."); CalibrationData calibration; + calibration.fast.yaw_zero = calibration.slow.yaw_zero = CALIBRATION_ZERO; + calibration.fast.roll_zero = calibration.slow.roll_zero = CALIBRATION_ZERO; + calibration.fast.pitch_zero = calibration.slow.pitch_zero = CALIBRATION_ZERO; + + calibration.fast.yaw_scale = calibration.slow.yaw_scale = YAW_SCALE; + calibration.fast.roll_scale = calibration.slow.roll_scale = ROLL_SCALE; + calibration.fast.pitch_scale = calibration.slow.pitch_scale = PITCH_SCALE; + calibration.fast.degrees_div_6 = CALIBRATION_FAST_SCALE_DEGREES / 6; calibration.slow.degrees_div_6 = CALIBRATION_SLOW_SCALE_DEGREES / 6; @@ -120,17 +139,22 @@ void MotionPlus::Reset() calibration.uid_1 = 0x0b; calibration.uid_2 = 0xe9; - // Update checksum (crc32 of all data other than the checksum itself): - auto crc_result = crc32(0, Z_NULL, 0); - crc_result = crc32(crc_result, reinterpret_cast(&calibration), 0xe); - crc_result = crc32(crc_result, reinterpret_cast(&calibration) + 0x10, 0xe); - - calibration.crc32_lsb = u16(crc_result); - calibration.crc32_msb = u16(crc_result >> 16); + calibration.UpdateChecksum(); Common::BitCastPtr(m_reg_data.calibration_data.data()) = calibration; } +void MotionPlus::CalibrationData::UpdateChecksum() +{ + // Checksum is crc32 of all data other than the checksum itself. + auto crc_result = crc32(0, Z_NULL, 0); + crc_result = crc32(crc_result, reinterpret_cast(this), 0xe); + crc_result = crc32(crc_result, reinterpret_cast(this) + 0x10, 0xe); + + crc32_lsb = u16(crc_result); + crc32_msb = u16(crc_result >> 16); +} + void MotionPlus::DoState(PointerWrap& p) { p.Do(m_reg_data); @@ -547,47 +571,10 @@ void MotionPlus::PrepareInput(const Common::Vec3& angular_velocity) break; } case PassthroughMode::Nunchuk: - { - if (EXT_AMT == m_i2c_bus.BusRead(EXT_SLAVE, EXT_ADDR, EXT_AMT, data)) - { - // Passthrough data modifications via wiibrew.org - // Verified on real hardware via a test of every bit. - // Data passing through drops the least significant bit of the three accelerometer values. - // Bit 7 of byte 5 is moved to bit 6 of byte 5, overwriting it - Common::SetBit(data[5], 6, Common::ExtractBit(data[5], 7)); - // Bit 0 of byte 4 is moved to bit 7 of byte 5 - Common::SetBit(data[5], 7, Common::ExtractBit(data[4], 0)); - // Bit 3 of byte 5 is moved to bit 4 of byte 5, overwriting it - Common::SetBit(data[5], 4, Common::ExtractBit(data[5], 3)); - // Bit 1 of byte 5 is moved to bit 3 of byte 5 - Common::SetBit(data[5], 3, Common::ExtractBit(data[5], 1)); - // Bit 0 of byte 5 is moved to bit 2 of byte 5, overwriting it - Common::SetBit(data[5], 2, Common::ExtractBit(data[5], 0)); - - mplus_data = Common::BitCastPtr(data); - - // Bit 0 and 1 of byte 5 contain a M+ flag and a zero bit which is set below. - mplus_data.is_mp_data = false; - } - else - { - // Read failed (extension unplugged), Send M+ data instead - mplus_data.is_mp_data = true; - } - break; - } case PassthroughMode::Classic: - { if (EXT_AMT == m_i2c_bus.BusRead(EXT_SLAVE, EXT_ADDR, EXT_AMT, data)) { - // Passthrough data modifications via wiibrew.org - // Verified on real hardware via a test of every bit. - // Data passing through drops the least significant bit of the axes of the left (or only) - // joystick Bit 0 of Byte 4 is overwritten [by the 'extension_connected' flag] Bits 0 and - // 1 of Byte 5 are moved to bit 0 of Bytes 0 and 1, overwriting what was there before. - Common::SetBit(data[0], 0, Common::ExtractBit(data[5], 0)); - Common::SetBit(data[1], 0, Common::ExtractBit(data[5], 1)); - + ApplyPassthroughModifications(GetPassthroughMode(), data); mplus_data = Common::BitCastPtr(data); // Bit 0 and 1 of byte 5 contain a M+ flag and a zero bit which is set below. @@ -599,7 +586,6 @@ void MotionPlus::PrepareInput(const Common::Vec3& angular_velocity) mplus_data.is_mp_data = true; } break; - } default: // This really shouldn't happen as the M+ deactivates on an invalid mode write. ERROR_LOG(WIIMOTE, "M+ unknown passthrough-mode %d", int(GetPassthroughMode())); @@ -664,4 +650,66 @@ void MotionPlus::PrepareInput(const Common::Vec3& angular_velocity) Common::BitCastPtr(data) = mplus_data; } +void MotionPlus::ApplyPassthroughModifications(PassthroughMode mode, u8* data) +{ + if (mode == PassthroughMode::Nunchuk) + { + // Passthrough data modifications via wiibrew.org + // Verified on real hardware via a test of every bit. + // Data passing through drops the least significant bit of the three accelerometer values. + // Bit 7 of byte 5 is moved to bit 6 of byte 5, overwriting it + Common::SetBit<6>(data[5], Common::ExtractBit<7>(data[5])); + // Bit 0 of byte 4 is moved to bit 7 of byte 5 + Common::SetBit<7>(data[5], Common::ExtractBit<0>(data[4])); + // Bit 3 of byte 5 is moved to bit 4 of byte 5, overwriting it + Common::SetBit<4>(data[5], Common::ExtractBit<3>(data[5])); + // Bit 1 of byte 5 is moved to bit 3 of byte 5 + Common::SetBit<3>(data[5], Common::ExtractBit<1>(data[5])); + // Bit 0 of byte 5 is moved to bit 2 of byte 5, overwriting it + Common::SetBit<2>(data[5], Common::ExtractBit<0>(data[5])); + } + else if (mode == PassthroughMode::Classic) + { + // Passthrough data modifications via wiibrew.org + // Verified on real hardware via a test of every bit. + // Data passing through drops the least significant bit of the axes of the left (or only) + // joystick Bit 0 of Byte 4 is overwritten [by the 'extension_connected' flag] Bits 0 and + // 1 of Byte 5 are moved to bit 0 of Bytes 0 and 1, overwriting what was there before. + Common::SetBit<0>(data[0], Common::ExtractBit<0>(data[5])); + Common::SetBit<0>(data[1], Common::ExtractBit<1>(data[5])); + } +} + +void MotionPlus::ReversePassthroughModifications(PassthroughMode mode, u8* data) +{ + if (mode == PassthroughMode::Nunchuk) + { + // Undo M+'s "nunchuk passthrough" modifications. + Common::SetBit<0>(data[5], Common::ExtractBit<2>(data[5])); + Common::SetBit<1>(data[5], Common::ExtractBit<3>(data[5])); + Common::SetBit<3>(data[5], Common::ExtractBit<4>(data[5])); + Common::SetBit<0>(data[4], Common::ExtractBit<7>(data[5])); + Common::SetBit<7>(data[5], Common::ExtractBit<6>(data[5])); + + // Set the overwritten bits from the next LSB. + Common::SetBit<2>(data[5], Common::ExtractBit<3>(data[5])); + Common::SetBit<4>(data[5], Common::ExtractBit<5>(data[5])); + Common::SetBit<6>(data[5], Common::ExtractBit<7>(data[5])); + } + else if (mode == PassthroughMode::Classic) + { + // Undo M+'s "classic controller passthrough" modifications. + Common::SetBit<0>(data[5], Common::ExtractBit<0>(data[0])); + Common::SetBit<1>(data[5], Common::ExtractBit<0>(data[1])); + + // Set the overwritten bits from the next LSB. + Common::SetBit<0>(data[0], Common::ExtractBit<1>(data[0])); + Common::SetBit<0>(data[1], Common::ExtractBit<1>(data[1])); + + // This is an overwritten unused button bit on the Classic Controller. + // Note it's a significant bit on the DJ Hero Turntable. (passthrough not feasible) + Common::SetBit<0>(data[4], 1); + } +} + } // namespace WiimoteEmu diff --git a/Source/Core/Core/HW/WiimoteEmu/MotionPlus.h b/Source/Core/Core/HW/WiimoteEmu/MotionPlus.h index 07869957f7..63807db415 100644 --- a/Source/Core/Core/HW/WiimoteEmu/MotionPlus.h +++ b/Source/Core/Core/HW/WiimoteEmu/MotionPlus.h @@ -7,59 +7,90 @@ #include #include "Common/CommonTypes.h" +#include "Common/Swap.h" #include "Core/HW/WiimoteEmu/Dynamics.h" #include "Core/HW/WiimoteEmu/ExtensionPort.h" #include "Core/HW/WiimoteEmu/I2CBus.h" namespace WiimoteEmu { -struct AngularVelocity; - struct MotionPlus : public Extension { public: - MotionPlus(); - - void Update() override; - void Reset() override; - void DoState(PointerWrap& p) override; - - ExtensionPort& GetExtPort(); - - // Vec3 is interpreted as radians/s about the x,y,z axes following the "right-hand rule". - void PrepareInput(const Common::Vec3& angular_velocity); - -private: - enum class ChallengeState : u8 - { - // Note: This is not a value seen on a real M+. - // Used to emulate activation state during which the M+ is not responsive. - Activating = 0x00, - - PreparingX = 0x02, - ParameterXReady = 0x0e, - PreparingY = 0x14, - ParameterYReady = 0x1a, - }; - enum class PassthroughMode : u8 { + // Note: `Disabled` is an M+ enabled with no passthrough. Maybe there is a better name. Disabled = 0x04, Nunchuk = 0x05, Classic = 0x07, }; - enum class ActivationStatus +#pragma pack(push, 1) + struct CalibrationBlock { - Inactive, - Activating, - Deactivating, - Active, + Common::BigEndianValue yaw_zero; + Common::BigEndianValue roll_zero; + Common::BigEndianValue pitch_zero; + Common::BigEndianValue yaw_scale; + Common::BigEndianValue roll_scale; + Common::BigEndianValue pitch_scale; + u8 degrees_div_6; }; -#pragma pack(push, 1) + struct CalibrationBlocks + { + using GyroType = Common::TVec3; + using SlowType = Common::TVec3; + + struct RelevantCalibration + { + ControllerEmu::TwoPointCalibration value; + Common::TVec3 degrees; + }; + + // Each axis may be using either slow or fast calibration. + // This function builds calibration that is relevant for current data. + RelevantCalibration GetRelevantCalibration(SlowType is_slow) const; + + CalibrationBlock fast; + CalibrationBlock slow; + }; + + struct CalibrationData + { + void UpdateChecksum(); + + CalibrationBlock fast; + u8 uid_1; + Common::BigEndianValue crc32_msb; + CalibrationBlock slow; + u8 uid_2; + Common::BigEndianValue crc32_lsb; + }; + static_assert(sizeof(CalibrationData) == 0x20, "Wrong size"); + struct DataFormat { + using GyroType = CalibrationBlocks::GyroType; + using SlowType = CalibrationBlocks::SlowType; + using GyroRawValue = ControllerEmu::RawValue; + + struct Data + { + // Return radian/s following "right-hand rule" with given calibration blocks. + Common::Vec3 GetAngularVelocity(const CalibrationBlocks&) const; + + GyroRawValue gyro; + SlowType is_slow; + }; + + auto GetData() const + { + return Data{ + GyroRawValue{GyroType(pitch1 | pitch2 << 8, roll1 | roll2 << 8, yaw1 | yaw2 << 8)}, + SlowType(pitch_slow, roll_slow, yaw_slow)}; + } + // yaw1, roll1, pitch1: Bits 0-7 // yaw2, roll2, pitch2: Bits 8-13 @@ -79,7 +110,50 @@ private: u8 is_mp_data : 1; u8 pitch2 : 6; }; + static_assert(sizeof(DataFormat) == 6, "Wrong size"); +#pragma pack(pop) + static constexpr u8 INACTIVE_DEVICE_ADDR = 0x53; + static constexpr u8 ACTIVE_DEVICE_ADDR = 0x52; + static constexpr u8 PASSTHROUGH_MODE_OFFSET = 0xfe; + + MotionPlus(); + + void Update() override; + void Reset() override; + void DoState(PointerWrap& p) override; + + ExtensionPort& GetExtPort(); + + // Vec3 is interpreted as radians/s about the x,y,z axes following the "right-hand rule". + void PrepareInput(const Common::Vec3& angular_velocity); + + // Pointer to 6 bytes is expected. + static void ApplyPassthroughModifications(PassthroughMode, u8* data); + static void ReversePassthroughModifications(PassthroughMode, u8* data); + +private: + enum class ChallengeState : u8 + { + // Note: This is not a value seen on a real M+. + // Used to emulate activation state during which the M+ is not responsive. + Activating = 0x00, + + PreparingX = 0x02, + ParameterXReady = 0x0e, + PreparingY = 0x14, + ParameterYReady = 0x1a, + }; + + enum class ActivationStatus + { + Inactive, + Activating, + Deactivating, + Active, + }; + +#pragma pack(push, 1) struct Register { std::array controller_data; @@ -135,14 +209,8 @@ private: std::array ext_identifier; }; #pragma pack(pop) - static_assert(sizeof(DataFormat) == 6, "Wrong size"); static_assert(0x100 == sizeof(Register), "Wrong size"); - static constexpr u8 INACTIVE_DEVICE_ADDR = 0x53; - static constexpr u8 ACTIVE_DEVICE_ADDR = 0x52; - - static constexpr u8 PASSTHROUGH_MODE_OFFSET = 0xfe; - static constexpr int CALIBRATION_BITS = 16; static constexpr u16 CALIBRATION_ZERO = 1 << (CALIBRATION_BITS - 1); diff --git a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp index 65eaa0612b..a9d564d116 100644 --- a/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp +++ b/Source/Core/Core/HW/WiimoteEmu/WiimoteEmu.cpp @@ -500,7 +500,7 @@ void Wiimote::SendDataReport() if (rpt_builder.HasAccel()) { // Calibration values are 8-bit but we want 10-bit precision, so << 2. - DataReportBuilder::AccelData accel = + AccelData accel = ConvertAccelData(GetTotalAcceleration(), ACCEL_ZERO_G << 2, ACCEL_ONE_G << 2); rpt_builder.SetAccelData(accel); } diff --git a/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm b/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm index 41a09ce363..de652db756 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm +++ b/Source/Core/Core/HW/WiimoteReal/IOdarwin.mm @@ -257,12 +257,13 @@ void WiimoteDarwin::DisablePowerAssertionInternal() @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 WiimoteDarwin::DisablePowerAssertionInternal() @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 WiimoteDarwin::DisablePowerAssertionInternal() 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 8822621de4..595bc5922c 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; + // 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; - // Does stuff needed to detect disconnects on Windows - for (const auto& backend : m_backends) - backend->Update(); - - if (0 == CalculateWantedWiimotes() && 0 == CalculateWantedBB()) - 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 4c04a492ac..5b20694ca2 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h @@ -65,6 +65,7 @@ public: void Update(); bool CheckForButtonPress(); + bool GetNextReport(Report* report); Report& ProcessReadQueue(); void Read(); @@ -101,8 +102,16 @@ public: 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 @@ public: private: void ThreadFunc(); + void PoolThreadFunc(); std::vector> m_backends; mutable std::mutex m_backends_mutex; @@ -183,10 +193,13 @@ private: 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/Core/Movie.cpp b/Source/Core/Core/Movie.cpp index d5ceda57f5..f85c625016 100644 --- a/Source/Core/Core/Movie.cpp +++ b/Source/Core/Core/Movie.cpp @@ -678,12 +678,13 @@ static void SetWiiInputDisplayString(int remoteID, const DataReportBuilder& rpt, if (rpt.HasAccel()) { - DataReportBuilder::AccelData accel_data; + AccelData accel_data; rpt.GetAccelData(&accel_data); // FYI: This will only print partial data for interleaved reports. - display_str += fmt::format(" ACC:{},{},{}", accel_data.x, accel_data.y, accel_data.z); + display_str += + fmt::format(" ACC:{},{},{}", accel_data.value.x, accel_data.value.y, accel_data.value.z); } if (rpt.HasIR()) @@ -707,9 +708,8 @@ static void SetWiiInputDisplayString(int remoteID, const DataReportBuilder& rpt, key.Decrypt((u8*)&nunchuk, 0, sizeof(nunchuk)); nunchuk.bt.hex = nunchuk.bt.hex ^ 0x3; - const std::string accel = fmt::format( - " N-ACC:{},{},{}", (nunchuk.ax << 2) | nunchuk.bt.acc_x_lsb, - (nunchuk.ay << 2) | nunchuk.bt.acc_y_lsb, (nunchuk.az << 2) | nunchuk.bt.acc_z_lsb); + const std::string accel = fmt::format(" N-ACC:{},{},{}", nunchuk.GetAccelX(), + nunchuk.GetAccelY(), nunchuk.GetAccelZ()); if (nunchuk.bt.c) display_str += " C"; @@ -756,10 +756,14 @@ static void SetWiiInputDisplayString(int remoteID, const DataReportBuilder& rpt, if (cc.bt.home) display_str += " HOME"; - display_str += Analog1DToString(cc.lt1 | (cc.lt2 << 3), " L", 31); - display_str += Analog1DToString(cc.rt, " R", 31); - display_str += Analog2DToString(cc.lx, cc.ly, " ANA", 63); - display_str += Analog2DToString(cc.rx1 | (cc.rx2 << 1) | (cc.rx3 << 3), cc.ry, " R-ANA", 31); + display_str += Analog1DToString(cc.GetLeftTrigger().value, " L", 31); + display_str += Analog1DToString(cc.GetRightTrigger().value, " R", 31); + + const auto left_stick = cc.GetLeftStick().value; + display_str += Analog2DToString(left_stick.x, left_stick.y, " ANA", 63); + + const auto right_stick = cc.GetRightStick().value; + display_str += Analog2DToString(right_stick.x, right_stick.y, " R-ANA", 31); } std::lock_guard guard(s_input_display_lock); diff --git a/Source/Core/DolphinQt/Config/ControllersWindow.cpp b/Source/Core/DolphinQt/Config/ControllersWindow.cpp index 1c60cd4682..65ab32cbe6 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(); - - if (i < 2) - m_wiimote_pt_labels[i]->setEnabled(passthrough); - - m_wiimote_labels[i]->setEnabled(!passthrough); - m_wiimote_boxes[i]->setEnabled(!passthrough); - m_wiimote_buttons[i]->setEnabled(!passthrough && index != 0 && index != 2); - } - - m_wiimote_refresh->setEnabled(!passthrough); - m_wiimote_real_balance_board->setEnabled(!passthrough); - m_wiimote_speaker_data->setEnabled(!passthrough); - m_wiimote_continuous_scanning->setEnabled(!passthrough); + UpdateDisabledWiimoteControls(); } -void ControllersWindow::OnWiimoteTypeChanged(int type) +void ControllersWindow::UpdateDisabledWiimoteControls() { - const auto* box = static_cast(QObject::sender()); + const bool running = Core::GetState() != Core::State::Uninitialized; + + m_wiimote_emu->setEnabled(!running); + m_wiimote_passthrough->setEnabled(!running); + + 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); + 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 368ef36cdb..93807be9b6 100644 --- a/Source/Core/DolphinQt/Config/ControllersWindow.h +++ b/Source/Core/DolphinQt/Config/ControllersWindow.h @@ -27,9 +27,8 @@ public: 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 @@ private: 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 2fd39b2a5b..e6549e48d6 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.cpp @@ -159,10 +159,12 @@ bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::Calibration // Even the GC controller's small range would pass this test. constexpr double REASONABLE_AVERAGE_RADIUS = 0.6; - const double sum = std::accumulate(data.begin(), data.end(), 0.0); - const double mean = sum / data.size(); + MathUtil::RunningVariance stats; - if (mean < REASONABLE_AVERAGE_RADIUS) + for (auto& x : data) + stats.Push(x); + + if (stats.Mean() < REASONABLE_AVERAGE_RADIUS) { return false; } @@ -173,11 +175,7 @@ bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::Calibration // Approx. deviation of a square input gate, anything much more than that would be unusual. constexpr double REASONABLE_DEVIATION = 0.14; - // Population standard deviation. - const double square_sum = std::inner_product(data.begin(), data.end(), data.begin(), 0.0); - const double standard_deviation = std::sqrt(square_sum / data.size() - mean * mean); - - return standard_deviation < REASONABLE_DEVIATION; + return stats.StandardDeviation() < REASONABLE_DEVIATION; } // Used to test for a miscalibrated stick so the user can be informed. @@ -754,33 +752,34 @@ void AccelerometerMappingIndicator::paintEvent(QPaintEvent*) p.setBrush(Qt::NoBrush); p.drawEllipse(QPointF{}, scale * SPHERE_SIZE, scale * SPHERE_SIZE); - // Red dot upright target. - p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); - p.drawEllipse(QPointF{0, SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + p.setPen(Qt::NoPen); // Red dot. const auto point = rotation * Common::Vec3{0, 0, SPHERE_INDICATOR_DIST}; if (point.y > 0 || Common::Vec2(point.x, point.z).Length() > SPHERE_SIZE) { - p.setPen(Qt::NoPen); p.setBrush(GetAdjustedInputColor()); p.drawEllipse(QPointF(point.x, point.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } - // Blue dot target. - p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); - p.setBrush(Qt::NoBrush); - p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); - // Blue dot. const auto point2 = -point; if (point2.y > 0 || Common::Vec2(point2.x, point2.z).Length() > SPHERE_SIZE) { - p.setPen(Qt::NoPen); p.setBrush(Qt::blue); p.drawEllipse(QPointF(point2.x, point2.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } + p.setBrush(Qt::NoBrush); + + // Red dot upright target. + p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{0, SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + + // Blue dot target. + p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + // Only draw g-force text if acceleration data is present. if (!accel_state.has_value()) return; @@ -802,16 +801,18 @@ GyroMappingIndicator::GyroMappingIndicator(ControllerEmu::IMUGyroscope* group) void GyroMappingIndicator::paintEvent(QPaintEvent*) { const auto gyro_state = m_gyro_group.GetState(); + const auto raw_gyro_state = m_gyro_group.GetRawState(); const auto angular_velocity = gyro_state.value_or(Common::Vec3{}); + 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; - // This works well with my DS4 but a potentially noisy device might not behave. - const bool is_stable = angular_velocity.Length() < MathUtil::TAU / 30; + // Consider device stable when data (with deadzone applied) is zero. + const bool is_stable = !angular_velocity.LengthSquared(); if (!is_stable) m_stable_steps = 0; @@ -839,10 +840,39 @@ void GyroMappingIndicator::paintEvent(QPaintEvent*) p.setRenderHint(QPainter::Antialiasing, true); p.setRenderHint(QPainter::SmoothPixmapTransform, true); + // Deadzone. + if (const auto deadzone_value = m_gyro_group.GetDeadzone(); deadzone_value) + { + static constexpr auto DEADZONE_DRAW_SIZE = 1 - SPHERE_SIZE; + static constexpr auto DEADZONE_DRAW_BOTTOM = 1; + + p.setPen(GetDeadZonePen()); + p.setBrush(GetDeadZoneBrush()); + p.scale(-1.0, 1.0); + p.drawRect(-scale, DEADZONE_DRAW_BOTTOM * scale, scale * 2, -scale * DEADZONE_DRAW_SIZE); + p.scale(-1.0, 1.0); + + if (gyro_state.has_value()) + { + const auto max_jitter = + std::max({std::abs(jitter.x), std::abs(jitter.y), std::abs(jitter.z)}); + const auto jitter_line_y = + std::min(max_jitter / deadzone_value * DEADZONE_DRAW_SIZE - DEADZONE_DRAW_BOTTOM, 1.0); + p.setPen(QPen(GetRawInputColor(), INPUT_DOT_RADIUS)); + p.drawLine(-scale, jitter_line_y * -scale, scale, jitter_line_y * -scale); + + // Sphere background. + p.setPen(Qt::NoPen); + p.setBrush(GetBBoxBrush()); + p.drawEllipse(QPointF{}, scale * SPHERE_SIZE, scale * SPHERE_SIZE); + } + } + + // Sphere dots. p.setPen(Qt::NoPen); p.setBrush(GetRawInputColor()); - GenerateFibonacciSphere(SPHERE_POINT_COUNT, [&, this](const Common::Vec3& point) { + GenerateFibonacciSphere(SPHERE_POINT_COUNT, [&](const Common::Vec3& point) { const auto pt = rotation * point; if (pt.y > 0) @@ -850,49 +880,39 @@ void GyroMappingIndicator::paintEvent(QPaintEvent*) }); // Sphere outline. - p.setPen(GetRawInputColor()); + const auto outline_color = is_stable ? + (m_gyro_group.IsCalibrating() ? Qt::blue : GetRawInputColor()) : + GetAdjustedInputColor(); + p.setPen(outline_color); p.setBrush(Qt::NoBrush); p.drawEllipse(QPointF{}, scale * SPHERE_SIZE, scale * SPHERE_SIZE); - // Red dot upright target. - p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); - p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); + p.setPen(Qt::NoPen); // Red dot. const auto point = rotation * Common::Vec3{0, 0, -SPHERE_INDICATOR_DIST}; if (point.y > 0 || Common::Vec2(point.x, point.z).Length() > SPHERE_SIZE) { - p.setPen(Qt::NoPen); p.setBrush(GetAdjustedInputColor()); p.drawEllipse(QPointF(point.x, point.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } - - // Blue dot target. - p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); - p.setBrush(Qt::NoBrush); - p.drawEllipse(QPointF{}, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); - // Blue dot. const auto point2 = rotation * Common::Vec3{0, SPHERE_INDICATOR_DIST, 0}; if (point2.y > 0 || Common::Vec2(point2.x, point2.z).Length() > SPHERE_SIZE) { - p.setPen(Qt::NoPen); p.setBrush(Qt::blue); p.drawEllipse(QPointF(point2.x, point2.z) * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } - // Only draw text if data is present. - if (!gyro_state.has_value()) - return; + p.setBrush(Qt::NoBrush); - // Angle of red dot from starting position. - const auto angle = std::acos(point.Normalized().Dot({0, 0, -1})); + // Red dot upright target. + p.setPen(QPen(GetAdjustedInputColor(), INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{0, -SPHERE_INDICATOR_DIST} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); - // Angle text: - p.setPen(GetTextColor()); - p.drawText(QRectF(-2, 0, scale, scale), Qt::AlignBottom | Qt::AlignRight, - // i18n: "°" is the symbol for degrees (angular measurement). - QString::fromStdString(fmt::format("{:.2f} °", angle / MathUtil::TAU * 360))); + // Blue dot target. + p.setPen(QPen(Qt::blue, INPUT_DOT_RADIUS / 2)); + p.drawEllipse(QPointF{}, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS); } void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point) diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h index 8928f8db72..2405ebcc0c 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h +++ b/Source/Core/DolphinQt/Config/Mapping/MappingIndicator.h @@ -101,6 +101,7 @@ public: private: ControllerEmu::IMUGyroscope& m_gyro_group; Common::Matrix33 m_state; + Common::Vec3 m_previous_velocity = {}; u32 m_stable_steps = 0; }; diff --git a/Source/Core/DolphinQt/TAS/WiiTASInputWindow.cpp b/Source/Core/DolphinQt/TAS/WiiTASInputWindow.cpp index 904e21bf15..b656ca280d 100644 --- a/Source/Core/DolphinQt/TAS/WiiTASInputWindow.cpp +++ b/Source/Core/DolphinQt/TAS/WiiTASInputWindow.cpp @@ -334,18 +334,20 @@ void WiiTASInputWindow::GetValues(DataReportBuilder& rpt, int ext, DataReportBuilder::CoreData core; rpt.GetCoreData(&core); + using EmuWiimote = WiimoteEmu::Wiimote; + u16& buttons = core.hex; - GetButton(m_a_button, buttons, WiimoteEmu::Wiimote::BUTTON_A); - GetButton(m_b_button, buttons, WiimoteEmu::Wiimote::BUTTON_B); - GetButton(m_1_button, buttons, WiimoteEmu::Wiimote::BUTTON_ONE); - GetButton(m_2_button, buttons, WiimoteEmu::Wiimote::BUTTON_TWO); - GetButton(m_plus_button, buttons, WiimoteEmu::Wiimote::BUTTON_PLUS); - GetButton(m_minus_button, buttons, WiimoteEmu::Wiimote::BUTTON_MINUS); - GetButton(m_home_button, buttons, WiimoteEmu::Wiimote::BUTTON_HOME); - GetButton(m_left_button, buttons, WiimoteEmu::Wiimote::PAD_LEFT); - GetButton(m_up_button, buttons, WiimoteEmu::Wiimote::PAD_UP); - GetButton(m_down_button, buttons, WiimoteEmu::Wiimote::PAD_DOWN); - GetButton(m_right_button, buttons, WiimoteEmu::Wiimote::PAD_RIGHT); + GetButton(m_a_button, buttons, EmuWiimote::BUTTON_A); + GetButton(m_b_button, buttons, EmuWiimote::BUTTON_B); + GetButton(m_1_button, buttons, EmuWiimote::BUTTON_ONE); + GetButton(m_2_button, buttons, EmuWiimote::BUTTON_TWO); + GetButton(m_plus_button, buttons, EmuWiimote::BUTTON_PLUS); + GetButton(m_minus_button, buttons, EmuWiimote::BUTTON_MINUS); + GetButton(m_home_button, buttons, EmuWiimote::BUTTON_HOME); + GetButton(m_left_button, buttons, EmuWiimote::PAD_LEFT); + GetButton(m_up_button, buttons, EmuWiimote::PAD_UP); + GetButton(m_down_button, buttons, EmuWiimote::PAD_DOWN); + GetButton(m_right_button, buttons, EmuWiimote::PAD_RIGHT); rpt.SetCoreData(core); } @@ -354,12 +356,12 @@ void WiiTASInputWindow::GetValues(DataReportBuilder& rpt, int ext, { // FYI: Interleaved reports may behave funky as not all data is always available. - DataReportBuilder::AccelData accel; + AccelData accel; rpt.GetAccelData(&accel); - GetSpinBoxU16(m_remote_orientation_x_value, accel.x); - GetSpinBoxU16(m_remote_orientation_y_value, accel.y); - GetSpinBoxU16(m_remote_orientation_z_value, accel.z); + GetSpinBoxU16(m_remote_orientation_x_value, accel.value.x); + GetSpinBoxU16(m_remote_orientation_y_value, accel.value.y); + GetSpinBoxU16(m_remote_orientation_z_value, accel.value.z); rpt.SetAccelData(accel); } @@ -439,26 +441,16 @@ void WiiTASInputWindow::GetValues(DataReportBuilder& rpt, int ext, GetSpinBoxU8(m_nunchuk_stick_x_value, nunchuk.jx); GetSpinBoxU8(m_nunchuk_stick_y_value, nunchuk.jy); - u16 accel_x = nunchuk.ax << 2 & (nunchuk.bt.acc_x_lsb & 0b11); - u16 accel_y = nunchuk.ay << 2 & (nunchuk.bt.acc_y_lsb & 0b11); - u16 accel_z = nunchuk.az << 2 & (nunchuk.bt.acc_z_lsb & 0b11); + auto accel = nunchuk.GetAccel().value; + GetSpinBoxU16(m_nunchuk_orientation_x_value, accel.x); + GetSpinBoxU16(m_nunchuk_orientation_y_value, accel.y); + GetSpinBoxU16(m_nunchuk_orientation_z_value, accel.z); + nunchuk.SetAccel(accel); - GetSpinBoxU16(m_nunchuk_orientation_x_value, accel_x); - GetSpinBoxU16(m_nunchuk_orientation_y_value, accel_y); - GetSpinBoxU16(m_nunchuk_orientation_z_value, accel_z); - - nunchuk.ax = accel_x >> 2; - nunchuk.ay = accel_y >> 2; - nunchuk.az = accel_z >> 2; - - nunchuk.bt.acc_x_lsb = accel_x & 0b11; - nunchuk.bt.acc_y_lsb = accel_y & 0b11; - nunchuk.bt.acc_z_lsb = accel_z & 0b11; - - nunchuk.bt.hex ^= 0b11; - GetButton(m_c_button, nunchuk.bt.hex, WiimoteEmu::Nunchuk::BUTTON_C); - GetButton(m_z_button, nunchuk.bt.hex, WiimoteEmu::Nunchuk::BUTTON_Z); - nunchuk.bt.hex ^= 0b11; + u8 bt = nunchuk.GetButtons(); + GetButton(m_c_button, bt, WiimoteEmu::Nunchuk::BUTTON_C); + GetButton(m_z_button, bt, WiimoteEmu::Nunchuk::BUTTON_Z); + nunchuk.SetButtons(bt); key.Encrypt(reinterpret_cast(&nunchuk), 0, sizeof(nunchuk)); } @@ -470,50 +462,41 @@ void WiiTASInputWindow::GetValues(DataReportBuilder& rpt, int ext, auto& cc = *reinterpret_cast(ext_data); key.Decrypt(reinterpret_cast(&cc), 0, sizeof(cc)); - cc.bt.hex ^= 0xFFFF; - GetButton(m_classic_a_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_A); - GetButton(m_classic_b_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_B); - GetButton(m_classic_x_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_X); - GetButton(m_classic_y_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_Y); - GetButton(m_classic_plus_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_PLUS); - GetButton(m_classic_minus_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_MINUS); - GetButton(m_classic_l_button, cc.bt.hex, WiimoteEmu::Classic::TRIGGER_L); - GetButton(m_classic_r_button, cc.bt.hex, WiimoteEmu::Classic::TRIGGER_R); - GetButton(m_classic_zl_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_ZL); - GetButton(m_classic_zr_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_ZR); - GetButton(m_classic_home_button, cc.bt.hex, WiimoteEmu::Classic::BUTTON_HOME); - GetButton(m_classic_left_button, cc.bt.hex, WiimoteEmu::Classic::PAD_LEFT); - GetButton(m_classic_up_button, cc.bt.hex, WiimoteEmu::Classic::PAD_UP); - GetButton(m_classic_down_button, cc.bt.hex, WiimoteEmu::Classic::PAD_DOWN); - GetButton(m_classic_right_button, cc.bt.hex, WiimoteEmu::Classic::PAD_RIGHT); - cc.bt.hex ^= 0xFFFF; + u16 bt = cc.GetButtons(); + GetButton(m_classic_a_button, bt, WiimoteEmu::Classic::BUTTON_A); + GetButton(m_classic_b_button, bt, WiimoteEmu::Classic::BUTTON_B); + GetButton(m_classic_x_button, bt, WiimoteEmu::Classic::BUTTON_X); + GetButton(m_classic_y_button, bt, WiimoteEmu::Classic::BUTTON_Y); + GetButton(m_classic_plus_button, bt, WiimoteEmu::Classic::BUTTON_PLUS); + GetButton(m_classic_minus_button, bt, WiimoteEmu::Classic::BUTTON_MINUS); + GetButton(m_classic_l_button, bt, WiimoteEmu::Classic::TRIGGER_L); + GetButton(m_classic_r_button, bt, WiimoteEmu::Classic::TRIGGER_R); + GetButton(m_classic_zl_button, bt, WiimoteEmu::Classic::BUTTON_ZL); + GetButton(m_classic_zr_button, bt, WiimoteEmu::Classic::BUTTON_ZR); + GetButton(m_classic_home_button, bt, WiimoteEmu::Classic::BUTTON_HOME); + GetButton(m_classic_left_button, bt, WiimoteEmu::Classic::PAD_LEFT); + GetButton(m_classic_up_button, bt, WiimoteEmu::Classic::PAD_UP); + GetButton(m_classic_down_button, bt, WiimoteEmu::Classic::PAD_DOWN); + GetButton(m_classic_right_button, bt, WiimoteEmu::Classic::PAD_RIGHT); + cc.SetButtons(bt); - u8 rx = (cc.rx1 & 0b1) & ((cc.rx2 & 0b11) << 1) & ((cc.rx3 & 0b11) << 3); - GetSpinBoxU8(m_classic_right_stick_x_value, rx); - cc.rx1 = rx & 0b1; - cc.rx2 = (rx >> 1) & 0b11; - cc.rx3 = (rx >> 3) & 0b11; + auto right_stick = cc.GetRightStick().value; + GetSpinBoxU8(m_classic_right_stick_x_value, right_stick.x); + GetSpinBoxU8(m_classic_right_stick_y_value, right_stick.y); + cc.SetRightStick(right_stick); - u8 ry = cc.ry; - GetSpinBoxU8(m_classic_right_stick_y_value, ry); - cc.ry = ry; + auto left_stick = cc.GetLeftStick().value; + GetSpinBoxU8(m_classic_left_stick_x_value, left_stick.x); + GetSpinBoxU8(m_classic_left_stick_y_value, left_stick.y); + cc.SetLeftStick(left_stick); - u8 lx = cc.lx; - GetSpinBoxU8(m_classic_left_stick_x_value, lx); - cc.lx = lx; - - u8 ly = cc.ly; - GetSpinBoxU8(m_classic_left_stick_y_value, ly); - cc.ly = ly; - - u8 rt = cc.rt; + u8 rt = cc.GetRightTrigger().value; GetSpinBoxU8(m_right_trigger_value, rt); - cc.rt = rt; + cc.SetRightTrigger(rt); - u8 lt = (cc.lt1 & 0b111) & (cc.lt2 >> 3); + u8 lt = cc.GetLeftTrigger().value; GetSpinBoxU8(m_left_trigger_value, lt); - cc.lt1 = lt & 0b111; - cc.lt2 = (lt >> 3) & 0b11; + cc.SetLeftTrigger(lt); key.Encrypt(reinterpret_cast(&cc), 0, sizeof(cc)); } diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index 30b2a4714c..d5efe58248 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 b5a621e43b..382bd8c2a4 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/ControllerEmu/ControlGroup/IMUGyroscope.cpp b/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.cpp index 84f9f20821..8837c67f13 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.cpp @@ -7,6 +7,7 @@ #include #include "Common/Common.h" +#include "Common/MathUtil.h" #include "InputCommon/ControlReference/ControlReference.h" #include "InputCommon/ControllerEmu/Control/Control.h" @@ -14,6 +15,15 @@ namespace ControllerEmu { +// Maximum period for calculating an average stable value. +// Just to prevent failures due to timer overflow. +static constexpr auto MAXIMUM_CALIBRATION_DURATION = std::chrono::hours(1); + +// If calibration updates do not happen at this rate, restart calibration period. +// This prevents calibration across periods of no regular updates. (e.g. between game sessions) +// This is made slightly lower than the UI update frequency of 30. +static constexpr auto WORST_ACCEPTABLE_CALIBRATION_UPDATE_FREQUENCY = 25; + IMUGyroscope::IMUGyroscope(std::string name, std::string ui_name) : ControlGroup(std::move(name), std::move(ui_name), GroupType::IMUGyroscope) { @@ -23,18 +33,130 @@ IMUGyroscope::IMUGyroscope(std::string name, std::string ui_name) AddInput(Translate, _trans("Roll Right")); AddInput(Translate, _trans("Yaw Left")); AddInput(Translate, _trans("Yaw Right")); + + AddSetting(&m_deadzone_setting, + {_trans("Dead Zone"), + // i18n: "°/s" is the symbol for degrees (angular measurement) divided by seconds. + _trans("°/s"), + // i18n: Refers to the dead-zone setting of gyroscope input. + _trans("Angular velocity to ignore.")}, + 2, 0, 180); + + AddSetting(&m_calibration_period_setting, + {_trans("Calibration Period"), + // i18n: "s" is the symbol for seconds. + _trans("s"), + // i18n: Refers to the "Calibration" setting of gyroscope input. + _trans("Time period of stable input to trigger calibration. (zero to disable)")}, + 3, 0, 30); +} + +void IMUGyroscope::RestartCalibration() const +{ + m_calibration_period_start = Clock::now(); + m_running_calibration.Clear(); +} + +void IMUGyroscope::UpdateCalibration(const StateData& state) const +{ + const auto now = Clock::now(); + const auto calibration_period = m_calibration_period_setting.GetValue(); + + // If calibration time is zero. User is choosing to not calibrate. + if (!calibration_period) + { + // Set calibration to zero. + m_calibration = {}; + RestartCalibration(); + return; + } + + // If there is no running calibration a new gyro was just mapped or calibration was just enabled, + // apply the current state as calibration, it's often better than zeros. + if (!m_running_calibration.Count()) + { + m_calibration = state; + } + else + { + const auto calibration_freq = + m_running_calibration.Count() / + std::chrono::duration_cast>(now - m_calibration_period_start) + .count(); + + const auto potential_calibration = m_running_calibration.Mean(); + const auto current_difference = state - potential_calibration; + const auto deadzone = GetDeadzone(); + + // Check for required calibration update frequency + // and if current data is within deadzone distance of mean stable value. + if (calibration_freq < WORST_ACCEPTABLE_CALIBRATION_UPDATE_FREQUENCY || + std::any_of(current_difference.data.begin(), current_difference.data.end(), + [&](auto c) { return std::abs(c) > deadzone; })) + { + RestartCalibration(); + } + } + + // Update running mean stable value. + m_running_calibration.Push(state); + + // Apply calibration after configured time. + const auto calibration_duration = now - m_calibration_period_start; + if (calibration_duration >= std::chrono::duration(calibration_period)) + { + m_calibration = m_running_calibration.Mean(); + + if (calibration_duration >= MAXIMUM_CALIBRATION_DURATION) + { + RestartCalibration(); + m_running_calibration.Push(m_calibration); + } + } +} + +auto IMUGyroscope::GetRawState() const -> StateData +{ + return StateData(controls[1]->GetState() - controls[0]->GetState(), + controls[2]->GetState() - controls[3]->GetState(), + controls[4]->GetState() - controls[5]->GetState()); } std::optional IMUGyroscope::GetState() const { if (controls[0]->control_ref->BoundCount() == 0) + { + // Set calibration to zero. + m_calibration = {}; + RestartCalibration(); return std::nullopt; + } + + auto state = GetRawState(); + + // If the input gate is disabled, miscalibration to zero values would occur. + if (ControlReference::GetInputGate()) + UpdateCalibration(state); + + state -= m_calibration; + + // Apply "deadzone". + for (auto& c : state.data) + c *= std::abs(c) > GetDeadzone(); - StateData state; - state.x = (controls[1]->GetState() - controls[0]->GetState()); - state.y = (controls[2]->GetState() - controls[3]->GetState()); - state.z = (controls[4]->GetState() - controls[5]->GetState()); return state; } +ControlState IMUGyroscope::GetDeadzone() const +{ + return m_deadzone_setting.GetValue() / 360 * MathUtil::TAU; +} + +bool IMUGyroscope::IsCalibrating() const +{ + const auto calibration_period = m_calibration_period_setting.GetValue(); + return calibration_period && (Clock::now() - m_calibration_period_start) >= + std::chrono::duration(calibration_period); +} + } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.h b/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.h index c173c2f8b2..459f885882 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.h +++ b/Source/Core/InputCommon/ControllerEmu/ControlGroup/IMUGyroscope.h @@ -4,11 +4,14 @@ #pragma once +#include #include #include +#include "Common/MathUtil.h" #include "Common/Matrix.h" #include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h" +#include "InputCommon/ControllerEmu/Setting/NumericSetting.h" namespace ControllerEmu { @@ -19,6 +22,25 @@ public: IMUGyroscope(std::string name, std::string ui_name); + StateData GetRawState() const; std::optional GetState() const; + + // Value is in rad/s. + ControlState GetDeadzone() const; + + bool IsCalibrating() const; + +private: + using Clock = std::chrono::steady_clock; + + void RestartCalibration() const; + void UpdateCalibration(const StateData&) const; + + SettingValue m_deadzone_setting; + SettingValue m_calibration_period_setting; + + mutable StateData m_calibration = {}; + mutable MathUtil::RunningMean m_running_calibration; + mutable Clock::time_point m_calibration_period_start = Clock::now(); }; } // namespace ControllerEmu diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h index 2611880ee9..3f497b1f88 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h @@ -11,6 +11,7 @@ #include #include +#include "Common/BitUtils.h" #include "Common/Common.h" #include "Common/IniFile.h" #include "InputCommon/ControlReference/ExpressionParser.h" @@ -27,6 +28,106 @@ namespace ControllerEmu { class ControlGroup; +// Represents calibration data found on Wii Remotes + extensions with a zero and a max value. +// (e.g. accelerometer data) +// Bits of precision specified to handle common situation of differing precision in the actual data. +template +struct TwoPointCalibration +{ + TwoPointCalibration() = default; + TwoPointCalibration(const T& zero_, const T& max_) : zero{zero_}, max{max_} {} + + static constexpr size_t BITS_OF_PRECISION = Bits; + + T zero; + T max; +}; + +// Represents calibration data with a min, zero, and max value. (e.g. joystick data) +template +struct ThreePointCalibration +{ + ThreePointCalibration() = default; + ThreePointCalibration(const T& min_, const T& zero_, const T& max_) + : min{min_}, zero{zero_}, max{max_} + { + } + + static constexpr size_t BITS_OF_PRECISION = Bits; + + T min; + T zero; + T max; +}; + +// Represents a raw/uncalibrated N-dimensional value of input data. (e.g. Joystick X and Y) +// A normalized value can be calculated with a provided {Two,Three}PointCalibration. +// Values are adjusted with mismatched bits of precision. +// Underlying type may be an unsigned type or a a Common::TVecN<> of an unsigned type. +template +struct RawValue +{ + RawValue() = default; + explicit RawValue(const T& value_) : value{value_} {} + + static constexpr size_t BITS_OF_PRECISION = Bits; + + T value; + + template + auto GetNormalizedValue(const TwoPointCalibration& calibration) const + { + const auto value_expansion = + std::max(0, int(calibration.BITS_OF_PRECISION) - int(BITS_OF_PRECISION)); + + const auto calibration_expansion = + std::max(0, int(BITS_OF_PRECISION) - int(calibration.BITS_OF_PRECISION)); + + const auto calibration_zero = ExpandValue(calibration.zero, calibration_expansion) * 1.f; + const auto calibration_max = ExpandValue(calibration.max, calibration_expansion) * 1.f; + + // Multiplication by 1.f to floatify either a scalar or a Vec. + return (ExpandValue(value, value_expansion) * 1.f - calibration_zero) / + (calibration_max - calibration_zero); + } + + template + auto GetNormalizedValue(const ThreePointCalibration& calibration) const + { + const auto value_expansion = + std::max(0, int(calibration.BITS_OF_PRECISION) - int(BITS_OF_PRECISION)); + + const auto calibration_expansion = + std::max(0, int(BITS_OF_PRECISION) - int(calibration.BITS_OF_PRECISION)); + + const auto calibration_min = ExpandValue(calibration.min, calibration_expansion) * 1.f; + const auto calibration_zero = ExpandValue(calibration.zero, calibration_expansion) * 1.f; + const auto calibration_max = ExpandValue(calibration.max, calibration_expansion) * 1.f; + + const auto use_max = calibration.zero < value; + + // Multiplication by 1.f to floatify either a scalar or a Vec. + return (ExpandValue(value, value_expansion) * 1.f - calibration_zero) / + (use_max * 1.f * (calibration_max - calibration_zero) + + !use_max * 1.f * (calibration_zero - calibration_min)); + } + + template + static OtherT ExpandValue(OtherT value, size_t bits) + { + if constexpr (std::is_arithmetic_v) + { + return Common::ExpandValue(value, bits); + } + else + { + for (size_t i = 0; i != std::size(value.data); ++i) + value.data[i] = Common::ExpandValue(value.data[i], bits); + return value; + } + } +}; + class EmulatedController { public: diff --git a/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp b/Source/Core/InputCommon/ControllerInterface/ControllerInterface.cpp index 130f0f357a..833f288da5 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" @@ -93,7 +94,7 @@ void ControllerInterface::RefreshDevices() return; { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); m_devices.clear(); } @@ -132,6 +133,8 @@ void ControllerInterface::RefreshDevices() ciface::DualShockUDPClient::PopulateDevices(); #endif + WiimoteReal::ProcessWiimotePool(); + m_is_populating_devices = false; InvokeDevicesChangedCallbacks(); } @@ -146,7 +149,7 @@ void ControllerInterface::Shutdown() m_is_init = false; { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); for (const auto& d : m_devices) { @@ -193,7 +196,7 @@ void ControllerInterface::AddDevice(std::shared_ptr device return; { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); const auto is_id_in_use = [&device, this](int id) { return std::any_of(m_devices.begin(), m_devices.end(), [&device, &id](const auto& d) { @@ -229,7 +232,7 @@ void ControllerInterface::AddDevice(std::shared_ptr device void ControllerInterface::RemoveDevice(std::function callback) { { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); auto it = std::remove_if(m_devices.begin(), m_devices.end(), [&callback](const auto& dev) { if (callback(dev.get())) { @@ -251,7 +254,7 @@ void ControllerInterface::UpdateInput() // Don't block the UI or CPU thread (to avoid a short but noticeable frame drop) if (m_devices_mutex.try_lock()) { - std::lock_guard lk(m_devices_mutex, std::adopt_lock); + std::lock_guard lk(m_devices_mutex, std::adopt_lock); for (const auto& d : m_devices) d->UpdateInput(); } diff --git a/Source/Core/InputCommon/ControllerInterface/Device.cpp b/Source/Core/InputCommon/ControllerInterface/Device.cpp index d91a9e119e..15c403ca24 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Device.cpp @@ -180,7 +180,7 @@ bool DeviceQualifier::operator!=(const DeviceQualifier& devq) const std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) const { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); for (const auto& d : m_devices) { if (devq == d.get()) @@ -192,7 +192,7 @@ std::shared_ptr DeviceContainer::FindDevice(const DeviceQualifier& devq) std::vector DeviceContainer::GetAllDeviceStrings() const { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); std::vector device_strings; DeviceQualifier device_qualifier; @@ -208,7 +208,7 @@ std::vector DeviceContainer::GetAllDeviceStrings() const std::string DeviceContainer::GetDefaultDeviceString() const { - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); if (m_devices.empty()) return ""; @@ -226,7 +226,7 @@ Device::Input* DeviceContainer::FindInput(std::string_view name, const Device* d return inp; } - std::lock_guard lk(m_devices_mutex); + std::lock_guard lk(m_devices_mutex); for (const auto& d : m_devices) { Device::Input* const i = d->FindInput(name); diff --git a/Source/Core/InputCommon/ControllerInterface/Device.h b/Source/Core/InputCommon/ControllerInterface/Device.h index e07f4fb16a..39ff6e9bb5 100644 --- a/Source/Core/InputCommon/ControllerInterface/Device.h +++ b/Source/Core/InputCommon/ControllerInterface/Device.h @@ -198,7 +198,7 @@ public: DetectInput(u32 wait_ms, const std::vector& device_strings) const; protected: - mutable std::mutex m_devices_mutex; + mutable std::recursive_mutex m_devices_mutex; std::vector> m_devices; }; } // namespace Core diff --git a/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp new file mode 100644 index 0000000000..34a169aa83 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.cpp @@ -0,0 +1,1592 @@ +// 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; + + gyro_data = data.GetData().GetAngularVelocity(*calibration); +} + +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 0000000000..75296c65b1 --- /dev/null +++ b/Source/Core/InputCommon/ControllerInterface/Wiimote/Wiimote.h @@ -0,0 +1,270 @@ +// 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; + }; + + 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 1b0b42c7ab..b5c9ef0bef 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 0c8d79326f..671c2b077d 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 +