// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #include "sdl_input_source.h" #include "input_manager.h" #include "core/host.h" #include "core/settings.h" #include "common/assert.h" #include "common/bitutils.h" #include "common/file_system.h" #include "common/log.h" #include "common/path.h" #include "common/string_util.h" #include "IconsPromptFont.h" #include #ifdef __APPLE__ #include #endif Log_SetChannel(SDLInputSource); static constexpr const char* CONTROLLER_DB_FILENAME = "gamecontrollerdb.txt"; static constexpr const char* s_sdl_axis_names[] = { "LeftX", // SDL_CONTROLLER_AXIS_LEFTX "LeftY", // SDL_CONTROLLER_AXIS_LEFTY "RightX", // SDL_CONTROLLER_AXIS_RIGHTX "RightY", // SDL_CONTROLLER_AXIS_RIGHTY "LeftTrigger", // SDL_CONTROLLER_AXIS_TRIGGERLEFT "RightTrigger", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT }; static constexpr const char* s_sdl_axis_icons[][2] = { {ICON_PF_LEFT_ANALOG_LEFT, ICON_PF_LEFT_ANALOG_RIGHT}, // SDL_CONTROLLER_AXIS_LEFTX {ICON_PF_LEFT_ANALOG_UP, ICON_PF_LEFT_ANALOG_DOWN}, // SDL_CONTROLLER_AXIS_LEFTY {ICON_PF_RIGHT_ANALOG_LEFT, ICON_PF_RIGHT_ANALOG_RIGHT}, // SDL_CONTROLLER_AXIS_RIGHTX {ICON_PF_RIGHT_ANALOG_UP, ICON_PF_RIGHT_ANALOG_DOWN}, // SDL_CONTROLLER_AXIS_RIGHTY {nullptr, ICON_PF_LEFT_TRIGGER_PULL}, // SDL_CONTROLLER_AXIS_TRIGGERLEFT {nullptr, ICON_PF_RIGHT_TRIGGER_PULL}, // SDL_CONTROLLER_AXIS_TRIGGERRIGHT }; static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = { {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // SDL_CONTROLLER_AXIS_LEFTX {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // SDL_CONTROLLER_AXIS_LEFTY {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_CONTROLLER_AXIS_RIGHTX {GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown}, // SDL_CONTROLLER_AXIS_RIGHTY {GenericInputBinding::Unknown, GenericInputBinding::L2}, // SDL_CONTROLLER_AXIS_TRIGGERLEFT {GenericInputBinding::Unknown, GenericInputBinding::R2}, // SDL_CONTROLLER_AXIS_TRIGGERRIGHT }; static constexpr const char* s_sdl_button_names[] = { "A", // SDL_CONTROLLER_BUTTON_A "B", // SDL_CONTROLLER_BUTTON_B "X", // SDL_CONTROLLER_BUTTON_X "Y", // SDL_CONTROLLER_BUTTON_Y "Back", // SDL_CONTROLLER_BUTTON_BACK "Guide", // SDL_CONTROLLER_BUTTON_GUIDE "Start", // SDL_CONTROLLER_BUTTON_START "LeftStick", // SDL_CONTROLLER_BUTTON_LEFTSTICK "RightStick", // SDL_CONTROLLER_BUTTON_RIGHTSTICK "LeftShoulder", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER "RightShoulder", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER "DPadUp", // SDL_CONTROLLER_BUTTON_DPAD_UP "DPadDown", // SDL_CONTROLLER_BUTTON_DPAD_DOWN "DPadLeft", // SDL_CONTROLLER_BUTTON_DPAD_LEFT "DPadRight", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT "Misc1", // SDL_CONTROLLER_BUTTON_MISC1 "Paddle1", // SDL_CONTROLLER_BUTTON_PADDLE1 "Paddle2", // SDL_CONTROLLER_BUTTON_PADDLE2 "Paddle3", // SDL_CONTROLLER_BUTTON_PADDLE3 "Paddle4", // SDL_CONTROLLER_BUTTON_PADDLE4 "Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD }; static constexpr const char* s_sdl_button_icons[] = { ICON_PF_BUTTON_A, // SDL_CONTROLLER_BUTTON_A ICON_PF_BUTTON_B, // SDL_CONTROLLER_BUTTON_B ICON_PF_BUTTON_X, // SDL_CONTROLLER_BUTTON_X ICON_PF_BUTTON_Y, // SDL_CONTROLLER_BUTTON_Y ICON_PF_SHARE_CAPTURE, // SDL_CONTROLLER_BUTTON_BACK ICON_PF_XBOX, // SDL_CONTROLLER_BUTTON_GUIDE ICON_PF_BURGER_MENU, // SDL_CONTROLLER_BUTTON_START ICON_PF_LEFT_ANALOG_CLICK, // SDL_CONTROLLER_BUTTON_LEFTSTICK ICON_PF_RIGHT_ANALOG_CLICK, // SDL_CONTROLLER_BUTTON_RIGHTSTICK ICON_PF_LEFT_SHOULDER_LB, // SDL_CONTROLLER_BUTTON_LEFTSHOULDER ICON_PF_RIGHT_SHOULDER_RB, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER ICON_PF_XBOX_DPAD_UP, // SDL_CONTROLLER_BUTTON_DPAD_UP ICON_PF_XBOX_DPAD_DOWN, // SDL_CONTROLLER_BUTTON_DPAD_DOWN ICON_PF_XBOX_DPAD_LEFT, // SDL_CONTROLLER_BUTTON_DPAD_LEFT ICON_PF_XBOX_DPAD_RIGHT, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT }; static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = { GenericInputBinding::Cross, // SDL_CONTROLLER_BUTTON_A GenericInputBinding::Circle, // SDL_CONTROLLER_BUTTON_B GenericInputBinding::Square, // SDL_CONTROLLER_BUTTON_X GenericInputBinding::Triangle, // SDL_CONTROLLER_BUTTON_Y GenericInputBinding::Select, // SDL_CONTROLLER_BUTTON_BACK GenericInputBinding::System, // SDL_CONTROLLER_BUTTON_GUIDE GenericInputBinding::Start, // SDL_CONTROLLER_BUTTON_START GenericInputBinding::L3, // SDL_CONTROLLER_BUTTON_LEFTSTICK GenericInputBinding::R3, // SDL_CONTROLLER_BUTTON_RIGHTSTICK GenericInputBinding::L1, // SDL_CONTROLLER_BUTTON_LEFTSHOULDER GenericInputBinding::R1, // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER GenericInputBinding::DPadUp, // SDL_CONTROLLER_BUTTON_DPAD_UP GenericInputBinding::DPadDown, // SDL_CONTROLLER_BUTTON_DPAD_DOWN GenericInputBinding::DPadLeft, // SDL_CONTROLLER_BUTTON_DPAD_LEFT GenericInputBinding::DPadRight, // SDL_CONTROLLER_BUTTON_DPAD_RIGHT GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_MISC1 GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE1 GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE2 GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE3 GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_PADDLE4 GenericInputBinding::Unknown, // SDL_CONTROLLER_BUTTON_TOUCHPAD }; static constexpr const char* s_sdl_hat_direction_names[] = { // clang-format off "North", "East", "South", "West", // clang-format on }; static constexpr const char* s_sdl_default_led_colors[] = { "0000ff", // SDL-0 "ff0000", // SDL-1 "00ff00", // SDL-2 "ffff00", // SDL-3 }; static void SetControllerRGBLED(SDL_GameController* gc, u32 color) { SDL_GameControllerSetLED(gc, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff); } static void SDLLogCallback(void* userdata, int category, SDL_LogPriority priority, const char* message) { static constexpr LOGLEVEL priority_map[SDL_NUM_LOG_PRIORITIES] = { LOGLEVEL_DEBUG, LOGLEVEL_DEBUG, // SDL_LOG_PRIORITY_VERBOSE LOGLEVEL_DEBUG, // SDL_LOG_PRIORITY_DEBUG LOGLEVEL_INFO, // SDL_LOG_PRIORITY_INFO LOGLEVEL_WARNING, // SDL_LOG_PRIORITY_WARN LOGLEVEL_ERROR, // SDL_LOG_PRIORITY_ERROR LOGLEVEL_ERROR, // SDL_LOG_PRIORITY_CRITICAL }; Log::Write("SDL", "SDL", priority_map[priority], message); } SDLInputSource::SDLInputSource() = default; SDLInputSource::~SDLInputSource() { Assert(m_controllers.empty()); } bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock& settings_lock) { LoadSettings(si); settings_lock.unlock(); SetHints(); bool result = InitializeSubsystem(); settings_lock.lock(); return result; } void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock& settings_lock) { const bool old_controller_enhanced_mode = m_controller_enhanced_mode; LoadSettings(si); if (m_controller_enhanced_mode != old_controller_enhanced_mode) { settings_lock.unlock(); ShutdownSubsystem(); SetHints(); InitializeSubsystem(); settings_lock.lock(); } } bool SDLInputSource::ReloadDevices() { // We'll get a GC added/removed event here. PollEvents(); return false; } void SDLInputSource::Shutdown() { ShutdownSubsystem(); } void SDLInputSource::LoadSettings(SettingsInterface& si) { m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false); m_sdl_hints = si.GetKeyValueList("SDLHints"); for (u32 i = 0; i < MAX_LED_COLORS; i++) { const u32 color = GetRGBForPlayerId(si, i); if (m_led_colors[i] == color) continue; m_led_colors[i] = color; const auto it = GetControllerDataForPlayerId(i); if (it == m_controllers.end() || !it->game_controller || !SDL_GameControllerHasLED(it->game_controller)) continue; SetControllerRGBLED(it->game_controller, color); } } u32 SDLInputSource::GetRGBForPlayerId(SettingsInterface& si, u32 player_id) { return ParseRGBForPlayerId( si.GetStringValue("SDLExtra", fmt::format("Player{}LED", player_id).c_str(), s_sdl_default_led_colors[player_id]), player_id); } u32 SDLInputSource::ParseRGBForPlayerId(const std::string_view& str, u32 player_id) { if (player_id >= MAX_LED_COLORS) return 0; const u32 default_color = StringUtil::FromChars(s_sdl_default_led_colors[player_id], 16).value_or(0); const u32 color = StringUtil::FromChars(str, 16).value_or(default_color); return color; } void SDLInputSource::SetHints() { const std::string controller_db_path = Path::Combine(EmuFolders::Resources, CONTROLLER_DB_FILENAME); if (FileSystem::FileExists(controller_db_path.c_str())) SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG_FILE, controller_db_path.c_str()); else Log_ErrorFmt("Controller DB not found at '{}'", controller_db_path); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); // Enable Wii U Pro Controller support // New as of SDL 2.26, so use string SDL_SetHint("SDL_JOYSTICK_HIDAPI_WII", "1"); #ifndef _WIN32 // Gets us pressure sensitive button support on Linux // Apparently doesn't work on Windows, so leave it off there // New as of SDL 2.26, so use string SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS3", "1"); #endif for (const std::pair& hint : m_sdl_hints) SDL_SetHint(hint.first.c_str(), hint.second.c_str()); } bool SDLInputSource::InitializeSubsystem() { if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) < 0) { Log_ErrorPrint("SDL_InitSubSystem(SDL_INIT_JOYSTICK |SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC) failed"); return false; } SDL_LogSetOutputFunction(SDLLogCallback, nullptr); #ifdef _DEBUG SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE); #else SDL_LogSetAllPriority(SDL_LOG_PRIORITY_INFO); #endif // we should open the controllers as the connected events come in, so no need to do any more here m_sdl_subsystem_initialized = true; return true; } void SDLInputSource::ShutdownSubsystem() { while (!m_controllers.empty()) CloseDevice(m_controllers.begin()->joystick_id); if (m_sdl_subsystem_initialized) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC); m_sdl_subsystem_initialized = false; } } void SDLInputSource::PollEvents() { for (;;) { SDL_Event ev; if (SDL_PollEvent(&ev)) ProcessSDLEvent(&ev); else break; } } std::vector> SDLInputSource::EnumerateDevices() { std::vector> ret; for (const ControllerData& cd : m_controllers) { std::string id = fmt::format("SDL-{}", cd.player_id); const char* name = cd.game_controller ? SDL_GameControllerName(cd.game_controller) : SDL_JoystickName(cd.joystick); if (name) ret.emplace_back(std::move(id), name); else ret.emplace_back(std::move(id), "Unknown Device"); } return ret; } std::optional SDLInputSource::ParseKeyString(const std::string_view& device, const std::string_view& binding) { if (!StringUtil::StartsWith(device, "SDL-") || binding.empty()) return std::nullopt; const std::optional player_id = StringUtil::FromChars(device.substr(4)); if (!player_id.has_value() || player_id.value() < 0) return std::nullopt; InputBindingKey key = {}; key.source_type = InputSourceType::SDL; key.source_index = static_cast(player_id.value()); if (StringUtil::EndsWith(binding, "Motor")) { key.source_subtype = InputSubclass::ControllerMotor; if (binding == "LargeMotor") { key.data = 0; return key; } else if (binding == "SmallMotor") { key.data = 1; return key; } else { return std::nullopt; } } else if (StringUtil::EndsWith(binding, "Haptic")) { key.source_subtype = InputSubclass::ControllerHaptic; key.data = 0; return key; } else if (binding[0] == '+' || binding[0] == '-') { // likely an axis const std::string_view axis_name(binding.substr(1)); if (StringUtil::StartsWith(axis_name, "Axis")) { std::string_view end; if (auto value = StringUtil::FromChars(axis_name.substr(4), 10, &end)) { key.source_subtype = InputSubclass::ControllerAxis; key.data = *value + static_cast(std::size(s_sdl_axis_names)); key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; key.invert = (end == "~"); return key; } } for (u32 i = 0; i < std::size(s_sdl_axis_names); i++) { if (axis_name == s_sdl_axis_names[i]) { // found an axis! key.source_subtype = InputSubclass::ControllerAxis; key.data = i; key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None; return key; } } } else if (StringUtil::StartsWith(binding, "FullAxis")) { std::string_view end; if (auto value = StringUtil::FromChars(binding.substr(8), 10, &end)) { key.source_subtype = InputSubclass::ControllerAxis; key.data = *value + static_cast(std::size(s_sdl_axis_names)); key.modifier = InputModifier::FullAxis; key.invert = (end == "~"); return key; } } else if (StringUtil::StartsWith(binding, "Hat")) { std::string_view hat_dir; if (auto value = StringUtil::FromChars(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty()) { for (u8 dir = 0; dir < static_cast(std::size(s_sdl_hat_direction_names)); dir++) { if (hat_dir == s_sdl_hat_direction_names[dir]) { key.source_subtype = InputSubclass::ControllerHat; key.data = value.value() * static_cast(std::size(s_sdl_hat_direction_names)) + dir; return key; } } } } else { // must be a button if (StringUtil::StartsWith(binding, "Button")) { if (auto value = StringUtil::FromChars(binding.substr(6))) { key.source_subtype = InputSubclass::ControllerButton; key.data = *value + static_cast(std::size(s_sdl_button_names)); return key; } } for (u32 i = 0; i < std::size(s_sdl_button_names); i++) { if (binding == s_sdl_button_names[i]) { key.source_subtype = InputSubclass::ControllerButton; key.data = i; return key; } } } // unknown axis/button return std::nullopt; } TinyString SDLInputSource::ConvertKeyToString(InputBindingKey key) { TinyString ret; if (key.source_type == InputSourceType::SDL) { if (key.source_subtype == InputSubclass::ControllerAxis) { const char* modifier = (key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+")); if (key.data < std::size(s_sdl_axis_names)) { ret.format("SDL-{}/{}{}", static_cast(key.source_index), modifier, s_sdl_axis_names[key.data]); } else { ret.format("SDL-{}/{}Axis{}{}", static_cast(key.source_index), modifier, key.data - static_cast(std::size(s_sdl_axis_names)), key.invert ? "~" : ""); } } else if (key.source_subtype == InputSubclass::ControllerButton) { if (key.data < std::size(s_sdl_button_names)) { ret.format("SDL-{}/{}", static_cast(key.source_index), s_sdl_button_names[key.data]); } else { ret.format("SDL-{}/Button{}", static_cast(key.source_index), key.data - static_cast(std::size(s_sdl_button_names))); } } else if (key.source_subtype == InputSubclass::ControllerHat) { const u32 hat_index = key.data / static_cast(std::size(s_sdl_hat_direction_names)); const u32 hat_direction = key.data % static_cast(std::size(s_sdl_hat_direction_names)); ret.format("SDL-{}/Hat{}{}", static_cast(key.source_index), hat_index, s_sdl_hat_direction_names[hat_direction]); } else if (key.source_subtype == InputSubclass::ControllerMotor) { ret.format("SDL-{}/{}Motor", static_cast(key.source_index), key.data ? "Large" : "Small"); } else if (key.source_subtype == InputSubclass::ControllerHaptic) { ret.format("SDL-{}/Haptic", static_cast(key.source_index)); } } return ret; } TinyString SDLInputSource::ConvertKeyToIcon(InputBindingKey key) { TinyString ret; if (key.source_type == InputSourceType::SDL) { if (key.source_subtype == InputSubclass::ControllerAxis) { if (key.data < std::size(s_sdl_axis_icons) && key.modifier != InputModifier::FullAxis) { ret.format("SDL-{} {}", static_cast(key.source_index), s_sdl_axis_icons[key.data][key.modifier == InputModifier::None]); } } else if (key.source_subtype == InputSubclass::ControllerButton) { if (key.data < std::size(s_sdl_button_icons)) ret.format("SDL-{} {}", static_cast(key.source_index), s_sdl_button_icons[key.data]); } } return ret; } bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event) { switch (event->type) { case SDL_CONTROLLERDEVICEADDED: { Log_InfoPrintf("(SDLInputSource) Controller %d inserted", event->cdevice.which); OpenDevice(event->cdevice.which, true); return true; } case SDL_CONTROLLERDEVICEREMOVED: { Log_InfoPrintf("(SDLInputSource) Controller %d removed", event->cdevice.which); CloseDevice(event->cdevice.which); return true; } case SDL_JOYDEVICEADDED: { // Let game controller handle.. well.. game controllers. if (SDL_IsGameController(event->jdevice.which)) return false; Log_InfoPrintf("(SDLInputSource) Joystick %d inserted", event->jdevice.which); OpenDevice(event->cdevice.which, false); return true; } break; case SDL_JOYDEVICEREMOVED: { if (auto it = GetControllerDataForJoystickId(event->cdevice.which); it != m_controllers.end() && it->game_controller) return false; Log_InfoPrintf("(SDLInputSource) Joystick %d removed", event->jdevice.which); CloseDevice(event->cdevice.which); return true; } case SDL_CONTROLLERAXISMOTION: return HandleControllerAxisEvent(&event->caxis); case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: return HandleControllerButtonEvent(&event->cbutton); case SDL_JOYAXISMOTION: return HandleJoystickAxisEvent(&event->jaxis); case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: return HandleJoystickButtonEvent(&event->jbutton); case SDL_JOYHATMOTION: return HandleJoystickHatEvent(&event->jhat); default: return false; } } SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view& device) { if (!StringUtil::StartsWith(device, "SDL-")) return nullptr; const std::optional player_id = StringUtil::FromChars(device.substr(4)); if (!player_id.has_value() || player_id.value() < 0) return nullptr; auto it = GetControllerDataForPlayerId(player_id.value()); if (it == m_controllers.end()) return nullptr; return it->joystick; } SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForJoystickId(int id) { return std::find_if(m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.joystick_id == id; }); } SDLInputSource::ControllerDataVector::iterator SDLInputSource::GetControllerDataForPlayerId(int id) { return std::find_if(m_controllers.begin(), m_controllers.end(), [id](const ControllerData& cd) { return cd.player_id == id; }); } int SDLInputSource::GetFreePlayerId() const { for (int player_id = 0;; player_id++) { size_t i; for (i = 0; i < m_controllers.size(); i++) { if (m_controllers[i].player_id == player_id) break; } if (i == m_controllers.size()) return player_id; } return 0; } bool SDLInputSource::OpenDevice(int index, bool is_gamecontroller) { SDL_GameController* gcontroller; SDL_Joystick* joystick; if (is_gamecontroller) { gcontroller = SDL_GameControllerOpen(index); joystick = gcontroller ? SDL_GameControllerGetJoystick(gcontroller) : nullptr; } else { gcontroller = nullptr; joystick = SDL_JoystickOpen(index); } if (!gcontroller && !joystick) { Log_ErrorPrintf("(SDLInputSource) Failed to open controller %d", index); if (gcontroller) SDL_GameControllerClose(gcontroller); return false; } const int joystick_id = SDL_JoystickInstanceID(joystick); int player_id = gcontroller ? SDL_GameControllerGetPlayerIndex(gcontroller) : SDL_JoystickGetPlayerIndex(joystick); if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end()) { const int free_player_id = GetFreePlayerId(); Log_WarningPrintf("(SDLInputSource) Controller %d (joystick %d) returned player ID %d, which is invalid or in " "use. Using ID %d instead.", index, joystick_id, player_id, free_player_id); player_id = free_player_id; } const char* name = gcontroller ? SDL_GameControllerName(gcontroller) : SDL_JoystickName(joystick); if (!name) name = "Unknown Device"; Log_VerbosePrintf("(SDLInputSource) Opened %s %d (instance id %d, player id %d): %s", is_gamecontroller ? "game controller" : "joystick", index, joystick_id, player_id, name); ControllerData cd = {}; cd.player_id = player_id; cd.joystick_id = joystick_id; cd.haptic_left_right_effect = -1; cd.game_controller = gcontroller; cd.joystick = joystick; if (gcontroller) { const int num_axes = SDL_JoystickNumAxes(joystick); const int num_buttons = SDL_JoystickNumButtons(joystick); cd.joy_axis_used_in_gc.resize(num_axes, false); cd.joy_button_used_in_gc.resize(num_buttons, false); auto mark_bind = [&](SDL_GameControllerButtonBind bind) { if (bind.bindType == SDL_CONTROLLER_BINDTYPE_AXIS && bind.value.axis < num_axes) cd.joy_axis_used_in_gc[bind.value.axis] = true; if (bind.bindType == SDL_CONTROLLER_BINDTYPE_BUTTON && bind.value.button < num_buttons) cd.joy_button_used_in_gc[bind.value.button] = true; }; for (size_t i = 0; i < std::size(s_sdl_axis_names); i++) mark_bind(SDL_GameControllerGetBindForAxis(gcontroller, static_cast(i))); for (size_t i = 0; i < std::size(s_sdl_button_names); i++) mark_bind(SDL_GameControllerGetBindForButton(gcontroller, static_cast(i))); } else { // GC doesn't have the concept of hats, so we only need to do this for joysticks. const int num_hats = SDL_JoystickNumHats(joystick); if (num_hats > 0) cd.last_hat_state.resize(static_cast(num_hats), u8(0)); } cd.use_game_controller_rumble = (gcontroller && SDL_GameControllerRumble(gcontroller, 0, 0, 0) == 0); if (cd.use_game_controller_rumble) { Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via gamecontroller", name); } else { SDL_Haptic* haptic = SDL_HapticOpenFromJoystick(joystick); if (haptic) { SDL_HapticEffect ef = {}; ef.leftright.type = SDL_HAPTIC_LEFTRIGHT; ef.leftright.length = 1000; int ef_id = SDL_HapticNewEffect(haptic, &ef); if (ef_id >= 0) { cd.haptic = haptic; cd.haptic_left_right_effect = ef_id; } else { Log_ErrorPrintf("(SDLInputSource) Failed to create haptic left/right effect: %s", SDL_GetError()); if (SDL_HapticRumbleSupported(haptic) && SDL_HapticRumbleInit(haptic) != 0) { cd.haptic = haptic; } else { Log_ErrorPrintf("(SDLInputSource) No haptic rumble supported: %s", SDL_GetError()); SDL_HapticClose(haptic); } } } if (cd.haptic) Log_VerbosePrintf("(SDLInputSource) Rumble is supported on '%s' via haptic", name); } if (!cd.haptic && !cd.use_game_controller_rumble) Log_VerbosePrintf("(SDLInputSource) Rumble is not supported on '%s'", name); if (player_id >= 0 && static_cast(player_id) < MAX_LED_COLORS && gcontroller && SDL_GameControllerHasLED(gcontroller)) { SetControllerRGBLED(gcontroller, m_led_colors[player_id]); } m_controllers.push_back(std::move(cd)); InputManager::OnInputDeviceConnected(fmt::format("SDL-{}", player_id), name); return true; } bool SDLInputSource::CloseDevice(int joystick_index) { auto it = GetControllerDataForJoystickId(joystick_index); if (it == m_controllers.end()) return false; InputManager::OnInputDeviceDisconnected(fmt::format("SDL-{}", it->player_id)); if (it->haptic) SDL_HapticClose(it->haptic); if (it->game_controller) SDL_GameControllerClose(it->game_controller); else SDL_JoystickClose(it->joystick); m_controllers.erase(it); return true; } static float NormalizeS16(s16 value) { return static_cast(value) / (value < 0 ? 32768.0f : 32767.0f); } bool SDLInputSource::HandleControllerAxisEvent(const SDL_ControllerAxisEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); if (it == m_controllers.end()) return false; const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, ev->axis)); InputManager::InvokeEvents(key, NormalizeS16(ev->value)); return true; } bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); if (it == m_controllers.end()) return false; const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button)); const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ? s_sdl_generic_binding_button_mapping[ev->button] : GenericInputBinding::Unknown; InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key); return true; } bool SDLInputSource::HandleJoystickAxisEvent(const SDL_JoyAxisEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); if (it == m_controllers.end()) return false; if (ev->axis < it->joy_axis_used_in_gc.size() && it->joy_axis_used_in_gc[ev->axis]) return false; // Will get handled by GC event const u32 axis = ev->axis + static_cast(std::size(s_sdl_axis_names)); // Ensure we don't conflict with GC axes const InputBindingKey key(MakeGenericControllerAxisKey(InputSourceType::SDL, it->player_id, axis)); InputManager::InvokeEvents(key, NormalizeS16(ev->value)); return true; } bool SDLInputSource::HandleJoystickButtonEvent(const SDL_JoyButtonEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); if (it == m_controllers.end()) return false; if (ev->button < it->joy_button_used_in_gc.size() && it->joy_button_used_in_gc[ev->button]) return false; // Will get handled by GC event const u32 button = ev->button + static_cast(std::size(s_sdl_button_names)); // Ensure we don't conflict with GC buttons const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button)); InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f); return true; } bool SDLInputSource::HandleJoystickHatEvent(const SDL_JoyHatEvent* ev) { auto it = GetControllerDataForJoystickId(ev->which); if (it == m_controllers.end() || ev->hat >= it->last_hat_state.size()) return false; const unsigned long last_direction = it->last_hat_state[ev->hat]; it->last_hat_state[ev->hat] = ev->value; unsigned long changed_direction = last_direction ^ ev->value; while (changed_direction != 0) { const u32 pos = CountTrailingZeros(changed_direction); const unsigned long mask = (1u << pos); changed_direction &= ~mask; const InputBindingKey key(MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat, static_cast(pos), static_cast(std::size(s_sdl_hat_direction_names)))); InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f); } return true; } std::vector SDLInputSource::EnumerateMotors() { std::vector ret; InputBindingKey key = {}; key.source_type = InputSourceType::SDL; for (ControllerData& cd : m_controllers) { key.source_index = cd.player_id; if (cd.use_game_controller_rumble || cd.haptic_left_right_effect) { // two motors key.source_subtype = InputSubclass::ControllerMotor; key.data = 0; ret.push_back(key); key.data = 1; ret.push_back(key); } else if (cd.haptic) { // haptic effect key.source_subtype = InputSubclass::ControllerHaptic; key.data = 0; ret.push_back(key); } } return ret; } bool SDLInputSource::GetGenericBindingMapping(const std::string_view& device, GenericInputBindingMapping* mapping) { if (!StringUtil::StartsWith(device, "SDL-")) return false; const std::optional player_id = StringUtil::FromChars(device.substr(4)); if (!player_id.has_value() || player_id.value() < 0) return false; ControllerDataVector::iterator it = GetControllerDataForPlayerId(player_id.value()); if (it == m_controllers.end()) return false; if (it->game_controller) { // assume all buttons are present. const s32 pid = player_id.value(); for (u32 i = 0; i < std::size(s_sdl_generic_binding_axis_mapping); i++) { const GenericInputBinding negative = s_sdl_generic_binding_axis_mapping[i][0]; const GenericInputBinding positive = s_sdl_generic_binding_axis_mapping[i][1]; if (negative != GenericInputBinding::Unknown) mapping->emplace_back(negative, fmt::format("SDL-{}/-{}", pid, s_sdl_axis_names[i])); if (positive != GenericInputBinding::Unknown) mapping->emplace_back(positive, fmt::format("SDL-{}/+{}", pid, s_sdl_axis_names[i])); } for (u32 i = 0; i < std::size(s_sdl_generic_binding_button_mapping); i++) { const GenericInputBinding binding = s_sdl_generic_binding_button_mapping[i]; if (binding != GenericInputBinding::Unknown) mapping->emplace_back(binding, fmt::format("SDL-{}/{}", pid, s_sdl_button_names[i])); } if (it->use_game_controller_rumble || it->haptic_left_right_effect) { mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("SDL-{}/SmallMotor", pid)); mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("SDL-{}/LargeMotor", pid)); } else { mapping->emplace_back(GenericInputBinding::SmallMotor, fmt::format("SDL-{}/Haptic", pid)); mapping->emplace_back(GenericInputBinding::LargeMotor, fmt::format("SDL-{}/Haptic", pid)); } return true; } else { // joysticks have arbitrary axis numbers, so automapping isn't going to work here. return false; } } void SDLInputSource::UpdateMotorState(InputBindingKey key, float intensity) { if (key.source_subtype != InputSubclass::ControllerMotor && key.source_subtype != InputSubclass::ControllerHaptic) return; auto it = GetControllerDataForPlayerId(key.source_index); if (it == m_controllers.end()) return; it->rumble_intensity[key.data] = static_cast(intensity * 65535.0f); SendRumbleUpdate(&(*it)); } void SDLInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity, float small_intensity) { if (large_key.source_index != small_key.source_index || large_key.source_subtype != InputSubclass::ControllerMotor || small_key.source_subtype != InputSubclass::ControllerMotor) { // bonkers config where they're mapped to different controllers... who would do such a thing? UpdateMotorState(large_key, large_intensity); UpdateMotorState(small_key, small_intensity); return; } auto it = GetControllerDataForPlayerId(large_key.source_index); if (it == m_controllers.end()) return; it->rumble_intensity[large_key.data] = static_cast(large_intensity * 65535.0f); it->rumble_intensity[small_key.data] = static_cast(small_intensity * 65535.0f); SendRumbleUpdate(&(*it)); } void SDLInputSource::SendRumbleUpdate(ControllerData* cd) { // we'll update before this duration is elapsed static constexpr u32 DURATION = 65535; // SDL_MAX_RUMBLE_DURATION_MS if (cd->use_game_controller_rumble) { SDL_GameControllerRumble(cd->game_controller, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION); return; } if (cd->haptic_left_right_effect >= 0) { if ((static_cast(cd->rumble_intensity[0]) + static_cast(cd->rumble_intensity[1])) > 0) { SDL_HapticEffect ef; ef.type = SDL_HAPTIC_LEFTRIGHT; ef.leftright.large_magnitude = cd->rumble_intensity[0]; ef.leftright.small_magnitude = cd->rumble_intensity[1]; ef.leftright.length = DURATION; SDL_HapticUpdateEffect(cd->haptic, cd->haptic_left_right_effect, &ef); SDL_HapticRunEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY); } else { SDL_HapticStopEffect(cd->haptic, cd->haptic_left_right_effect); } } else { const float strength = static_cast(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f); if (strength > 0.0f) SDL_HapticRumblePlay(cd->haptic, strength, DURATION); else SDL_HapticRumbleStop(cd->haptic); } } std::unique_ptr InputSource::CreateSDLSource() { return std::make_unique(); }