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

sdl_input_source.cpp (36898B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "sdl_input_source.h"
      5 #include "input_manager.h"
      6 
      7 #include "core/host.h"
      8 #include "core/settings.h"
      9 
     10 #include "common/assert.h"
     11 #include "common/bitutils.h"
     12 #include "common/file_system.h"
     13 #include "common/log.h"
     14 #include "common/path.h"
     15 #include "common/string_util.h"
     16 
     17 #include "IconsPromptFont.h"
     18 
     19 #include <cmath>
     20 
     21 #ifdef __APPLE__
     22 #include <dispatch/dispatch.h>
     23 #endif
     24 
     25 Log_SetChannel(SDLInputSource);
     26 
     27 static constexpr const char* CONTROLLER_DB_FILENAME = "gamecontrollerdb.txt";
     28 
     29 static constexpr const char* s_sdl_axis_names[] = {
     30   "LeftX",        // SDL_CONTROLLER_AXIS_LEFTX
     31   "LeftY",        // SDL_CONTROLLER_AXIS_LEFTY
     32   "RightX",       // SDL_CONTROLLER_AXIS_RIGHTX
     33   "RightY",       // SDL_CONTROLLER_AXIS_RIGHTY
     34   "LeftTrigger",  // SDL_CONTROLLER_AXIS_TRIGGERLEFT
     35   "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
     36 };
     37 static constexpr const char* s_sdl_axis_icons[][2] = {
     38   {ICON_PF_LEFT_ANALOG_LEFT, ICON_PF_LEFT_ANALOG_RIGHT},   // SDL_CONTROLLER_AXIS_LEFTX
     39   {ICON_PF_LEFT_ANALOG_UP, ICON_PF_LEFT_ANALOG_DOWN},      // SDL_CONTROLLER_AXIS_LEFTY
     40   {ICON_PF_RIGHT_ANALOG_LEFT, ICON_PF_RIGHT_ANALOG_RIGHT}, // SDL_CONTROLLER_AXIS_RIGHTX
     41   {ICON_PF_RIGHT_ANALOG_UP, ICON_PF_RIGHT_ANALOG_DOWN},    // SDL_CONTROLLER_AXIS_RIGHTY
     42   {nullptr, ICON_PF_LEFT_TRIGGER_PULL},                    // SDL_CONTROLLER_AXIS_TRIGGERLEFT
     43   {nullptr, ICON_PF_RIGHT_TRIGGER_PULL},                   // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
     44 };
     45 static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
     46   {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight},   // SDL_CONTROLLER_AXIS_LEFTX
     47   {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown},      // SDL_CONTROLLER_AXIS_LEFTY
     48   {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX
     49   {GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown},    // SDL_CONTROLLER_AXIS_RIGHTY
     50   {GenericInputBinding::Unknown, GenericInputBinding::L2},                     // SDL_CONTROLLER_AXIS_TRIGGERLEFT
     51   {GenericInputBinding::Unknown, GenericInputBinding::R2},                     // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
     52 };
     53 
     54 static constexpr const char* s_sdl_button_names[] = {
     55   "A",             // SDL_CONTROLLER_BUTTON_A
     56   "B",             // SDL_CONTROLLER_BUTTON_B
     57   "X",             // SDL_CONTROLLER_BUTTON_X
     58   "Y",             // SDL_CONTROLLER_BUTTON_Y
     59   "Back",          // SDL_CONTROLLER_BUTTON_BACK
     60   "Guide",         // SDL_CONTROLLER_BUTTON_GUIDE
     61   "Start",         // SDL_CONTROLLER_BUTTON_START
     62   "LeftStick",     // SDL_CONTROLLER_BUTTON_LEFTSTICK
     63   "RightStick",    // SDL_CONTROLLER_BUTTON_RIGHTSTICK
     64   "LeftShoulder",  // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
     65   "RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
     66   "DPadUp",        // SDL_CONTROLLER_BUTTON_DPAD_UP
     67   "DPadDown",      // SDL_CONTROLLER_BUTTON_DPAD_DOWN
     68   "DPadLeft",      // SDL_CONTROLLER_BUTTON_DPAD_LEFT
     69   "DPadRight",     // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
     70   "Misc1",         // SDL_CONTROLLER_BUTTON_MISC1
     71   "Paddle1",       // SDL_CONTROLLER_BUTTON_PADDLE1
     72   "Paddle2",       // SDL_CONTROLLER_BUTTON_PADDLE2
     73   "Paddle3",       // SDL_CONTROLLER_BUTTON_PADDLE3
     74   "Paddle4",       // SDL_CONTROLLER_BUTTON_PADDLE4
     75   "Touchpad",      // SDL_CONTROLLER_BUTTON_TOUCHPAD
     76 };
     77 static constexpr const char* s_sdl_button_icons[] = {
     78   ICON_PF_BUTTON_A,           // SDL_CONTROLLER_BUTTON_A
     79   ICON_PF_BUTTON_B,           // SDL_CONTROLLER_BUTTON_B
     80   ICON_PF_BUTTON_X,           // SDL_CONTROLLER_BUTTON_X
     81   ICON_PF_BUTTON_Y,           // SDL_CONTROLLER_BUTTON_Y
     82   ICON_PF_SHARE_CAPTURE,      // SDL_CONTROLLER_BUTTON_BACK
     83   ICON_PF_XBOX,               // SDL_CONTROLLER_BUTTON_GUIDE
     84   ICON_PF_BURGER_MENU,        // SDL_CONTROLLER_BUTTON_START
     85   ICON_PF_LEFT_ANALOG_CLICK,  // SDL_CONTROLLER_BUTTON_LEFTSTICK
     86   ICON_PF_RIGHT_ANALOG_CLICK, // SDL_CONTROLLER_BUTTON_RIGHTSTICK
     87   ICON_PF_LEFT_SHOULDER_LB,   // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
     88   ICON_PF_RIGHT_SHOULDER_RB,  // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
     89   ICON_PF_XBOX_DPAD_UP,       // SDL_CONTROLLER_BUTTON_DPAD_UP
     90   ICON_PF_XBOX_DPAD_DOWN,     // SDL_CONTROLLER_BUTTON_DPAD_DOWN
     91   ICON_PF_XBOX_DPAD_LEFT,     // SDL_CONTROLLER_BUTTON_DPAD_LEFT
     92   ICON_PF_XBOX_DPAD_RIGHT,    // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
     93 };
     94 static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
     95   GenericInputBinding::Cross,     // SDL_CONTROLLER_BUTTON_A
     96   GenericInputBinding::Circle,    // SDL_CONTROLLER_BUTTON_B
     97   GenericInputBinding::Square,    // SDL_CONTROLLER_BUTTON_X
     98   GenericInputBinding::Triangle,  // SDL_CONTROLLER_BUTTON_Y
     99   GenericInputBinding::Select,    // SDL_CONTROLLER_BUTTON_BACK
    100   GenericInputBinding::System,    // SDL_CONTROLLER_BUTTON_GUIDE
    101   GenericInputBinding::Start,     // SDL_CONTROLLER_BUTTON_START
    102   GenericInputBinding::L3,        // SDL_CONTROLLER_BUTTON_LEFTSTICK
    103   GenericInputBinding::R3,        // SDL_CONTROLLER_BUTTON_RIGHTSTICK
    104   GenericInputBinding::L1,        // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
    105   GenericInputBinding::R1,        // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
    106   GenericInputBinding::DPadUp,    // SDL_CONTROLLER_BUTTON_DPAD_UP
    107   GenericInputBinding::DPadDown,  // SDL_CONTROLLER_BUTTON_DPAD_DOWN
    108   GenericInputBinding::DPadLeft,  // SDL_CONTROLLER_BUTTON_DPAD_LEFT
    109   GenericInputBinding::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
    110   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_MISC1
    111   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE1
    112   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE2
    113   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE3
    114   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_PADDLE4
    115   GenericInputBinding::Unknown,   // SDL_CONTROLLER_BUTTON_TOUCHPAD
    116 };
    117 
    118 static constexpr const char* s_sdl_hat_direction_names[] = {
    119   // clang-format off
    120 	"North",
    121 	"East",
    122 	"South",
    123 	"West",
    124   // clang-format on
    125 };
    126 
    127 static constexpr const char* s_sdl_default_led_colors[] = {
    128   "0000ff", // SDL-0
    129   "ff0000", // SDL-1
    130   "00ff00", // SDL-2
    131   "ffff00", // SDL-3
    132 };
    133 
    134 static void SetControllerRGBLED(SDL_GameController* gc, u32 color)
    135 {
    136   SDL_GameControllerSetLED(gc, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff);
    137 }
    138 
    139 static void SDLLogCallback(void* userdata, int category, SDL_LogPriority priority, const char* message)
    140 {
    141   static constexpr LOGLEVEL priority_map[SDL_NUM_LOG_PRIORITIES] = {
    142     LOGLEVEL_DEBUG,
    143     LOGLEVEL_DEBUG,   // SDL_LOG_PRIORITY_VERBOSE
    144     LOGLEVEL_DEBUG,   // SDL_LOG_PRIORITY_DEBUG
    145     LOGLEVEL_INFO,    // SDL_LOG_PRIORITY_INFO
    146     LOGLEVEL_WARNING, // SDL_LOG_PRIORITY_WARN
    147     LOGLEVEL_ERROR,   // SDL_LOG_PRIORITY_ERROR
    148     LOGLEVEL_ERROR,   // SDL_LOG_PRIORITY_CRITICAL
    149   };
    150 
    151   Log::Write("SDL", "SDL", priority_map[priority], message);
    152 }
    153 
    154 bool SDLInputSource::ALLOW_EVENT_POLLING = true;
    155 
    156 SDLInputSource::SDLInputSource() = default;
    157 
    158 SDLInputSource::~SDLInputSource()
    159 {
    160   Assert(m_controllers.empty());
    161 }
    162 
    163 bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
    164 {
    165   LoadSettings(si);
    166   settings_lock.unlock();
    167   SetHints();
    168   bool result = InitializeSubsystem();
    169   settings_lock.lock();
    170   return result;
    171 }
    172 
    173 void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
    174 {
    175   const bool old_controller_enhanced_mode = m_controller_enhanced_mode;
    176   const bool old_controller_ps5_player_led = m_controller_ps5_player_led;
    177 
    178 #ifdef __APPLE__
    179   const bool old_enable_iokit_driver = m_enable_iokit_driver;
    180   const bool old_enable_mfi_driver = m_enable_mfi_driver;
    181 #endif
    182 
    183   LoadSettings(si);
    184 
    185 #ifdef __APPLE__
    186   const bool drivers_changed =
    187     (m_enable_iokit_driver != old_enable_iokit_driver || m_enable_mfi_driver != old_enable_mfi_driver);
    188 #else
    189   constexpr bool drivers_changed = false;
    190 #endif
    191 
    192   if (m_controller_enhanced_mode != old_controller_enhanced_mode ||
    193       m_controller_ps5_player_led != old_controller_ps5_player_led ||
    194       drivers_changed)
    195   {
    196     settings_lock.unlock();
    197     ShutdownSubsystem();
    198     SetHints();
    199     InitializeSubsystem();
    200     settings_lock.lock();
    201   }
    202 }
    203 
    204 bool SDLInputSource::ReloadDevices()
    205 {
    206   // We'll get a GC added/removed event here.
    207   PollEvents();
    208   return false;
    209 }
    210 
    211 void SDLInputSource::Shutdown()
    212 {
    213   ShutdownSubsystem();
    214 }
    215 
    216 void SDLInputSource::LoadSettings(SettingsInterface& si)
    217 {
    218   for (u32 i = 0; i < MAX_LED_COLORS; i++)
    219   {
    220     const u32 color = GetRGBForPlayerId(si, i);
    221     if (m_led_colors[i] == color)
    222       continue;
    223 
    224     m_led_colors[i] = color;
    225 
    226     const auto it = GetControllerDataForPlayerId(i);
    227     if (it == m_controllers.end() || !it->game_controller || !SDL_GameControllerHasLED(it->game_controller))
    228       continue;
    229 
    230     SetControllerRGBLED(it->game_controller, color);
    231   }
    232 
    233   m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
    234   m_controller_ps5_player_led = si.GetBoolValue("InputSources", "SDLPS5PlayerLED", false);
    235   m_sdl_hints = si.GetKeyValueList("SDLHints");
    236 
    237 #ifdef __APPLE__
    238   m_enable_iokit_driver = si.GetBoolValue("InputSources", "SDLIOKitDriver", true);
    239   m_enable_mfi_driver = si.GetBoolValue("InputSources", "SDLMFIDriver", true);
    240 #endif
    241 }
    242 
    243 u32 SDLInputSource::GetRGBForPlayerId(SettingsInterface& si, u32 player_id)
    244 {
    245   return ParseRGBForPlayerId(
    246     si.GetStringValue("SDLExtra", fmt::format("Player{}LED", player_id).c_str(), s_sdl_default_led_colors[player_id]),
    247     player_id);
    248 }
    249 
    250 u32 SDLInputSource::ParseRGBForPlayerId(std::string_view str, u32 player_id)
    251 {
    252   if (player_id >= MAX_LED_COLORS)
    253     return 0;
    254 
    255   const u32 default_color = StringUtil::FromChars<u32>(s_sdl_default_led_colors[player_id], 16).value_or(0);
    256   const u32 color = StringUtil::FromChars<u32>(str, 16).value_or(default_color);
    257 
    258   return color;
    259 }
    260 
    261 void SDLInputSource::SetHints()
    262 {
    263   if (const std::string upath = Path::Combine(EmuFolders::DataRoot, CONTROLLER_DB_FILENAME);
    264       FileSystem::FileExists(upath.c_str()))
    265   {
    266     INFO_LOG("Using Controller DB from user directory: '{}'", upath);
    267     SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG_FILE, upath.c_str());
    268   }
    269   else if (const std::string rpath = EmuFolders::GetOverridableResourcePath(CONTROLLER_DB_FILENAME);
    270            FileSystem::FileExists(rpath.c_str()))
    271   {
    272     INFO_LOG("Using Controller DB from resources.");
    273     SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG_FILE, rpath.c_str());
    274   }
    275   else
    276   {
    277     ERROR_LOG("Controller DB not found, it should be named '{}'", CONTROLLER_DB_FILENAME);
    278   }
    279 
    280   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
    281   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0");
    282   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, m_controller_ps5_player_led ? "1" : "0");
    283   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1");
    284   SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS3, "1");
    285 
    286 #ifdef __APPLE__
    287   INFO_LOG("IOKit is {}, MFI is {}.", m_enable_iokit_driver ? "enabled" : "disabled",
    288            m_enable_mfi_driver ? "enabled" : "disabled");
    289   SDL_SetHint(SDL_HINT_JOYSTICK_IOKIT, m_enable_iokit_driver ? "1" : "0");
    290   SDL_SetHint(SDL_HINT_JOYSTICK_MFI, m_enable_mfi_driver ? "1" : "0");
    291 #endif
    292 
    293   for (const std::pair<std::string, std::string>& hint : m_sdl_hints)
    294     SDL_SetHint(hint.first.c_str(), hint.second.c_str());
    295 }
    296 
    297 bool SDLInputSource::InitializeSubsystem()
    298 {
    299   if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0)
    300   {
    301     ERROR_LOG("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed");
    302     return false;
    303   }
    304 
    305   SDL_LogSetOutputFunction(SDLLogCallback, nullptr);
    306 #ifdef _DEBUG
    307   SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
    308 #else
    309   SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO);
    310 #endif
    311 
    312   // we should open the controllers as the connected events come in, so no need to do any more here
    313   m_sdl_subsystem_initialized = true;
    314   INFO_LOG("{} controller mappings are loaded.", SDL_GameControllerNumMappings());
    315   return true;
    316 }
    317 
    318 void SDLInputSource::ShutdownSubsystem()
    319 {
    320   while (!m_controllers.empty())
    321     CloseDevice(m_controllers.begin()->joystick_id);
    322 
    323   if (m_sdl_subsystem_initialized)
    324   {
    325     SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC);
    326     m_sdl_subsystem_initialized = false;
    327   }
    328 }
    329 
    330 void SDLInputSource::PollEvents()
    331 {
    332   if (!ALLOW_EVENT_POLLING)
    333     return;
    334 
    335   for (;;)
    336   {
    337     SDL_Event ev;
    338     if (SDL_PollEvent(&ev))
    339       ProcessSDLEvent(&ev);
    340     else
    341       break;
    342   }
    343 }
    344 
    345 std::vector<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevices()
    346 {
    347   std::vector<std::pair<std::string, std::string>> ret;
    348 
    349   for (const ControllerData& cd : m_controllers)
    350   {
    351     std::string id = fmt::format("SDL-{}", cd.player_id);
    352 
    353     const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick);
    354     if (name)
    355       ret.emplace_back(std::move(id), name);
    356     else
    357       ret.emplace_back(std::move(id), "Unknown Device");
    358   }
    359 
    360   return ret;
    361 }
    362 
    363 std::optional<InputBindingKey> SDLInputSource::ParseKeyString(std::string_view device, std::string_view binding)
    364 {
    365   if (!device.starts_with("SDL-") || binding.empty())
    366     return std::nullopt;
    367 
    368   const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
    369   if (!player_id.has_value() || player_id.value() < 0)
    370     return std::nullopt;
    371 
    372   InputBindingKey key = {};
    373   key.source_type = InputSourceType::SDL;
    374   key.source_index = static_cast<u32>(player_id.value());
    375 
    376   if (binding.ends_with("Motor"))
    377   {
    378     key.source_subtype = InputSubclass::ControllerMotor;
    379     if (binding == "LargeMotor")
    380     {
    381       key.data = 0;
    382       return key;
    383     }
    384     else if (binding == "SmallMotor")
    385     {
    386       key.data = 1;
    387       return key;
    388     }
    389     else
    390     {
    391       return std::nullopt;
    392     }
    393   }
    394   else if (binding.ends_with("Haptic"))
    395   {
    396     key.source_subtype = InputSubclass::ControllerHaptic;
    397     key.data = 0;
    398     return key;
    399   }
    400   else if (binding[0] == '+' || binding[0] == '-')
    401   {
    402     // likely an axis
    403     const std::string_view axis_name(binding.substr(1));
    404 
    405     if (axis_name.starts_with("Axis"))
    406     {
    407       std::string_view end;
    408       if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
    409       {
    410         key.source_subtype = InputSubclass::ControllerAxis;
    411         key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
    412         key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
    413         key.invert = (end == "~");
    414         return key;
    415       }
    416     }
    417     for (u32 i = 0; i < std::size(s_sdl_axis_names); i++)
    418     {
    419       if (axis_name == s_sdl_axis_names[i])
    420       {
    421         // found an axis!
    422         key.source_subtype = InputSubclass::ControllerAxis;
    423         key.data = i;
    424         key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
    425         return key;
    426       }
    427     }
    428   }
    429   else if (binding.starts_with("FullAxis"))
    430   {
    431     std::string_view end;
    432     if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end))
    433     {
    434       key.source_subtype = InputSubclass::ControllerAxis;
    435       key.data = *value + static_cast<u32>(std::size(s_sdl_axis_names));
    436       key.modifier = InputModifier::FullAxis;
    437       key.invert = (end == "~");
    438       return key;
    439     }
    440   }
    441   else if (binding.starts_with("Hat"))
    442   {
    443     std::string_view hat_dir;
    444     if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty())
    445     {
    446       for (u8 dir = 0; dir < static_cast<u8>(std::size(s_sdl_hat_direction_names)); dir++)
    447       {
    448         if (hat_dir == s_sdl_hat_direction_names[dir])
    449         {
    450           key.source_subtype = InputSubclass::ControllerHat;
    451           key.data = value.value() * static_cast<u32>(std::size(s_sdl_hat_direction_names)) + dir;
    452           return key;
    453         }
    454       }
    455     }
    456   }
    457   else
    458   {
    459     // must be a button
    460     if (binding.starts_with("Button"))
    461     {
    462       if (auto value = StringUtil::FromChars<u32>(binding.substr(6)))
    463       {
    464         key.source_subtype = InputSubclass::ControllerButton;
    465         key.data = *value + static_cast<u32>(std::size(s_sdl_button_names));
    466         return key;
    467       }
    468     }
    469     for (u32 i = 0; i < std::size(s_sdl_button_names); i++)
    470     {
    471       if (binding == s_sdl_button_names[i])
    472       {
    473         key.source_subtype = InputSubclass::ControllerButton;
    474         key.data = i;
    475         return key;
    476       }
    477     }
    478   }
    479 
    480   // unknown axis/button
    481   return std::nullopt;
    482 }
    483 
    484 TinyString SDLInputSource::ConvertKeyToString(InputBindingKey key)
    485 {
    486   TinyString ret;
    487 
    488   if (key.source_type == InputSourceType::SDL)
    489   {
    490     if (key.source_subtype == InputSubclass::ControllerAxis)
    491     {
    492       const char* modifier =
    493         (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
    494       if (key.data < std::size(s_sdl_axis_names))
    495       {
    496         ret.format("SDL-{}/{}{}", static_cast<u32>(key.source_index), modifier, s_sdl_axis_names[key.data]);
    497       }
    498       else
    499       {
    500         ret.format("SDL-{}/{}Axis{}{}", static_cast<u32>(key.source_index), modifier,
    501                    key.data - static_cast<u32>(std::size(s_sdl_axis_names)), key.invert ? "~" : "");
    502       }
    503     }
    504     else if (key.source_subtype == InputSubclass::ControllerButton)
    505     {
    506       if (key.data < std::size(s_sdl_button_names))
    507       {
    508         ret.format("SDL-{}/{}", static_cast<u32>(key.source_index), s_sdl_button_names[key.data]);
    509       }
    510       else
    511       {
    512         ret.format("SDL-{}/Button{}", static_cast<u32>(key.source_index),
    513                    key.data - static_cast<u32>(std::size(s_sdl_button_names)));
    514       }
    515     }
    516     else if (key.source_subtype == InputSubclass::ControllerHat)
    517     {
    518       const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names));
    519       const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names));
    520       ret.format("SDL-{}/Hat{}{}", static_cast<u32>(key.source_index), hat_index,
    521                  s_sdl_hat_direction_names[hat_direction]);
    522     }
    523     else if (key.source_subtype == InputSubclass::ControllerMotor)
    524     {
    525       ret.format("SDL-{}/{}Motor", static_cast<u32>(key.source_index), key.data ? "Large" : "Small");
    526     }
    527     else if (key.source_subtype == InputSubclass::ControllerHaptic)
    528     {
    529       ret.format("SDL-{}/Haptic", static_cast<u32>(key.source_index));
    530     }
    531   }
    532 
    533   return ret;
    534 }
    535 
    536 TinyString SDLInputSource::ConvertKeyToIcon(InputBindingKey key)
    537 {
    538   TinyString ret;
    539 
    540   if (key.source_type == InputSourceType::SDL)
    541   {
    542     if (key.source_subtype == InputSubclass::ControllerAxis)
    543     {
    544       if (key.data < std::size(s_sdl_axis_icons) && key.modifier != InputModifier::FullAxis)
    545       {
    546         ret.format("SDL-{}  {}", static_cast<u32>(key.source_index),
    547                    s_sdl_axis_icons[key.data][key.modifier == InputModifier::None]);
    548       }
    549     }
    550     else if (key.source_subtype == InputSubclass::ControllerButton)
    551     {
    552       if (key.data < std::size(s_sdl_button_icons))
    553         ret.format("SDL-{}  {}", static_cast<u32>(key.source_index), s_sdl_button_icons[key.data]);
    554     }
    555   }
    556 
    557   return ret;
    558 }
    559 
    560 bool SDLInputSource::IsHandledInputEvent(const SDL_Event* ev)
    561 {
    562   switch (ev->type)
    563   {
    564     case SDL_CONTROLLERDEVICEADDED:
    565     case SDL_CONTROLLERDEVICEREMOVED:
    566     case SDL_JOYDEVICEADDED:
    567     case SDL_JOYDEVICEREMOVED:
    568     case SDL_CONTROLLERAXISMOTION:
    569     case SDL_CONTROLLERBUTTONDOWN:
    570     case SDL_CONTROLLERBUTTONUP:
    571     case SDL_JOYAXISMOTION:
    572     case SDL_JOYBUTTONDOWN:
    573     case SDL_JOYBUTTONUP:
    574     case SDL_JOYHATMOTION:
    575       return true;
    576 
    577     default:
    578       return false;
    579   }
    580 }
    581 
    582 bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
    583 {
    584   switch (event->type)
    585   {
    586     case SDL_CONTROLLERDEVICEADDED:
    587     {
    588       INFO_LOG("Controller {} inserted", event->cdevice.which);
    589       OpenDevice(event->cdevice.which, true);
    590       return true;
    591     }
    592 
    593     case SDL_CONTROLLERDEVICEREMOVED:
    594     {
    595       INFO_LOG("Controller {} removed", event->cdevice.which);
    596       CloseDevice(event->cdevice.which);
    597       return true;
    598     }
    599 
    600     case SDL_JOYDEVICEADDED:
    601     {
    602       // Let game controller handle.. well.. game controllers.
    603       if (SDL_IsGameController(event->jdevice.which))
    604         return false;
    605 
    606       INFO_LOG("Joystick {} inserted", event->jdevice.which);
    607       OpenDevice(event->cdevice.which, false);
    608       return true;
    609     }
    610     break;
    611 
    612     case SDL_JOYDEVICEREMOVED:
    613     {
    614       if (auto it = GetControllerDataForJoystickId(event->cdevice.which);
    615           it != m_controllers.end() && it->game_controller)
    616         return false;
    617 
    618       INFO_LOG("Joystick {} removed", event->jdevice.which);
    619       CloseDevice(event->cdevice.which);
    620       return true;
    621     }
    622 
    623     case SDL_CONTROLLERAXISMOTION:
    624       return HandleControllerAxisEvent(&event->caxis);
    625 
    626     case SDL_CONTROLLERBUTTONDOWN:
    627     case SDL_CONTROLLERBUTTONUP:
    628       return HandleControllerButtonEvent(&event->cbutton);
    629 
    630     case SDL_JOYAXISMOTION:
    631       return HandleJoystickAxisEvent(&event->jaxis);
    632 
    633     case SDL_JOYBUTTONDOWN:
    634     case SDL_JOYBUTTONUP:
    635       return HandleJoystickButtonEvent(&event->jbutton);
    636 
    637     case SDL_JOYHATMOTION:
    638       return HandleJoystickHatEvent(&event->jhat);
    639 
    640     default:
    641       return false;
    642   }
    643 }
    644 
    645 SDL_Joystick* SDLInputSource::GetJoystickForDevice(std::string_view device)
    646 {
    647   if (!device.starts_with("SDL-"))
    648     return nullptr;
    649 
    650   const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
    651   if (!player_id.has_value() || player_id.value() < 0)
    652     return nullptr;
    653 
    654   auto it = GetControllerDataForPlayerId(player_id.value());
    655   if (it == m_controllers.end())
    656     return nullptr;
    657 
    658   return it->joystick;
    659 }
    660 
    661 SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id)
    662 {
    663   return std::find_if(m_controllers.begin(), m_controllers.end(),
    664                       [id](const ControllerData& cd) { return cd.joystick_id == id; });
    665 }
    666 
    667 SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id)
    668 {
    669   return std::find_if(m_controllers.begin(), m_controllers.end(),
    670                       [id](const ControllerData& cd) { return cd.player_id == id; });
    671 }
    672 
    673 int SDLInputSource::GetFreePlayerId() const
    674 {
    675   for (int player_id = 0;; player_id++)
    676   {
    677     size_t i;
    678     for (i = 0; i < m_controllers.size(); i++)
    679     {
    680       if (m_controllers[i].player_id == player_id)
    681         break;
    682     }
    683     if (i == m_controllers.size())
    684       return player_id;
    685   }
    686 
    687   return 0;
    688 }
    689 
    690 bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller)
    691 {
    692   SDL_GameController* gcontroller;
    693   SDL_Joystick* joystick;
    694 
    695   if (is_gamecontroller)
    696   {
    697     gcontroller = SDL_GameControllerOpen(index);
    698     joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr;
    699   }
    700   else
    701   {
    702     gcontroller = nullptr;
    703     joystick = SDL_JoystickOpen(index);
    704   }
    705 
    706   if (!gcontroller && !joystick)
    707   {
    708     ERROR_LOG("Failed to open controller {}", index);
    709     if (gcontroller)
    710       SDL_GameControllerClose(gcontroller);
    711 
    712     return false;
    713   }
    714 
    715   const int joystick_id = SDL_JoystickInstanceID(joystick);
    716   int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick);
    717   if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
    718   {
    719     const int free_player_id = GetFreePlayerId();
    720     WARNING_LOG("Controller {} (joystick {}) returned player ID {}, which is invalid or in use. Using ID {} instead.",
    721                 index, joystick_id, player_id, free_player_id);
    722     player_id = free_player_id;
    723   }
    724 
    725   const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick);
    726   if (!name)
    727     name = "Unknown Device";
    728 
    729   VERBOSE_LOG("Opened {} {} (instance id {}, player id {}): {}", is_gamecontroller ? "game controller" : "joystick",
    730               index, joystick_id, player_id, name);
    731 
    732   ControllerData cd = {};
    733   cd.player_id = player_id;
    734   cd.joystick_id = joystick_id;
    735   cd.haptic_left_right_effect = -1;
    736   cd.game_controller = gcontroller;
    737   cd.joystick = joystick;
    738 
    739   if (gcontroller)
    740   {
    741     const int num_axes = SDL_JoystickNumAxes(joystick);
    742     const int num_buttons = SDL_JoystickNumButtons(joystick);
    743     cd.joy_axis_used_in_gc.resize(num_axes, false);
    744     cd.joy_button_used_in_gc.resize(num_buttons, false);
    745     auto mark_bind = [&](SDL_GameControllerButtonBind bind) {
    746       if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes)
    747         cd.joy_axis_used_in_gc[bind.value.axis] = true;
    748       if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons)
    749         cd.joy_button_used_in_gc[bind.value.button] = true;
    750     };
    751     for (size_t i = 0; i < std::size(s_sdl_axis_names); i++)
    752       mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast<SDL_GameControllerAxis>(i)));
    753     for (size_t i = 0; i < std::size(s_sdl_button_names); i++)
    754       mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast<SDL_GameControllerButton>(i)));
    755 
    756     VERBOSE_LOG("Controller {} has {} axes and {} buttons", player_id, num_axes, num_buttons);
    757   }
    758   else
    759   {
    760     // GC doesn't have the concept of hats, so we only need to do this for joysticks.
    761     const int num_hats = SDL_JoystickNumHats(joystick);
    762     if (num_hats > 0)
    763       cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8(0));
    764 
    765     VERBOSE_LOG("Joystick {} has {} axes, {} buttons and {} hats", player_id, SDL_JoystickNumAxes(joystick),
    766                 SDL_JoystickNumButtons(joystick), num_hats);
    767   }
    768 
    769   cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0);
    770   if (cd.use_game_controller_rumble)
    771   {
    772     VERBOSE_LOG("Rumble is supported on '{}' via gamecontroller", name);
    773   }
    774   else
    775   {
    776     SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick);
    777     if (haptic)
    778     {
    779       SDL_HapticEffect ef = {};
    780       ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
    781       ef.leftright.length = 1000;
    782 
    783       int ef_id = SDL_HapticNewEffect(haptic, &ef);
    784       if (ef_id >= 0)
    785       {
    786         cd.haptic = haptic;
    787         cd.haptic_left_right_effect = ef_id;
    788       }
    789       else
    790       {
    791         ERROR_LOG("Failed to create haptic left/right effect: {}", SDL_GetError());
    792         if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0)
    793         {
    794           cd.haptic = haptic;
    795         }
    796         else
    797         {
    798           ERROR_LOG("No haptic rumble supported: {}", SDL_GetError());
    799           SDL_HapticClose(haptic);
    800         }
    801       }
    802     }
    803 
    804     if (cd.haptic)
    805       VERBOSE_LOG("Rumble is supported on '{}' via haptic", name);
    806   }
    807 
    808   if (!cd.haptic && !cd.use_game_controller_rumble)
    809     VERBOSE_LOG("Rumble is not supported on '{}'", name);
    810 
    811   if (player_id >= 0 && static_cast<u32>(player_id) < MAX_LED_COLORS && gcontroller &&
    812       SDL_GameControllerHasLED(gcontroller))
    813   {
    814     SetControllerRGBLED(gcontroller, m_led_colors[player_id]);
    815   }
    816 
    817   m_controllers.push_back(std::move(cd));
    818 
    819   InputManager::OnInputDeviceConnected(fmt::format("SDL-{}", player_id), name);
    820   return true;
    821 }
    822 
    823 bool SDLInputSource::CloseDevice(int joystick_index)
    824 {
    825   auto it = GetControllerDataForJoystickId(joystick_index);
    826   if (it == m_controllers.end())
    827     return false;
    828 
    829   InputManager::OnInputDeviceDisconnected(
    830     InputBindingKey{{.source_type = InputSourceType::SDL, .source_index = static_cast<u32>(it->player_id)}},
    831     fmt::format("SDL-{}", it->player_id));
    832 
    833   if (it->haptic)
    834     SDL_HapticClose(it->haptic);
    835 
    836   if (it->game_controller)
    837     SDL_GameControllerClose(it->game_controller);
    838   else
    839     SDL_JoystickClose(it->joystick);
    840 
    841   m_controllers.erase(it);
    842   return true;
    843 }
    844 
    845 static float NormalizeS16(s16 value)
    846 {
    847   return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
    848 }
    849 
    850 bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev)
    851 {
    852   auto it = GetControllerDataForJoystickId(ev->which);
    853   if (it == m_controllers.end())
    854     return false;
    855 
    856   const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis));
    857   InputManager::InvokeEvents(key, NormalizeS16(ev->value));
    858   return true;
    859 }
    860 
    861 bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev)
    862 {
    863   auto it = GetControllerDataForJoystickId(ev->which);
    864   if (it == m_controllers.end())
    865     return false;
    866 
    867   const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button));
    868   const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ?
    869                                             s_sdl_generic_binding_button_mapping[ev->button] :
    870                                             GenericInputBinding::Unknown;
    871   InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key);
    872   return true;
    873 }
    874 
    875 bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev)
    876 {
    877   auto it = GetControllerDataForJoystickId(ev->which);
    878   if (it == m_controllers.end())
    879     return false;
    880   if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis])
    881     return false;                                                            // Will get handled by GC event
    882   const u32 axis = ev->axis + static_cast<u32>(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes
    883   const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis));
    884   InputManager::InvokeEvents(key, NormalizeS16(ev->value));
    885   return true;
    886 }
    887 
    888 bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev)
    889 {
    890   auto it = GetControllerDataForJoystickId(ev->which);
    891   if (it == m_controllers.end())
    892     return false;
    893   if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button])
    894     return false; // Will get handled by GC event
    895   const u32 button =
    896     ev->button + static_cast<u32>(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons
    897   const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button));
    898   InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f);
    899   return true;
    900 }
    901 
    902 bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev)
    903 {
    904   auto it = GetControllerDataForJoystickId(ev->which);
    905   if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size())
    906     return false;
    907 
    908   const unsigned long last_direction = it->last_hat_state[ev->hat];
    909   it->last_hat_state[ev->hat] = ev->value;
    910 
    911   unsigned long changed_direction = last_direction ^ ev->value;
    912   while (changed_direction != 0)
    913   {
    914     const u32 pos = CountTrailingZeros(changed_direction);
    915 
    916     const unsigned long mask = (1u << pos);
    917     changed_direction &= ~mask;
    918 
    919     const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat,
    920                                                           static_cast<u8>(pos),
    921                                                           static_cast<u32>(std::size(s_sdl_hat_direction_names))));
    922     InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f);
    923   }
    924 
    925   return true;
    926 }
    927 
    928 std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
    929 {
    930   std::vector<InputBindingKey> ret;
    931 
    932   InputBindingKey key = {};
    933   key.source_type = InputSourceType::SDL;
    934 
    935   for (ControllerData& cd : m_controllers)
    936   {
    937     key.source_index = cd.player_id;
    938 
    939     if (cd.use_game_controller_rumble || cd.haptic_left_right_effect)
    940     {
    941       // two motors
    942       key.source_subtype = InputSubclass::ControllerMotor;
    943       key.data = 0;
    944       ret.push_back(key);
    945       key.data = 1;
    946       ret.push_back(key);
    947     }
    948     else if (cd.haptic)
    949     {
    950       // haptic effect
    951       key.source_subtype = InputSubclass::ControllerHaptic;
    952       key.data = 0;
    953       ret.push_back(key);
    954     }
    955   }
    956 
    957   return ret;
    958 }
    959 
    960 bool SDLInputSource::GetGenericBindingMapping(std::string_view device, GenericInputBindingMapping* mapping)
    961 {
    962   if (!device.starts_with("SDL-"))
    963     return false;
    964 
    965   const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(4));
    966   if (!player_id.has_value() || player_id.value() < 0)
    967     return false;
    968 
    969   ControllerDataVector::iterator it = GetControllerDataForPlayerId(player_id.value());
    970   if (it == m_controllers.end())
    971     return false;
    972 
    973   if (it->game_controller)
    974   {
    975     // assume all buttons are present.
    976     const s32 pid = player_id.value();
    977     for (u32 i = 0; i < std::size(s_sdl_generic_binding_axis_mapping); i++)
    978     {
    979       const GenericInputBinding negative = s_sdl_generic_binding_axis_mapping[i][0];
    980       const GenericInputBinding positive = s_sdl_generic_binding_axis_mapping[i][1];
    981       if (negative != GenericInputBinding::Unknown)
    982         mapping->emplace_back(negative, fmt::format("SDL-{}/-{}", pid, s_sdl_axis_names[i]));
    983 
    984       if (positive != GenericInputBinding::Unknown)
    985         mapping->emplace_back(positive, fmt::format("SDL-{}/+{}", pid, s_sdl_axis_names[i]));
    986     }
    987     for (u32 i = 0; i < std::size(s_sdl_generic_binding_button_mapping); i++)
    988     {
    989       const GenericInputBinding binding = s_sdl_generic_binding_button_mapping[i];
    990       if (binding != GenericInputBinding::Unknown)
    991         mapping->emplace_back(binding, fmt::format("SDL-{}/{}", pid, s_sdl_button_names[i]));
    992     }
    993 
    994     if (it->use_game_controller_rumble || it->haptic_left_right_effect)
    995     {
    996       mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("SDL-{}/SmallMotor", pid));
    997       mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("SDL-{}/LargeMotor", pid));
    998     }
    999     else
   1000     {
   1001       mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("SDL-{}/Haptic", pid));
   1002       mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("SDL-{}/Haptic", pid));
   1003     }
   1004 
   1005     return true;
   1006   }
   1007   else
   1008   {
   1009     // joysticks have arbitrary axis numbers, so automapping isn't going to work here.
   1010     return false;
   1011   }
   1012 }
   1013 
   1014 void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity)
   1015 {
   1016   if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic)
   1017     return;
   1018 
   1019   auto it = GetControllerDataForPlayerId(key.source_index);
   1020   if (it == m_controllers.end())
   1021     return;
   1022 
   1023   it->rumble_intensity[key.data] = static_cast<u16>(intensity * 65535.0f);
   1024   SendRumbleUpdate(&(*it));
   1025 }
   1026 
   1027 void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
   1028                                       float small_intensity)
   1029 {
   1030   if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor ||
   1031       small_key.source_subtype != InputSubclass::ControllerMotor)
   1032   {
   1033     // bonkers config where they're mapped to different controllers... who would do such a thing?
   1034     UpdateMotorState(large_key, large_intensity);
   1035     UpdateMotorState(small_key, small_intensity);
   1036     return;
   1037   }
   1038 
   1039   auto it = GetControllerDataForPlayerId(large_key.source_index);
   1040   if (it == m_controllers.end())
   1041     return;
   1042 
   1043   it->rumble_intensity[large_key.data] = static_cast<u16>(large_intensity * 65535.0f);
   1044   it->rumble_intensity[small_key.data] = static_cast<u16>(small_intensity * 65535.0f);
   1045   SendRumbleUpdate(&(*it));
   1046 }
   1047 
   1048 void SDLInputSource::SendRumbleUpdate(ControllerData* cd)
   1049 {
   1050   // we'll update before this duration is elapsed
   1051   static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS
   1052 
   1053   if (cd->use_game_controller_rumble)
   1054   {
   1055     SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
   1056     return;
   1057   }
   1058 
   1059   if (cd->haptic_left_right_effect >= 0)
   1060   {
   1061     if ((static_cast<u32>(cd->rumble_intensity[0]) + static_cast<u32>(cd->rumble_intensity[1])) > 0)
   1062     {
   1063       SDL_HapticEffect ef;
   1064       ef.type = SDL_HAPTIC_LEFTRIGHT;
   1065       ef.leftright.large_magnitude = cd->rumble_intensity[0];
   1066       ef.leftright.small_magnitude = cd->rumble_intensity[1];
   1067       ef.leftright.length = DURATION;
   1068       SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
   1069       SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
   1070     }
   1071     else
   1072     {
   1073       SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect);
   1074     }
   1075   }
   1076   else
   1077   {
   1078     const float strength =
   1079       static_cast<float>(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
   1080     if (strength > 0.0f)
   1081       SDL_HapticRumblePlay(cd->haptic, strength, DURATION);
   1082     else
   1083       SDL_HapticRumbleStop(cd->haptic);
   1084   }
   1085 }
   1086 
   1087 std::unique_ptr<InputSource> InputSource::CreateSDLSource()
   1088 {
   1089   return std::make_unique<SDLInputSource>();
   1090 }