duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

xinput_source.cpp (18829B)


      1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "xinput_source.h"
      5 #include "input_manager.h"
      6 
      7 #include "common/assert.h"
      8 #include "common/log.h"
      9 #include "common/string_util.h"
     10 
     11 #include "IconsPromptFont.h"
     12 
     13 #include <cmath>
     14 
     15 Log_SetChannel(XInputSource);
     16 
     17 static const char* s_axis_names[XInputSource::NUM_AXES] = {
     18   "LeftX",        // AXIS_LEFTX
     19   "LeftY",        // AXIS_LEFTY
     20   "RightX",       // AXIS_RIGHTX
     21   "RightY",       // AXIS_RIGHTY
     22   "LeftTrigger",  // AXIS_TRIGGERLEFT
     23   "RightTrigger", // AXIS_TRIGGERRIGHT
     24 };
     25 static constexpr const char* s_axis_icons[][2] = {
     26   {ICON_PF_LEFT_ANALOG_LEFT, ICON_PF_LEFT_ANALOG_RIGHT},   // AXIS_LEFTX
     27   {ICON_PF_LEFT_ANALOG_UP, ICON_PF_LEFT_ANALOG_DOWN},      // AXIS_LEFTY
     28   {ICON_PF_RIGHT_ANALOG_LEFT, ICON_PF_RIGHT_ANALOG_RIGHT}, // AXIS_RIGHTX
     29   {ICON_PF_RIGHT_ANALOG_UP, ICON_PF_RIGHT_ANALOG_DOWN},    // AXIS_RIGHTY
     30   {nullptr, ICON_PF_LEFT_TRIGGER_PULL},                    // AXIS_TRIGGERLEFT
     31   {nullptr, ICON_PF_RIGHT_TRIGGER_PULL},                   // AXIS_TRIGGERRIGHT
     32 };
     33 static const GenericInputBinding s_xinput_generic_binding_axis_mapping[][2] = {
     34   {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight},   // AXIS_LEFTX
     35   {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown},      // AXIS_LEFTY
     36   {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // AXIS_RIGHTX
     37   {GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown},    // AXIS_RIGHTY
     38   {GenericInputBinding::Unknown, GenericInputBinding::L2},                     // AXIS_TRIGGERLEFT
     39   {GenericInputBinding::Unknown, GenericInputBinding::R2},                     // AXIS_TRIGGERRIGHT
     40 };
     41 
     42 static const char* s_button_names[XInputSource::NUM_BUTTONS] = {
     43   "DPadUp",        // XINPUT_GAMEPAD_DPAD_UP
     44   "DPadDown",      // XINPUT_GAMEPAD_DPAD_DOWN
     45   "DPadLeft",      // XINPUT_GAMEPAD_DPAD_LEFT
     46   "DPadRight",     // XINPUT_GAMEPAD_DPAD_RIGHT
     47   "Start",         // XINPUT_GAMEPAD_START
     48   "Back",          // XINPUT_GAMEPAD_BACK
     49   "LeftStick",     // XINPUT_GAMEPAD_LEFT_THUMB
     50   "RightStick",    // XINPUT_GAMEPAD_RIGHT_THUMB
     51   "LeftShoulder",  // XINPUT_GAMEPAD_LEFT_SHOULDER
     52   "RightShoulder", // XINPUT_GAMEPAD_RIGHT_SHOULDER
     53   "A",             // XINPUT_GAMEPAD_A
     54   "B",             // XINPUT_GAMEPAD_B
     55   "X",             // XINPUT_GAMEPAD_X
     56   "Y",             // XINPUT_GAMEPAD_Y
     57   "Guide",         // XINPUT_GAMEPAD_GUIDE
     58 };
     59 static const u16 s_button_masks[XInputSource::NUM_BUTTONS] = {
     60   XINPUT_GAMEPAD_DPAD_UP,
     61   XINPUT_GAMEPAD_DPAD_DOWN,
     62   XINPUT_GAMEPAD_DPAD_LEFT,
     63   XINPUT_GAMEPAD_DPAD_RIGHT,
     64   XINPUT_GAMEPAD_START,
     65   XINPUT_GAMEPAD_BACK,
     66   XINPUT_GAMEPAD_LEFT_THUMB,
     67   XINPUT_GAMEPAD_RIGHT_THUMB,
     68   XINPUT_GAMEPAD_LEFT_SHOULDER,
     69   XINPUT_GAMEPAD_RIGHT_SHOULDER,
     70   XINPUT_GAMEPAD_A,
     71   XINPUT_GAMEPAD_B,
     72   XINPUT_GAMEPAD_X,
     73   XINPUT_GAMEPAD_Y,
     74   0x400, // XINPUT_GAMEPAD_GUIDE
     75 };
     76 static constexpr const char* s_button_icons[] = {
     77   ICON_PF_XBOX_DPAD_UP,       // XINPUT_GAMEPAD_DPAD_UP
     78   ICON_PF_XBOX_DPAD_DOWN,     // XINPUT_GAMEPAD_DPAD_DOWN
     79   ICON_PF_XBOX_DPAD_LEFT,     // XINPUT_GAMEPAD_DPAD_LEFT
     80   ICON_PF_XBOX_DPAD_RIGHT,    // XINPUT_GAMEPAD_DPAD_RIGHT
     81   ICON_PF_BURGER_MENU,        // XINPUT_GAMEPAD_START
     82   ICON_PF_SHARE_CAPTURE,      // XINPUT_GAMEPAD_BACK
     83   ICON_PF_LEFT_ANALOG_CLICK,  // XINPUT_GAMEPAD_LEFT_THUMB
     84   ICON_PF_RIGHT_ANALOG_CLICK, // XINPUT_GAMEPAD_RIGHT_THUMB
     85   ICON_PF_LEFT_SHOULDER_LB,   // XINPUT_GAMEPAD_LEFT_SHOULDER
     86   ICON_PF_RIGHT_SHOULDER_RB,  // XINPUT_GAMEPAD_RIGHT_SHOULDER
     87   ICON_PF_BUTTON_A,           // XINPUT_GAMEPAD_A
     88   ICON_PF_BUTTON_B,           // XINPUT_GAMEPAD_B
     89   ICON_PF_BUTTON_X,           // XINPUT_GAMEPAD_X
     90   ICON_PF_BUTTON_Y,           // XINPUT_GAMEPAD_Y
     91   ICON_PF_XBOX,               // XINPUT_GAMEPAD_GUIDE
     92 };
     93 static const GenericInputBinding s_xinput_generic_binding_button_mapping[] = {
     94   GenericInputBinding::DPadUp,    // XINPUT_GAMEPAD_DPAD_UP
     95   GenericInputBinding::DPadDown,  // XINPUT_GAMEPAD_DPAD_DOWN
     96   GenericInputBinding::DPadLeft,  // XINPUT_GAMEPAD_DPAD_LEFT
     97   GenericInputBinding::DPadRight, // XINPUT_GAMEPAD_DPAD_RIGHT
     98   GenericInputBinding::Start,     // XINPUT_GAMEPAD_START
     99   GenericInputBinding::Select,    // XINPUT_GAMEPAD_BACK
    100   GenericInputBinding::L3,        // XINPUT_GAMEPAD_LEFT_THUMB
    101   GenericInputBinding::R3,        // XINPUT_GAMEPAD_RIGHT_THUMB
    102   GenericInputBinding::L1,        // XINPUT_GAMEPAD_LEFT_SHOULDER
    103   GenericInputBinding::R1,        // XINPUT_GAMEPAD_RIGHT_SHOULDER
    104   GenericInputBinding::Cross,     // XINPUT_GAMEPAD_A
    105   GenericInputBinding::Circle,    // XINPUT_GAMEPAD_B
    106   GenericInputBinding::Square,    // XINPUT_GAMEPAD_X
    107   GenericInputBinding::Triangle,  // XINPUT_GAMEPAD_Y
    108   GenericInputBinding::System,    // XINPUT_GAMEPAD_GUIDE
    109 };
    110 
    111 XInputSource::XInputSource() = default;
    112 
    113 XInputSource::~XInputSource() = default;
    114 
    115 bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
    116 {
    117   // xinput1_3.dll is flawed and obsolete, but it's also commonly used by wrappers.
    118   // For this reason, try to load it *only* from the application directory, and not system32.
    119   m_xinput_module = LoadLibraryExW(L"xinput1_3", nullptr, LOAD_LIBRARY_SEARCH_APPLICATION_DIR);
    120   if (!m_xinput_module)
    121   {
    122     m_xinput_module = LoadLibraryW(L"xinput1_4");
    123   }
    124   if (!m_xinput_module)
    125   {
    126     m_xinput_module = LoadLibraryW(L"xinput9_1_0");
    127   }
    128   if (!m_xinput_module)
    129   {
    130     ERROR_LOG("Failed to load XInput module.");
    131     return false;
    132   }
    133 
    134   // Try the hidden version of XInputGetState(), which lets us query the guide button.
    135   m_xinput_get_state =
    136     reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, reinterpret_cast<LPCSTR>(100)));
    137   if (!m_xinput_get_state)
    138   {
    139     m_xinput_get_state =
    140       reinterpret_cast<decltype(m_xinput_get_state)>(GetProcAddress(m_xinput_module, "XInputGetState"));
    141   }
    142   m_xinput_set_state =
    143     reinterpret_cast<decltype(m_xinput_set_state)>(GetProcAddress(m_xinput_module, "XInputSetState"));
    144   m_xinput_get_capabilities =
    145     reinterpret_cast<decltype(m_xinput_get_capabilities)>(GetProcAddress(m_xinput_module, "XInputGetCapabilities"));
    146 
    147   if (!m_xinput_get_state || !m_xinput_set_state || !m_xinput_get_capabilities)
    148   {
    149     ERROR_LOG("Failed to get XInput function pointers.");
    150     return false;
    151   }
    152 
    153   ReloadDevices();
    154   return true;
    155 }
    156 
    157 void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
    158 {
    159 }
    160 
    161 bool XInputSource::ReloadDevices()
    162 {
    163   bool changed = false;
    164   for (u32 i = 0; i < NUM_CONTROLLERS; i++)
    165   {
    166     XINPUT_STATE new_state;
    167     DWORD result = m_xinput_get_state(i, &new_state);
    168 
    169     if (result == ERROR_SUCCESS)
    170     {
    171       if (m_controllers[i].connected)
    172         continue;
    173 
    174       HandleControllerConnection(i);
    175       changed = true;
    176     }
    177     else if (result == ERROR_DEVICE_NOT_CONNECTED)
    178     {
    179       if (!m_controllers[i].connected)
    180         continue;
    181 
    182       HandleControllerDisconnection(i);
    183       changed = true;
    184     }
    185   }
    186 
    187   return changed;
    188 }
    189 
    190 void XInputSource::Shutdown()
    191 {
    192   for (u32 i = 0; i < NUM_CONTROLLERS; i++)
    193   {
    194     if (m_controllers[i].connected)
    195       HandleControllerDisconnection(i);
    196   }
    197 
    198   if (m_xinput_module)
    199   {
    200     FreeLibrary(m_xinput_module);
    201     m_xinput_module = nullptr;
    202   }
    203 
    204   m_xinput_get_state = nullptr;
    205   m_xinput_set_state = nullptr;
    206   m_xinput_get_capabilities = nullptr;
    207 }
    208 
    209 void XInputSource::PollEvents()
    210 {
    211   for (u32 i = 0; i < NUM_CONTROLLERS; i++)
    212   {
    213     const bool was_connected = m_controllers[i].connected;
    214     if (!was_connected)
    215       continue;
    216 
    217     XINPUT_STATE new_state;
    218     DWORD result = m_xinput_get_state(i, &new_state);
    219 
    220     if (result == ERROR_SUCCESS)
    221     {
    222       if (!was_connected)
    223         HandleControllerConnection(i);
    224 
    225       CheckForStateChanges(i, new_state);
    226     }
    227     else
    228     {
    229       if (result != ERROR_DEVICE_NOT_CONNECTED)
    230         WARNING_LOG("XInputGetState({}) failed: 0x{:08X} / 0x{:08X}", i, result, GetLastError());
    231 
    232       if (was_connected)
    233         HandleControllerDisconnection(i);
    234     }
    235   }
    236 }
    237 
    238 std::vector<std::pair<std::string, std::string>> XInputSource::EnumerateDevices()
    239 {
    240   std::vector<std::pair<std::string, std::string>> ret;
    241 
    242   for (u32 i = 0; i < NUM_CONTROLLERS; i++)
    243   {
    244     if (!m_controllers[i].connected)
    245       continue;
    246 
    247     ret.emplace_back(fmt::format("XInput-{}", i), fmt::format("XInput Controller {}", i));
    248   }
    249 
    250   return ret;
    251 }
    252 
    253 std::optional<InputBindingKey> XInputSource::ParseKeyString(std::string_view device, std::string_view binding)
    254 {
    255   if (!device.starts_with("XInput-") || binding.empty())
    256     return std::nullopt;
    257 
    258   const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
    259   if (!player_id.has_value() || player_id.value() < 0)
    260     return std::nullopt;
    261 
    262   InputBindingKey key = {};
    263   key.source_type = InputSourceType::XInput;
    264   key.source_index = static_cast<u32>(player_id.value());
    265 
    266   if (binding.ends_with("Motor"))
    267   {
    268     key.source_subtype = InputSubclass::ControllerMotor;
    269     if (binding == "LargeMotor")
    270     {
    271       key.data = 0;
    272       return key;
    273     }
    274     else if (binding == "SmallMotor")
    275     {
    276       key.data = 1;
    277       return key;
    278     }
    279     else
    280     {
    281       return std::nullopt;
    282     }
    283   }
    284   else if (binding[0] == '+' || binding[0] == '-')
    285   {
    286     // likely an axis
    287     const std::string_view axis_name(binding.substr(1));
    288     for (u32 i = 0; i < std::size(s_axis_names); i++)
    289     {
    290       if (axis_name == s_axis_names[i])
    291       {
    292         // found an axis!
    293         key.source_subtype = InputSubclass::ControllerAxis;
    294         key.data = i;
    295         key.modifier = binding[0] == '-' ? InputModifier::Negate : InputModifier::None;
    296         return key;
    297       }
    298     }
    299   }
    300   else
    301   {
    302     // must be a button
    303     for (u32 i = 0; i < std::size(s_button_names); i++)
    304     {
    305       if (binding == s_button_names[i])
    306       {
    307         key.source_subtype = InputSubclass::ControllerButton;
    308         key.data = i;
    309         return key;
    310       }
    311     }
    312   }
    313 
    314   // unknown axis/button
    315   return std::nullopt;
    316 }
    317 
    318 TinyString XInputSource::ConvertKeyToString(InputBindingKey key)
    319 {
    320   TinyString ret;
    321 
    322   if (key.source_type == InputSourceType::XInput)
    323   {
    324     if (key.source_subtype == InputSubclass::ControllerAxis && key.data < std::size(s_axis_names))
    325     {
    326       const char modifier = key.modifier == InputModifier::Negate ? '-' : '+';
    327       ret.format("XInput-{}/{}{}", static_cast<u32>(key.source_index), modifier, s_axis_names[key.data]);
    328     }
    329     else if (key.source_subtype == InputSubclass::ControllerButton && key.data < std::size(s_button_names))
    330     {
    331       ret.format("XInput-{}/{}", static_cast<u32>(key.source_index), s_button_names[key.data]);
    332     }
    333     else if (key.source_subtype == InputSubclass::ControllerMotor)
    334     {
    335       ret.format("XInput-{}/{}Motor", static_cast<u32>(key.source_index), key.data ? "Large" : "Small");
    336     }
    337   }
    338 
    339   return ret;
    340 }
    341 
    342 TinyString XInputSource::ConvertKeyToIcon(InputBindingKey key)
    343 {
    344   TinyString ret;
    345 
    346   if (key.source_type == InputSourceType::SDL)
    347   {
    348     if (key.source_subtype == InputSubclass::ControllerAxis)
    349     {
    350       if (key.data < std::size(s_axis_icons) && key.modifier != InputModifier::FullAxis)
    351       {
    352         ret.format("XInput-{}  {}", static_cast<u32>(key.source_index),
    353                    s_axis_icons[key.data][key.modifier == InputModifier::None]);
    354       }
    355     }
    356     else if (key.source_subtype == InputSubclass::ControllerButton)
    357     {
    358       if (key.data < std::size(s_button_icons))
    359         ret.format("XInput-{}  {}", static_cast<u32>(key.source_index), s_button_icons[key.data]);
    360     }
    361   }
    362 
    363   return ret;
    364 }
    365 
    366 std::vector<InputBindingKey> XInputSource::EnumerateMotors()
    367 {
    368   std::vector<InputBindingKey> ret;
    369 
    370   for (u32 i = 0; i < NUM_CONTROLLERS; i++)
    371   {
    372     const ControllerData& cd = m_controllers[i];
    373     if (!cd.connected)
    374       continue;
    375 
    376     if (cd.has_large_motor)
    377       ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 0));
    378 
    379     if (cd.has_small_motor)
    380       ret.push_back(MakeGenericControllerMotorKey(InputSourceType::XInput, i, 1));
    381   }
    382 
    383   return ret;
    384 }
    385 
    386 bool XInputSource::GetGenericBindingMapping(std::string_view device, GenericInputBindingMapping* mapping)
    387 {
    388   if (!device.starts_with("XInput-"))
    389     return false;
    390 
    391   const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
    392   if (!player_id.has_value() || player_id.value() < 0)
    393     return false;
    394 
    395   if (player_id.value() < 0 || player_id.value() >= static_cast<s32>(XUSER_MAX_COUNT))
    396     return false;
    397 
    398   // assume all buttons are present.
    399   const s32 pid = player_id.value();
    400   for (u32 i = 0; i < std::size(s_xinput_generic_binding_axis_mapping); i++)
    401   {
    402     const GenericInputBinding negative = s_xinput_generic_binding_axis_mapping[i][0];
    403     const GenericInputBinding positive = s_xinput_generic_binding_axis_mapping[i][1];
    404     if (negative != GenericInputBinding::Unknown)
    405       mapping->emplace_back(negative, fmt::format("XInput-{}/-{}", pid, s_axis_names[i]));
    406 
    407     if (positive != GenericInputBinding::Unknown)
    408       mapping->emplace_back(positive, fmt::format("XInput-{}/+{}", pid, s_axis_names[i]));
    409   }
    410   for (u32 i = 0; i < std::size(s_xinput_generic_binding_button_mapping); i++)
    411   {
    412     const GenericInputBinding binding = s_xinput_generic_binding_button_mapping[i];
    413     if (binding != GenericInputBinding::Unknown)
    414       mapping->emplace_back(binding, fmt::format("XInput-{}/{}", pid, s_button_names[i]));
    415   }
    416 
    417   if (m_controllers[pid].has_small_motor)
    418     mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("XInput-{}/SmallMotor", pid));
    419   if (m_controllers[pid].has_large_motor)
    420     mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("XInput-{}/LargeMotor", pid));
    421 
    422   return true;
    423 }
    424 
    425 void XInputSource::HandleControllerConnection(u32 index)
    426 {
    427   INFO_LOG("XInput controller {} connected.", index);
    428 
    429   XINPUT_CAPABILITIES caps = {};
    430   if (m_xinput_get_capabilities(index, 0, &caps) != ERROR_SUCCESS)
    431     WARNING_LOG("Failed to get XInput capabilities for controller {}", index);
    432 
    433   ControllerData& cd = m_controllers[index];
    434   cd.connected = true;
    435   cd.has_large_motor = caps.Vibration.wLeftMotorSpeed != 0;
    436   cd.has_small_motor = caps.Vibration.wRightMotorSpeed != 0;
    437   cd.last_state = {};
    438 
    439   InputManager::OnInputDeviceConnected(fmt::format("XInput-{}", index), fmt::format("XInput Controller {}", index));
    440 }
    441 
    442 void XInputSource::HandleControllerDisconnection(u32 index)
    443 {
    444   INFO_LOG("XInput controller {} disconnected.", index);
    445 
    446   InputManager::OnInputDeviceDisconnected({{
    447                                             .source_type = InputSourceType::XInput,
    448                                             .source_index = index,
    449                                           }},
    450                                           fmt::format("XInput-{}", index));
    451   m_controllers[index] = {};
    452 }
    453 
    454 void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state)
    455 {
    456   ControllerData& cd = m_controllers[index];
    457   if (new_state.dwPacketNumber == cd.last_state.dwPacketNumber)
    458     return;
    459 
    460   XINPUT_GAMEPAD& ogp = cd.last_state.Gamepad;
    461   const XINPUT_GAMEPAD& ngp = new_state.Gamepad;
    462 
    463 #define CHECK_AXIS(field, axis, min_value, max_value)                                                                  \
    464   if (ogp.field != ngp.field)                                                                                          \
    465   {                                                                                                                    \
    466     InputManager::InvokeEvents(MakeGenericControllerAxisKey(InputSourceType::XInput, index, axis),                     \
    467                                static_cast<float>(ngp.field) / ((ngp.field < 0) ? min_value : max_value),              \
    468                                GenericInputBinding::Unknown);                                                          \
    469   }
    470 
    471   // Y axes is inverted in XInput when compared to SDL.
    472   CHECK_AXIS(sThumbLX, AXIS_LEFTX, 32768, 32767);
    473   CHECK_AXIS(sThumbLY, AXIS_LEFTY, -32768, -32767);
    474   CHECK_AXIS(sThumbRX, AXIS_RIGHTX, 32768, 32767);
    475   CHECK_AXIS(sThumbRY, AXIS_RIGHTY, -32768, -32767);
    476   CHECK_AXIS(bLeftTrigger, AXIS_LEFTTRIGGER, 0, 255);
    477   CHECK_AXIS(bRightTrigger, AXIS_RIGHTTRIGGER, 0, 255);
    478 
    479 #undef CHECK_AXIS
    480 
    481   const u16 old_button_bits = ogp.wButtons;
    482   const u16 new_button_bits = ngp.wButtons;
    483   if (old_button_bits != new_button_bits)
    484   {
    485     for (u32 button = 0; button < NUM_BUTTONS; button++)
    486     {
    487       const u16 button_mask = s_button_masks[button];
    488       if ((old_button_bits & button_mask) != (new_button_bits & button_mask))
    489       {
    490         const GenericInputBinding generic_key = (button < std::size(s_xinput_generic_binding_button_mapping)) ?
    491                                                   s_xinput_generic_binding_button_mapping[button] :
    492                                                   GenericInputBinding::Unknown;
    493         const float value = ((new_button_bits & button_mask) != 0) ? 1.0f : 0.0f;
    494         InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::XInput, index, button), value,
    495                                    generic_key);
    496       }
    497     }
    498   }
    499 
    500   cd.last_state = new_state;
    501 }
    502 
    503 void XInputSource::UpdateMotorState(InputBindingKey key, float intensity)
    504 {
    505   if (key.source_subtype != InputSubclass::ControllerMotor || key.source_index >= NUM_CONTROLLERS)
    506     return;
    507 
    508   ControllerData& cd = m_controllers[key.source_index];
    509   if (!cd.connected)
    510     return;
    511 
    512   const u16 i_intensity = static_cast<u16>(intensity * 65535.0f);
    513   if (key.data != 0)
    514     cd.last_vibration.wRightMotorSpeed = i_intensity;
    515   else
    516     cd.last_vibration.wLeftMotorSpeed = i_intensity;
    517 
    518   m_xinput_set_state(key.source_index, &cd.last_vibration);
    519 }
    520 
    521 void XInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
    522                                     float small_intensity)
    523 {
    524   if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
    525       small_key.source_subtype != InputSubclass::ControllerMotor)
    526   {
    527     // bonkers config where they're mapped to different controllers... who would do such a thing?
    528     UpdateMotorState(large_key, large_intensity);
    529     UpdateMotorState(small_key, small_intensity);
    530     return;
    531   }
    532 
    533   ControllerData& cd = m_controllers[large_key.source_index];
    534   if (!cd.connected)
    535     return;
    536 
    537   cd.last_vibration.wLeftMotorSpeed = static_cast<u16>(large_intensity * 65535.0f);
    538   cd.last_vibration.wRightMotorSpeed = static_cast<u16>(small_intensity * 65535.0f);
    539   m_xinput_set_state(large_key.source_index, &cd.last_vibration);
    540 }
    541 
    542 std::unique_ptr<InputSource> InputSource::CreateXInputSource()
    543 {
    544   return std::make_unique<XInputSource>();
    545 }