pcsx2/pcsx2/Input/SDLInputSource.cpp

1632 lines
55 KiB
C++

// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "Config.h"
#include "Input/SDLInputSource.h"
#include "Input/InputManager.h"
#include "Host.h"
#include "ImGui/FullscreenUI.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/Error.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "IconsPromptFont.h"
#include <bit>
#include <cmath>
#include <VMManager.h>
static constexpr const char* CONTROLLER_DB_FILENAME = "game_controller_db.txt";
static constexpr const char* s_sdl_axis_setting_names[] = {
"LeftX", // SDL_GAMEPAD_AXIS_LEFTX
"LeftY", // SDL_GAMEPAD_AXIS_LEFTY
"RightX", // SDL_GAMEPAD_AXIS_RIGHTX
"RightY", // SDL_GAMEPAD_AXIS_RIGHTY
"LeftTrigger", // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
"RightTrigger", // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static_assert(std::size(s_sdl_axis_setting_names) == SDL_GAMEPAD_AXIS_COUNT);
static constexpr const char* s_sdl_axis_names[] = {
"Left X", // SDL_GAMEPAD_AXIS_LEFTX
"Left Y", // SDL_GAMEPAD_AXIS_LEFTY
"Right X", // SDL_GAMEPAD_AXIS_RIGHTX
"Right Y", // SDL_GAMEPAD_AXIS_RIGHTY
};
static constexpr const char* s_sdl_trigger_names[] = {
"Left Trigger", // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
"Right Trigger", // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static constexpr const char* s_sdl_trigger_ps_names[] = {
"L2", // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
"R2", // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static const char* const* s_sdl_trigger_names_list[] = {
s_sdl_trigger_names, // SDL_GAMEPAD_TYPE_UNKNOWN
s_sdl_trigger_names, // SDL_GAMEPAD_TYPE_STANDARD
s_sdl_trigger_names, // SDL_GAMEPAD_TYPE_XBOX360
s_sdl_trigger_names, // SDL_GAMEPAD_TYPE_XBOXONE
s_sdl_trigger_ps_names, // SDL_GAMEPAD_TYPE_PS3
s_sdl_trigger_ps_names, // SDL_GAMEPAD_TYPE_PS4
s_sdl_trigger_ps_names, // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr const char* s_sdl_axis_icons[][2] = {
{ICON_PF_LEFT_ANALOG_LEFT, ICON_PF_LEFT_ANALOG_RIGHT}, // SDL_GAMEPAD_AXIS_LEFTX
{ICON_PF_LEFT_ANALOG_UP, ICON_PF_LEFT_ANALOG_DOWN}, // SDL_GAMEPAD_AXIS_LEFTY
{ICON_PF_RIGHT_ANALOG_LEFT, ICON_PF_RIGHT_ANALOG_RIGHT}, // SDL_GAMEPAD_AXIS_RIGHTX
{ICON_PF_RIGHT_ANALOG_UP, ICON_PF_RIGHT_ANALOG_DOWN}, // SDL_GAMEPAD_AXIS_RIGHTY
};
static constexpr const char* s_sdl_trigger_icons[] = {
ICON_PF_LEFT_TRIGGER_PULL, // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
ICON_PF_RIGHT_TRIGGER_PULL, // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static constexpr const char* s_sdl_trigger_ps_icons[] = {
ICON_PF_LEFT_TRIGGER_L2, // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
ICON_PF_RIGHT_TRIGGER_R2, // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static const char* const* s_sdl_trigger_icons_list[] = {
s_sdl_trigger_icons, // SDL_GAMEPAD_TYPE_UNKNOWN
s_sdl_trigger_icons, // SDL_GAMEPAD_TYPE_STANDARD
s_sdl_trigger_icons, // SDL_GAMEPAD_TYPE_XBOX360
s_sdl_trigger_icons, // SDL_GAMEPAD_TYPE_XBOXONE
s_sdl_trigger_ps_icons, // SDL_GAMEPAD_TYPE_PS3
s_sdl_trigger_ps_icons, // SDL_GAMEPAD_TYPE_PS4
s_sdl_trigger_ps_icons, // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = {
{GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // SDL_GAMEPAD_AXIS_LEFTX
{GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // SDL_GAMEPAD_AXIS_LEFTY
{GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // SDL_GAMEPAD_AXIS_RIGHTX
{GenericInputBinding::RightStickUp, GenericInputBinding::RightStickDown}, // SDL_GAMEPAD_AXIS_RIGHTY
{GenericInputBinding::Unknown, GenericInputBinding::L2}, // SDL_GAMEPAD_AXIS_LEFT_TRIGGER
{GenericInputBinding::Unknown, GenericInputBinding::R2}, // SDL_GAMEPAD_AXIS_RIGHT_TRIGGER
};
static constexpr const char* s_sdl_button_setting_names[] = {
"FaceSouth", // SDL_GAMEPAD_BUTTON_SOUTH
"FaceEast", // SDL_GAMEPAD_BUTTON_EAST
"FaceWest", // SDL_GAMEPAD_BUTTON_WEST
"FaceNorth", // SDL_GAMEPAD_BUTTON_NORTH
"Back", // SDL_GAMEPAD_BUTTON_BACK
"Guide", // SDL_GAMEPAD_BUTTON_GUIDE
"Start", // SDL_GAMEPAD_BUTTON_START
"LeftStick", // SDL_GAMEPAD_BUTTON_LEFT_STICK
"RightStick", // SDL_GAMEPAD_BUTTON_RIGHT_STICK
"LeftShoulder", // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
"RightShoulder", // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
"DPadUp", // SDL_GAMEPAD_BUTTON_DPAD_UP
"DPadDown", // SDL_GAMEPAD_BUTTON_DPAD_DOWN
"DPadLeft", // SDL_GAMEPAD_BUTTON_DPAD_LEFT
"DPadRight", // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
"Misc1", // SDL_GAMEPAD_BUTTON_MISC1
"Paddle1", // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1
"Paddle2", // SDL_GAMEPAD_BUTTON_LEFT_PADDLE1
"Paddle3", // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2
"Paddle4", // SDL_GAMEPAD_BUTTON_LEFT_PADDLE2
"Touchpad", // SDL_GAMEPAD_BUTTON_TOUCHPAD
"Misc2", // SDL_GAMEPAD_BUTTON_MISC2
"Misc3", // SDL_GAMEPAD_BUTTON_MISC3
"Misc4", // SDL_GAMEPAD_BUTTON_MISC4
"Misc5", // SDL_GAMEPAD_BUTTON_MISC5
"Misc6", // SDL_GAMEPAD_BUTTON_MISC6
};
static_assert(std::size(s_sdl_button_setting_names) == SDL_GAMEPAD_BUTTON_COUNT);
static constexpr const char* s_sdl_face_button_names[] = {
nullptr, // SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN
"A", // SDL_GAMEPAD_BUTTON_LABEL_A
"B", // SDL_GAMEPAD_BUTTON_LABEL_B
"X", // SDL_GAMEPAD_BUTTON_LABEL_X
"Y", // SDL_GAMEPAD_BUTTON_LABEL_Y
"Cross", // SDL_GAMEPAD_BUTTON_LABEL_CROSS
"Circle", // SDL_GAMEPAD_BUTTON_LABEL_CIRCLE
"Square", // SDL_GAMEPAD_BUTTON_LABEL_SQUARE
"Triangle", // SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE
};
static constexpr const char* s_sdl_button_names[] = {
"Face South", // SDL_GAMEPAD_BUTTON_SOUTH
"Face East", // SDL_GAMEPAD_BUTTON_EAST
"Face West", // SDL_GAMEPAD_BUTTON_WEST
"Face North", // SDL_GAMEPAD_BUTTON_NORTH
"Back", // SDL_GAMEPAD_BUTTON_BACK
"Guide", // SDL_GAMEPAD_BUTTON_GUIDE
"Start", // SDL_GAMEPAD_BUTTON_START
"Left Stick", // SDL_GAMEPAD_BUTTON_LEFT_STICK
"Right Stick", // SDL_GAMEPAD_BUTTON_RIGHT_STICK
"Left Shoulder", // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
"Right Shoulder", // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
"D-Pad Up", // SDL_GAMEPAD_BUTTON_DPAD_UP
"D-Pad Down", // SDL_GAMEPAD_BUTTON_DPAD_DOWN
"D-Pad Left", // SDL_GAMEPAD_BUTTON_DPAD_LEFT
"D-Pad Right", // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
"Misc 1", // SDL_GAMEPAD_BUTTON_MISC1
"Paddle 1", // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1
"Paddle 2", // SDL_GAMEPAD_BUTTON_LEFT_PADDLE1
"Paddle 3", // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2
"Paddle 4", // SDL_GAMEPAD_BUTTON_LEFT_PADDLE2
"Touchpad", // SDL_GAMEPAD_BUTTON_TOUCHPAD
"Misc 2", // SDL_GAMEPAD_BUTTON_MISC2
"Misc 3", // SDL_GAMEPAD_BUTTON_MISC3
"Misc 4", // SDL_GAMEPAD_BUTTON_MISC4
"Misc 5", // SDL_GAMEPAD_BUTTON_MISC5
"Misc 6", // SDL_GAMEPAD_BUTTON_MISC6
};
static constexpr const char* s_sdl_button_ps3_names[] = {
"Cross", // SDL_GAMEPAD_BUTTON_SOUTH
"Circle", // SDL_GAMEPAD_BUTTON_EAST
"Square", // SDL_GAMEPAD_BUTTON_WEST
"Triangle", // SDL_GAMEPAD_BUTTON_NORTH
"Select", // SDL_GAMEPAD_BUTTON_BACK
"PS", // SDL_GAMEPAD_BUTTON_GUIDE
"Start", // SDL_GAMEPAD_BUTTON_START
"Left Stick", // SDL_GAMEPAD_BUTTON_LEFT_STICK
"Right Stick", // SDL_GAMEPAD_BUTTON_RIGHT_STICK
"L1", // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
"R1", // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
};
static constexpr const char* s_sdl_button_ps4_names[] = {
"Cross", // SDL_GAMEPAD_BUTTON_SOUTH
"Circle", // SDL_GAMEPAD_BUTTON_EAST
"Square", // SDL_GAMEPAD_BUTTON_WEST
"Triangle", // SDL_GAMEPAD_BUTTON_NORTH
"Share", // SDL_GAMEPAD_BUTTON_BACK
"PS", // SDL_GAMEPAD_BUTTON_GUIDE
"Options", // SDL_GAMEPAD_BUTTON_START
"Left Stick", // SDL_GAMEPAD_BUTTON_LEFT_STICK
"Right Stick", // SDL_GAMEPAD_BUTTON_RIGHT_STICK
"L1", // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
"R1", // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
};
static constexpr const char* s_sdl_button_ps5_names[] = {
"Cross", // SDL_GAMEPAD_BUTTON_SOUTH
"Circle", // SDL_GAMEPAD_BUTTON_EAST
"Square", // SDL_GAMEPAD_BUTTON_WEST
"Triangle", // SDL_GAMEPAD_BUTTON_NORTH
"Create", // SDL_GAMEPAD_BUTTON_BACK
"PS", // SDL_GAMEPAD_BUTTON_GUIDE
"Options", // SDL_GAMEPAD_BUTTON_START
"Left Stick", // SDL_GAMEPAD_BUTTON_LEFT_STICK
"Right Stick", // SDL_GAMEPAD_BUTTON_RIGHT_STICK
"L1", // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
"R1", // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
nullptr, // SDL_GAMEPAD_BUTTON_DPAD_UP
nullptr, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
nullptr, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
nullptr, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
"Mute", // SDL_GAMEPAD_BUTTON_MISC1
};
static constexpr const char* const* s_sdl_button_names_list[] = {
s_sdl_button_names, // SDL_GAMEPAD_TYPE_UNKNOWN
s_sdl_button_names, // SDL_GAMEPAD_TYPE_STANDARD
s_sdl_button_names, // SDL_GAMEPAD_TYPE_XBOX360
s_sdl_button_names, // SDL_GAMEPAD_TYPE_XBOXONE
s_sdl_button_ps3_names, // SDL_GAMEPAD_TYPE_PS3
s_sdl_button_ps4_names, // SDL_GAMEPAD_TYPE_PS4
s_sdl_button_ps5_names, // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr size_t s_sdl_button_namesize_list[] = {
std::size(s_sdl_button_names), // SDL_GAMEPAD_TYPE_UNKNOWN
std::size(s_sdl_button_names), // SDL_GAMEPAD_TYPE_STANDARD
std::size(s_sdl_button_names), // SDL_GAMEPAD_TYPE_XBOX360
std::size(s_sdl_button_names), // SDL_GAMEPAD_TYPE_XBOXONE
std::size(s_sdl_button_ps3_names), // SDL_GAMEPAD_TYPE_PS3
std::size(s_sdl_button_ps4_names), // SDL_GAMEPAD_TYPE_PS4
std::size(s_sdl_button_ps5_names), // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr const char* s_sdl_face_button_icons[] = {
nullptr, // SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN
ICON_PF_BUTTON_A, // SDL_GAMEPAD_BUTTON_LABEL_A
ICON_PF_BUTTON_B, // SDL_GAMEPAD_BUTTON_LABEL_B
ICON_PF_BUTTON_X, // SDL_GAMEPAD_BUTTON_LABEL_X
ICON_PF_BUTTON_Y, // SDL_GAMEPAD_BUTTON_LABEL_Y
ICON_PF_BUTTON_CROSS, // SDL_GAMEPAD_BUTTON_LABEL_CROSS
ICON_PF_BUTTON_CIRCLE, // SDL_GAMEPAD_BUTTON_LABEL_CIRCLE
ICON_PF_BUTTON_SQUARE, // SDL_GAMEPAD_BUTTON_LABEL_SQUARE
ICON_PF_BUTTON_TRIANGLE, // SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE
};
static constexpr const char* s_sdl_button_icons[] = {
ICON_PF_BUTTON_DOWN_A, // SDL_GAMEPAD_BUTTON_SOUTH
ICON_PF_BUTTON_RIGHT_B, // SDL_GAMEPAD_BUTTON_EAST
ICON_PF_BUTTON_LEFT_X, // SDL_GAMEPAD_BUTTON_WEST
ICON_PF_BUTTON_UP_Y, // SDL_GAMEPAD_BUTTON_NORTH
ICON_PF_SHARE_CAPTURE, // SDL_GAMEPAD_BUTTON_BACK
ICON_PF_XBOX, // SDL_GAMEPAD_BUTTON_GUIDE
ICON_PF_BURGER_MENU, // SDL_GAMEPAD_BUTTON_START
ICON_PF_LEFT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_LEFT_STICK
ICON_PF_RIGHT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_RIGHT_STICK
ICON_PF_LEFT_SHOULDER_LB, // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
ICON_PF_RIGHT_SHOULDER_RB, // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
ICON_PF_XBOX_DPAD_UP, // SDL_GAMEPAD_BUTTON_DPAD_UP
ICON_PF_XBOX_DPAD_DOWN, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
ICON_PF_XBOX_DPAD_LEFT, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
ICON_PF_XBOX_DPAD_RIGHT, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
};
static constexpr const char* s_sdl_button_ps3_icons[] = {
ICON_PF_BUTTON_CROSS, // SDL_GAMEPAD_BUTTON_SOUTH
ICON_PF_BUTTON_CIRCLE, // SDL_GAMEPAD_BUTTON_EAST
ICON_PF_BUTTON_SQUARE, // SDL_GAMEPAD_BUTTON_WEST
ICON_PF_BUTTON_TRIANGLE, // SDL_GAMEPAD_BUTTON_NORTH
ICON_PF_SELECT_SHARE, // SDL_GAMEPAD_BUTTON_BACK
ICON_PF_XBOX, // SDL_GAMEPAD_BUTTON_GUIDE
ICON_PF_START, // SDL_GAMEPAD_BUTTON_START
ICON_PF_LEFT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_LEFT_STICK
ICON_PF_RIGHT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_RIGHT_STICK
ICON_PF_LEFT_SHOULDER_L1, // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
ICON_PF_RIGHT_SHOULDER_R1, // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
ICON_PF_DPAD_UP, // SDL_GAMEPAD_BUTTON_DPAD_UP
ICON_PF_DPAD_DOWN, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
ICON_PF_DPAD_LEFT, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
ICON_PF_DPAD_RIGHT, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
};
static constexpr const char* s_sdl_button_ps4_icons[] = {
ICON_PF_BUTTON_CROSS, // SDL_GAMEPAD_BUTTON_SOUTH
ICON_PF_BUTTON_CIRCLE, // SDL_GAMEPAD_BUTTON_EAST
ICON_PF_BUTTON_SQUARE, // SDL_GAMEPAD_BUTTON_WEST
ICON_PF_BUTTON_TRIANGLE, // SDL_GAMEPAD_BUTTON_NORTH
ICON_PF_DUALSHOCK_SHARE, // SDL_GAMEPAD_BUTTON_BACK
ICON_PF_PLAYSTATION, // SDL_GAMEPAD_BUTTON_GUIDE
ICON_PF_DUALSHOCK_OPTIONS, // SDL_GAMEPAD_BUTTON_START
ICON_PF_LEFT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_LEFT_STICK
ICON_PF_RIGHT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_RIGHT_STICK
ICON_PF_LEFT_SHOULDER_L1, // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
ICON_PF_RIGHT_SHOULDER_R1, // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
ICON_PF_DPAD_UP, // SDL_GAMEPAD_BUTTON_DPAD_UP
ICON_PF_DPAD_DOWN, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
ICON_PF_DPAD_LEFT, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
ICON_PF_DPAD_RIGHT, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
nullptr, // SDL_GAMEPAD_BUTTON_MISC1
nullptr, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1
nullptr, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE1
nullptr, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2
nullptr, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE2
ICON_PF_DUALSHOCK_TOUCHPAD, // SDL_GAMEPAD_BUTTON_TOUCHPAD
};
static constexpr const char* s_sdl_button_ps5_icons[] = {
ICON_PF_BUTTON_CROSS, // SDL_GAMEPAD_BUTTON_SOUTH
ICON_PF_BUTTON_CIRCLE, // SDL_GAMEPAD_BUTTON_EAST
ICON_PF_BUTTON_SQUARE, // SDL_GAMEPAD_BUTTON_WEST
ICON_PF_BUTTON_TRIANGLE, // SDL_GAMEPAD_BUTTON_NORTH
ICON_PF_DUALSENSE_SHARE, // SDL_GAMEPAD_BUTTON_BACK
ICON_PF_PLAYSTATION, // SDL_GAMEPAD_BUTTON_GUIDE
ICON_PF_DUALSENSE_OPTIONS, // SDL_GAMEPAD_BUTTON_START
ICON_PF_LEFT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_LEFT_STICK
ICON_PF_RIGHT_ANALOG_CLICK, // SDL_GAMEPAD_BUTTON_RIGHT_STICK
ICON_PF_LEFT_SHOULDER_L1, // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
ICON_PF_RIGHT_SHOULDER_R1, // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
ICON_PF_DPAD_UP, // SDL_GAMEPAD_BUTTON_DPAD_UP
ICON_PF_DPAD_DOWN, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
ICON_PF_DPAD_LEFT, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
ICON_PF_DPAD_RIGHT, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
nullptr, // SDL_GAMEPAD_BUTTON_MISC1
nullptr, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1
nullptr, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE1
nullptr, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2
nullptr, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE2
ICON_PF_DUALSENSE_TOUCHPAD, // SDL_GAMEPAD_BUTTON_TOUCHPAD
};
static constexpr const char* const* s_sdl_button_icons_list[] = {
s_sdl_button_icons, // SDL_GAMEPAD_TYPE_UNKNOWN
s_sdl_button_icons, // SDL_GAMEPAD_TYPE_STANDARD
s_sdl_button_icons, // SDL_GAMEPAD_TYPE_XBOX360
s_sdl_button_icons, // SDL_GAMEPAD_TYPE_XBOXONE
s_sdl_button_ps3_icons, // SDL_GAMEPAD_TYPE_PS3
s_sdl_button_ps4_icons, // SDL_GAMEPAD_TYPE_PS4
s_sdl_button_ps5_icons, // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr size_t s_sdl_button_iconsize_list[] = {
std::size(s_sdl_button_icons), // SDL_GAMEPAD_TYPE_UNKNOWN
std::size(s_sdl_button_icons), // SDL_GAMEPAD_TYPE_STANDARD
std::size(s_sdl_button_icons), // SDL_GAMEPAD_TYPE_XBOX360
std::size(s_sdl_button_icons), // SDL_GAMEPAD_TYPE_XBOXONE
std::size(s_sdl_button_ps3_icons), // SDL_GAMEPAD_TYPE_PS3
std::size(s_sdl_button_ps4_icons), // SDL_GAMEPAD_TYPE_PS4
std::size(s_sdl_button_ps5_icons), // SDL_GAMEPAD_TYPE_PS5
// Switch
};
static constexpr const GenericInputBinding s_sdl_generic_binding_button_mapping[] = {
GenericInputBinding::Cross, // SDL_GAMEPAD_BUTTON_SOUTH
GenericInputBinding::Circle, // SDL_GAMEPAD_BUTTON_EAST
GenericInputBinding::Square, // SDL_GAMEPAD_BUTTON_WEST
GenericInputBinding::Triangle, // SDL_GAMEPAD_BUTTON_NORTH
GenericInputBinding::Select, // SDL_GAMEPAD_BUTTON_BACK
GenericInputBinding::System, // SDL_GAMEPAD_BUTTON_GUIDE
GenericInputBinding::Start, // SDL_GAMEPAD_BUTTON_START
GenericInputBinding::L3, // SDL_GAMEPAD_BUTTON_LEFT_STICK
GenericInputBinding::R3, // SDL_GAMEPAD_BUTTON_RIGHT_STICK
GenericInputBinding::L1, // SDL_GAMEPAD_BUTTON_LEFT_SHOULDER
GenericInputBinding::R1, // SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER
GenericInputBinding::DPadUp, // SDL_GAMEPAD_BUTTON_DPAD_UP
GenericInputBinding::DPadDown, // SDL_GAMEPAD_BUTTON_DPAD_DOWN
GenericInputBinding::DPadLeft, // SDL_GAMEPAD_BUTTON_DPAD_LEFT
GenericInputBinding::DPadRight, // SDL_GAMEPAD_BUTTON_DPAD_RIGHT
GenericInputBinding::Unknown, // SDL_GAMEPAD_BUTTON_MISC1
GenericInputBinding::Unknown, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1
GenericInputBinding::Unknown, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE1
GenericInputBinding::Unknown, // SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2
GenericInputBinding::Unknown, // SDL_GAMEPAD_BUTTON_LEFT_PADDLE2
GenericInputBinding::Unknown, // SDL_GAMEPAD_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[] = {
"000080", // SDL-0
"800000", // SDL-1
"008000", // SDL-2
"808000", // SDL-3
};
static void SetGamepadRGBLED(SDL_Gamepad* pad, u32 color)
{
SDL_SetGamepadLED(pad, (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff);
}
static void SDLLogCallback(void* userdata, int category, SDL_LogPriority priority, const char* message)
{
if (priority >= SDL_LOG_PRIORITY_INFO)
Console.WriteLn(fmt::format("SDL: {}", message));
else
DevCon.WriteLn(fmt::format("SDL: {}", message));
}
SDLInputSource::SDLInputSource() = default;
SDLInputSource::~SDLInputSource()
{
pxAssert(m_controllers.empty());
}
bool SDLInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
LoadSettings(si);
settings_lock.unlock();
SetHints();
bool result = InitializeSubsystem();
settings_lock.lock();
return result;
}
bool SDLInputSource::IsInitialized()
{
return m_sdl_subsystem_initialized;
}
void SDLInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
{
const bool old_enable_enhanced_reports = m_enable_enhanced_reports;
const bool old_enable_ps5_player_leds = m_enable_ps5_player_leds;
const bool old_use_raw_input = m_use_raw_input;
#ifdef __APPLE__
const bool old_enable_iokit_driver = m_enable_iokit_driver;
const bool old_enable_mfi_driver = m_enable_mfi_driver;
#endif
LoadSettings(si);
#ifdef __APPLE__
const bool drivers_changed =
(m_enable_iokit_driver != old_enable_iokit_driver || m_enable_mfi_driver != old_enable_mfi_driver);
#else
constexpr bool drivers_changed = false;
#endif
if (m_enable_enhanced_reports != old_enable_enhanced_reports ||
m_enable_ps5_player_leds != old_enable_ps5_player_leds ||
m_use_raw_input != old_use_raw_input ||
drivers_changed)
{
settings_lock.unlock();
ShutdownSubsystem();
SetHints();
InitializeSubsystem();
settings_lock.lock();
}
}
bool SDLInputSource::ReloadDevices()
{
// We'll get a device added/removed event here.
PollEvents();
return false;
}
void SDLInputSource::Shutdown()
{
ShutdownSubsystem();
}
void SDLInputSource::LoadSettings(SettingsInterface& si)
{
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->gamepad)
continue;
const SDL_PropertiesID props = SDL_GetGamepadProperties(it->gamepad);
if (props == 0)
{
ERROR_LOG("SDLInputSource: SDL_GetGamepadProperties() failed");
continue;
}
if (!SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false))
continue;
SetGamepadRGBLED(it->gamepad, color);
}
m_sdl_hints = si.GetKeyValueList("SDLHints");
m_enable_enhanced_reports = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", true);
m_enable_ps5_player_leds = si.GetBoolValue("InputSources", "SDLPS5PlayerLED", true);
m_use_raw_input = si.GetBoolValue("InputSources", "SDLRawInput", false);
#ifdef __APPLE__
m_enable_iokit_driver = si.GetBoolValue("InputSources", "SDLIOKitDriver", true);
m_enable_mfi_driver = si.GetBoolValue("InputSources", "SDLMFIDriver", true);
#endif
}
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<u32>(s_sdl_default_led_colors[player_id], 16).value_or(0);
const u32 color = StringUtil::FromChars<u32>(str, 16).value_or(default_color);
return color;
}
void SDLInputSource::ResetRGBForAllPlayers(SettingsInterface& si)
{
for (u32 player_id = 0; player_id < MAX_LED_COLORS; player_id++)
{
si.DeleteValue("SDLExtra", fmt::format("Player{}LED", player_id).c_str());
}
}
void SDLInputSource::SetHints()
{
if (const std::string upath = Path::Combine(EmuFolders::DataRoot, CONTROLLER_DB_FILENAME); FileSystem::FileExists(upath.c_str()))
{
Console.WriteLn(Color_StrongGreen, fmt::format("SDLInputSource: Using Controller DB from user directory: '{}'", upath));
SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG_FILE, upath.c_str());
}
else if (const std::string rpath = EmuFolders::GetOverridableResourcePath(CONTROLLER_DB_FILENAME); FileSystem::FileExists(rpath.c_str()))
{
Console.WriteLn(Color_StrongGreen, "SDLInputSource: Using Controller DB from resources.");
SDL_SetHint(SDL_HINT_GAMECONTROLLERCONFIG_FILE, rpath.c_str());
}
else
{
Console.Error(fmt::format("SDLInputSource: Controller DB not found, it should be named '{}'", CONTROLLER_DB_FILENAME));
}
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, m_use_raw_input ? "1" : "0");
SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, m_enable_enhanced_reports ? "auto" : "0"); // PS4/PS5 Rumble
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_PLAYER_LED, m_enable_ps5_player_leds ? "1" : "0");
// Enable Wii U Pro Controller support
SDL_SetHint(SDL_HINT_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
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS3, "1");
#else
// Use the Sixaxis driver (or DsHidMini in SXS mode).
// We don't support DsHidMini's SDF mode as none of the
// PS3 hints allow accessing all the pressure sense axis.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS3_SIXAXIS_DRIVER, "1");
#endif
#ifdef __APPLE__
Console.WriteLnFmt("IOKit is {}, MFI is {}.", m_enable_iokit_driver ? "enabled" : "disabled", m_enable_mfi_driver ? "enabled" : "disabled");
SDL_SetHint(SDL_HINT_JOYSTICK_IOKIT, m_enable_iokit_driver ? "1" : "0");
SDL_SetHint(SDL_HINT_JOYSTICK_MFI, m_enable_mfi_driver ? "1" : "0");
#endif
for (const std::pair<std::string, std::string>& 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_GAMEPAD | SDL_INIT_HAPTIC))
{
Console.Error("SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC) failed");
return false;
}
SDL_SetLogOutputFunction(SDLLogCallback, nullptr);
#ifdef PCSX2_DEVBUILD
SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE);
#else
SDL_SetLogPriorities(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;
int count;
char** mappings = SDL_GetGamepadMappings(&count);
if (mappings != nullptr)
{
SDL_free(mappings);
Console.WriteLnFmt(Color_StrongGreen, "SDLInputSource: {} gamepad mappings are loaded.", count);
}
else
Console.Error("SDL_GetGamepadMappings() failed {}", SDL_GetError());
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_GAMEPAD | 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<std::pair<std::string, std::string>> SDLInputSource::EnumerateDevices()
{
std::vector<std::pair<std::string, std::string>> ret;
for (const ControllerData& cd : m_controllers)
{
std::string id(StringUtil::StdStringFromFormat("SDL-%d", cd.player_id));
const char* name = cd.gamepad ? SDL_GetGamepadName(cd.gamepad) : SDL_GetJoystickName(cd.joystick);
if (name)
ret.emplace_back(std::move(id), name);
else
ret.emplace_back(std::move(id), "Unknown Device");
}
return ret;
}
std::optional<InputBindingKey> SDLInputSource::ParseKeyString(const std::string_view device, const std::string_view binding)
{
if (!device.starts_with("SDL-") || binding.empty())
return std::nullopt;
const std::optional<s32> player_id = StringUtil::FromChars<s32>(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<u32>(player_id.value());
// SDL2-SDL3 migrations
static constexpr const char* sdl_button_legacy_names[] = {
"A", // SDL_CONTROLLER_BUTTON_A
"B", // SDL_CONTROLLER_BUTTON_B
"X", // SDL_CONTROLLER_BUTTON_X
"Y", // SDL_CONTROLLER_BUTTON_Y
};
for (u32 i = 0; i < std::size(sdl_button_legacy_names); i++)
{
if (binding == sdl_button_legacy_names[i])
{
key.source_subtype = InputSubclass::ControllerButton;
key.data = i;
// SDL2 would map A/B/X/Y based on the button's label
// We need to convert this to a positional binding for SDL3
static constexpr SDL_GamepadButton face_button_pos[] = {
SDL_GAMEPAD_BUTTON_SOUTH,
SDL_GAMEPAD_BUTTON_EAST,
SDL_GAMEPAD_BUTTON_WEST,
SDL_GAMEPAD_BUTTON_NORTH,
};
// This migrations needs to inspect the controller
{
std::lock_guard lock(m_controllers_key_mutex);
auto it = GetControllerDataForPlayerId(key.source_index);
if (it != m_controllers.end() && it->gamepad)
{
static bool shown_prompt = false;
for (u32 pos = 0; pos < std::size(face_button_pos); pos++)
{
// A/B/X/Y are equal to 1/2/3/4 in SDL_GamepadButtonLabel
// PS controllers have positional A/B/X/Y, so don't need adjusting
// Controllers with unknown labels are assumed to have positional A/B/X/Y
const SDL_GamepadButtonLabel label = SDL_GetGamepadButtonLabel(it->gamepad, face_button_pos[pos]);
if (key.data == (label - 1))
{
if (key.data != pos)
{
if (!shown_prompt)
{
shown_prompt = true;
Host::ReportInfoAsync(TRANSLATE("SDLInputSource", "SDL3 Migration"),
TRANSLATE("SDLInputSource", "As part of our upgrade to SDL3, we've had to migrate your binds\n"
"Your controller did not match the Xbox layout and may need rebinding\n"
"Please verify your controller settings and amend if required"));
// Also apply BPM setting for legacy binds
// We assume this is a Nintendo controller, BPM will check if it is
// Defer this, as we are probably under a setting lock
Host::RunOnCPUThread([] {
if (!Host::ContainsBaseSettingValue("UI", "SDL2NintendoLayout"))
{
Host::SetBaseStringSettingValue("UI", "SDL2NintendoLayout", "auto");
Host::CommitBaseSettingChanges();
// Get FSUI to recheck setting
if (FullscreenUI::IsInitialized())
FullscreenUI::GamepadLayoutChanged();
}
});
}
key.data = pos;
}
break;
}
}
key.needs_migration = true;
return key;
}
else if (std::find(m_gamepads_needing_migration.begin(), m_gamepads_needing_migration.end(), key.source_index) ==
m_gamepads_needing_migration.end())
{
// flag the device to migrate later
m_gamepads_needing_migration.push_back(key.source_index);
return std::nullopt;
}
}
}
}
if (binding.starts_with("+Axis") || binding.starts_with("-Axis"))
{
const std::string_view axis_name(binding.substr(1));
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(axis_name.substr(4), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value - 6 + std::size(s_sdl_axis_setting_names);
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
key.invert = (end == "~");
key.needs_migration = true;
return key;
}
}
else if (binding.starts_with("FullAxis"))
{
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(binding.substr(8), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value - 6 + std::size(s_sdl_axis_setting_names);
key.modifier = InputModifier::FullAxis;
key.invert = (end == "~");
key.needs_migration = true;
return key;
}
}
else if (binding.starts_with("Button"))
{
if (auto value = StringUtil::FromChars<u32>(binding.substr(6)))
{
key.source_subtype = InputSubclass::ControllerButton;
key.data = *value - 21 + std::size(s_sdl_button_setting_names);
key.needs_migration = true;
return key;
}
}
// End Migrations
if (binding.ends_with("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 (binding.ends_with("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 (axis_name.starts_with("JoyAxis"))
{
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(axis_name.substr(7), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value + std::size(s_sdl_axis_setting_names);
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
key.invert = (end == "~");
return key;
}
}
for (u32 i = 0; i < std::size(s_sdl_axis_setting_names); i++)
{
if (axis_name == s_sdl_axis_setting_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 (binding.starts_with("FullJoyAxis"))
{
std::string_view end;
if (auto value = StringUtil::FromChars<u32>(binding.substr(11), 10, &end))
{
key.source_subtype = InputSubclass::ControllerAxis;
key.data = *value + std::size(s_sdl_axis_setting_names);
key.modifier = InputModifier::FullAxis;
key.invert = (end == "~");
return key;
}
}
else if (binding.starts_with("Hat"))
{
std::string_view hat_dir;
if (auto value = StringUtil::FromChars<u32>(binding.substr(3), 10, &hat_dir); value.has_value() && !hat_dir.empty())
{
for (u8 dir = 0; dir < static_cast<u8>(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() * std::size(s_sdl_hat_direction_names) + dir;
return key;
}
}
}
}
else
{
// must be a button
if (binding.starts_with("JoyButton"))
{
if (auto value = StringUtil::FromChars<u32>(binding.substr(9)))
{
key.source_subtype = InputSubclass::ControllerButton;
key.data = *value + std::size(s_sdl_button_setting_names);
return key;
}
}
for (u32 i = 0; i < std::size(s_sdl_button_setting_names); i++)
{
if (binding == s_sdl_button_setting_names[i])
{
key.source_subtype = InputSubclass::ControllerButton;
key.data = i;
return key;
}
}
}
// unknown axis/button
return std::nullopt;
}
TinyString SDLInputSource::ConvertKeyToString(InputBindingKey key, bool display, bool migration)
{
TinyString ret;
if (key.source_type == InputSourceType::SDL)
{
if (key.source_subtype == InputSubclass::ControllerAxis)
{
const char* modifier = (key.modifier == InputModifier::FullAxis ? (display ? "Full " : "Full") : (key.modifier == InputModifier::Negate ? "-" : "+"));
if (display)
{
std::lock_guard lock(m_controllers_key_mutex);
SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN;
auto it = GetControllerDataForPlayerId(key.source_index);
if (it != m_controllers.end())
type = SDL_GetRealGamepadType(it->gamepad);
if (key.data < std::size(s_sdl_axis_names))
{
ret.format("SDL-{} {}{}", static_cast<u32>(key.source_index), modifier, s_sdl_axis_names[key.data]);
}
else if (key.data - std::size(s_sdl_axis_names) < std::size(s_sdl_trigger_names))
{
const u32 trigger_index = key.data - std::size(s_sdl_axis_names);
if (type < std::size(s_sdl_trigger_names_list))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_trigger_names_list[type][trigger_index]);
else
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_trigger_names[trigger_index]);
}
else
ret.format("SDL-{} {}Axis {}{}", static_cast<u32>(key.source_index), modifier, key.data - std::size(s_sdl_axis_setting_names) + 1, key.invert ? "~" : "");
}
else
{
if (key.data < std::size(s_sdl_axis_setting_names))
ret.format("SDL-{}/{}{}", static_cast<u32>(key.source_index), modifier, s_sdl_axis_setting_names[key.data]);
else
ret.format("SDL-{}/{}JoyAxis{}{}", static_cast<u32>(key.source_index), modifier, key.data - std::size(s_sdl_axis_setting_names), (key.invert && (migration || !ShouldIgnoreInversion())) ? "~" : "");
}
}
else if (key.source_subtype == InputSubclass::ControllerButton)
{
if (display)
{
std::lock_guard lock(m_controllers_key_mutex);
SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN;
auto it = GetControllerDataForPlayerId(key.source_index);
if (it != m_controllers.end())
type = SDL_GetRealGamepadType(it->gamepad);
if (type > SDL_GAMEPAD_TYPE_STANDARD && type < std::size(s_sdl_button_names_list) &&
key.data < s_sdl_button_namesize_list[type] && s_sdl_button_names_list[type][key.data] != nullptr)
{
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_names_list[type][key.data]);
}
else if (key.data < 4)
{
SDL_GamepadButtonLabel label = SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN;
if (it != m_controllers.end() && it->gamepad)
label = SDL_GetGamepadButtonLabel(it->gamepad, static_cast<SDL_GamepadButton>(key.data));
if (label > SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN && label < std::size(s_sdl_face_button_names))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_face_button_names[label]);
else
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_names[key.data]);
}
else if (key.data < std::size(s_sdl_button_names))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_names[key.data]);
else
ret.format("SDL-{} Button {}", static_cast<u32>(key.source_index), key.data - std::size(s_sdl_button_setting_names) + 1);
}
else
{
if (key.data < std::size(s_sdl_button_setting_names))
ret.format("SDL-{}/{}", static_cast<u32>(key.source_index), s_sdl_button_setting_names[key.data]);
else
ret.format("SDL-{}/JoyButton{}", static_cast<u32>(key.source_index), key.data - std::size(s_sdl_button_setting_names));
}
}
else if (key.source_subtype == InputSubclass::ControllerHat)
{
const u32 hat_index = key.data / static_cast<u32>(std::size(s_sdl_hat_direction_names));
const u32 hat_direction = key.data % static_cast<u32>(std::size(s_sdl_hat_direction_names));
if (display)
ret.format("SDL-{} Hat {} {}", static_cast<u32>(key.source_index), hat_index + 1, s_sdl_hat_direction_names[hat_direction]);
else
ret.format("SDL-{}/Hat{}{}", static_cast<u32>(key.source_index), hat_index, s_sdl_hat_direction_names[hat_direction]);
}
else if (key.source_subtype == InputSubclass::ControllerMotor)
{
if (display)
ret.format("SDL-{} {} Motor", static_cast<u32>(key.source_index), key.data ? "Small" : "Large");
else
ret.format("SDL-{}/{}Motor", static_cast<u32>(key.source_index), key.data ? "Small" : "Large");
}
else if (key.source_subtype == InputSubclass::ControllerHaptic)
{
if (display)
ret.format("SDL-{} Haptic", static_cast<u32>(key.source_index));
else
ret.format("SDL-{}/Haptic", static_cast<u32>(key.source_index));
}
}
return ret;
}
TinyString SDLInputSource::ConvertKeyToIcon(InputBindingKey key)
{
TinyString ret;
if (key.source_type == InputSourceType::SDL)
{
std::lock_guard lock(m_controllers_key_mutex);
SDL_GamepadType type = SDL_GAMEPAD_TYPE_UNKNOWN;
auto it = GetControllerDataForPlayerId(key.source_index);
if (it != m_controllers.end())
type = SDL_GetRealGamepadType(it->gamepad);
if (key.source_subtype == InputSubclass::ControllerAxis)
{
if (key.modifier != InputModifier::FullAxis)
{
if (key.data < std::size(s_sdl_axis_icons))
{
ret.format("SDL-{} {}", static_cast<u32>(key.source_index),
s_sdl_axis_icons[key.data][key.modifier == InputModifier::None]);
}
else if (key.data - std::size(s_sdl_axis_icons) < std::size(s_sdl_trigger_icons))
{
const u32 trigger_index = key.data - std::size(s_sdl_axis_icons);
if (type < std::size(s_sdl_trigger_icons_list))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_trigger_icons_list[type][trigger_index]);
else
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_trigger_icons[trigger_index]);
}
}
}
else if (key.source_subtype == InputSubclass::ControllerButton)
{
if (type > SDL_GAMEPAD_TYPE_STANDARD && type < std::size(s_sdl_button_icons_list) &&
key.data < s_sdl_button_iconsize_list[type] && s_sdl_button_icons_list[type][key.data] != nullptr)
{
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_icons_list[type][key.data]);
}
else if (key.data < 4)
{
SDL_GamepadButtonLabel label = SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN;
if (it != m_controllers.end() && it->gamepad)
label = SDL_GetGamepadButtonLabel(it->gamepad, static_cast<SDL_GamepadButton>(key.data));
if (label > SDL_GAMEPAD_BUTTON_LABEL_UNKNOWN && label < std::size(s_sdl_face_button_icons))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_face_button_icons[label]);
else
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_icons[key.data]);
}
else if (key.data < std::size(s_sdl_button_icons))
ret.format("SDL-{} {}", static_cast<u32>(key.source_index), s_sdl_button_icons[key.data]);
}
}
return ret;
}
bool SDLInputSource::ProcessSDLEvent(const SDL_Event* event)
{
switch (event->type)
{
case SDL_EVENT_GAMEPAD_ADDED:
{
Console.WriteLn("SDLInputSource: Gamepad %d inserted", event->gdevice.which);
OpenDevice(event->gdevice.which, true);
return true;
}
case SDL_EVENT_GAMEPAD_REMOVED:
{
Console.WriteLn("SDLInputSource: Gamepad %d removed", event->gdevice.which);
CloseDevice(event->gdevice.which);
return true;
}
case SDL_EVENT_JOYSTICK_ADDED:
{
// Let gamepad handle.. well.. gamepads.
if (SDL_IsGamepad(event->jdevice.which))
return false;
Console.WriteLn("SDLInputSource: Joystick %d inserted", event->jdevice.which);
OpenDevice(event->jdevice.which, false);
return true;
}
break;
case SDL_EVENT_JOYSTICK_REMOVED:
{
if (auto it = GetControllerDataForJoystickId(event->jdevice.which); it != m_controllers.end() && it->gamepad)
return false;
Console.WriteLn("SDLInputSource: Joystick %d removed", event->jdevice.which);
CloseDevice(event->jdevice.which);
return true;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
return HandleGamepadAxisEvent(&event->gaxis);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return HandleGamepadButtonEvent(&event->gbutton);
case SDL_EVENT_JOYSTICK_AXIS_MOTION:
return HandleJoystickAxisEvent(&event->jaxis);
case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
case SDL_EVENT_JOYSTICK_BUTTON_UP:
return HandleJoystickButtonEvent(&event->jbutton);
case SDL_EVENT_JOYSTICK_HAT_MOTION:
return HandleJoystickHatEvent(&event->jhat);
default:
return false;
}
}
SDL_Joystick* SDLInputSource::GetJoystickForDevice(const std::string_view device)
{
if (!device.starts_with("SDL-"))
return nullptr;
const std::optional<s32> player_id = StringUtil::FromChars<s32>(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(SDL_JoystickID 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(SDL_JoystickID index, bool is_gamepad)
{
SDL_Gamepad* gamepad;
SDL_Joystick* joystick;
if (is_gamepad)
{
gamepad = SDL_OpenGamepad(index);
joystick = gamepad ? SDL_GetGamepadJoystick(gamepad) : nullptr;
}
else
{
gamepad = nullptr;
joystick = SDL_OpenJoystick(index);
}
if (!gamepad && !joystick)
{
ERROR_LOG("SDLInputSource: Failed to open controller {}", index);
return false;
}
const SDL_JoystickID joystick_id = SDL_GetJoystickID(joystick);
int player_id = gamepad ? SDL_GetGamepadPlayerIndex(gamepad) : SDL_GetJoystickPlayerIndex(joystick);
for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it)
{
if (it->joystick_id == joystick_id)
{
ERROR_LOG("SDLInputSource: Controller {}, instance {}, player {} already connected, ignoring.", index, joystick_id, player_id);
if (gamepad)
SDL_CloseGamepad(gamepad);
else
SDL_CloseJoystick(joystick);
return false;
}
}
if (player_id < 0 || GetControllerDataForPlayerId(player_id) != m_controllers.end())
{
const int free_player_id = GetFreePlayerId();
WARNING_LOG("SDLInputSource: Controller {} (joystick {}) returned player ID {}, which is invalid or in "
"use. Using ID {} instead.",
index, joystick_id, player_id, free_player_id);
player_id = free_player_id;
}
const char* name = gamepad ? SDL_GetGamepadName(gamepad) : SDL_GetJoystickName(joystick);
if (!name)
name = "Unknown Device";
INFO_LOG("SDLInputSource: Opened {} {} (instance id {}, player id {}): {}", is_gamepad ? "gamepad" : "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.gamepad = gamepad;
cd.joystick = joystick;
if (gamepad)
{
int binding_count;
SDL_GamepadBinding** bindings = SDL_GetGamepadBindings(gamepad, &binding_count);
if (bindings)
{
const int num_axes = SDL_GetNumJoystickAxes(joystick);
const int num_buttons = SDL_GetNumJoystickButtons(joystick);
cd.joy_axis_used_in_pad.resize(num_axes, false);
cd.joy_button_used_in_pad.resize(num_buttons, false);
auto mark_bind = [&](SDL_GamepadBinding* bind) {
if (bind->input_type == SDL_GAMEPAD_BINDTYPE_AXIS && bind->input.axis.axis < num_axes)
cd.joy_axis_used_in_pad[bind->input.axis.axis] = true;
if (bind->input_type == SDL_GAMEPAD_BINDTYPE_BUTTON && bind->input.button < num_buttons)
cd.joy_button_used_in_pad[bind->input.button] = true;
};
for (int i = 0; i < binding_count; i++)
mark_bind(bindings[i]);
SDL_free(bindings);
INFO_LOG("SDLInputSource: Gamepad {} has {} axes and {} buttons", player_id, num_axes, num_buttons);
}
else
ERROR_LOG("SDLInputSource: Failed to get gamepad bindings {}", SDL_GetError());
}
else
{
// Gamepad doesn't have the concept of hats, so we only need to do this for joysticks.
const int num_hats = SDL_GetNumJoystickHats(joystick);
if (num_hats > 0)
cd.last_hat_state.resize(static_cast<size_t>(num_hats), u8{0});
INFO_LOG("SDLInputSource: Joystick {} has {} axes, {} buttons and {} hats", player_id,
SDL_GetNumJoystickAxes(joystick), SDL_GetNumJoystickButtons(joystick), num_hats);
}
cd.use_gamepad_rumble = (gamepad && SDL_RumbleGamepad(gamepad, 0, 0, 0));
if (cd.use_gamepad_rumble)
{
INFO_LOG("SDLInputSource: Rumble is supported on '{}' via gamepad", name);
}
else
{
SDL_Haptic* haptic = SDL_OpenHapticFromJoystick(joystick);
if (haptic)
{
SDL_HapticEffect ef = {};
ef.leftright.type = SDL_HAPTIC_LEFTRIGHT;
ef.leftright.length = 1000;
int ef_id = SDL_CreateHapticEffect(haptic, &ef);
if (ef_id >= 0)
{
cd.haptic = haptic;
cd.haptic_left_right_effect = ef_id;
}
else
{
ERROR_LOG("SDLInputSource: Failed to create haptic left/right effect: {}", SDL_GetError());
if (SDL_HapticRumbleSupported(haptic) && SDL_InitHapticRumble(haptic))
{
cd.haptic = haptic;
}
else
{
ERROR_LOG("SDLInputSource: No haptic rumble supported: {}", SDL_GetError());
SDL_CloseHaptic(haptic);
}
}
}
if (cd.haptic)
INFO_LOG("SDLInputSource: Rumble is supported on '{}' via haptic", name);
}
if (!cd.haptic && !cd.use_gamepad_rumble)
WARNING_LOG("SDLInputSource: Rumble is not supported on '{}'", name);
if (gamepad)
{
const SDL_PropertiesID props = SDL_GetGamepadProperties(gamepad);
bool hasLED = false;
if (props == 0)
ERROR_LOG("SDLInputSource: SDL_GetGamepadProperties() failed");
else
hasLED = SDL_GetBooleanProperty(props, SDL_PROP_GAMEPAD_CAP_RGB_LED_BOOLEAN, false);
if (player_id >= 0 && static_cast<u32>(player_id) < MAX_LED_COLORS && hasLED)
{
SetGamepadRGBLED(gamepad, m_led_colors[player_id]);
}
}
{
std::unique_lock lock(m_controllers_key_mutex);
m_controllers.push_back(std::move(cd));
if (gamepad)
{
// Perform SDL2-SDL3 migrations that require inspecting the gamepad
auto idx = std::find(m_gamepads_needing_migration.begin(), m_gamepads_needing_migration.end(), player_id);
if (idx != m_gamepads_needing_migration.end())
{
m_gamepads_needing_migration.erase(idx);
// ParseKeyString will need the lock when migrating
// unlock here so we don't deadlock reloading binds
lock.unlock();
// Reload bindings to perform migration
VMManager::ReloadInputBindings(true);
}
}
}
InputManager::OnInputDeviceConnected(fmt::format("SDL-{}", player_id), name);
return true;
}
bool SDLInputSource::CloseDevice(SDL_JoystickID joystick_index)
{
auto it = GetControllerDataForJoystickId(joystick_index);
if (it == m_controllers.end())
return false;
{
std::lock_guard lock(m_controllers_key_mutex);
InputManager::OnInputDeviceDisconnected(
{InputBindingKey{.source_type = InputSourceType::SDL, .source_index = static_cast<u32>(it->player_id)}},
fmt::format("SDL-{}", it->player_id));
if (it->haptic)
SDL_CloseHaptic(it->haptic);
if (it->gamepad)
SDL_CloseGamepad(it->gamepad);
else
SDL_CloseJoystick(it->joystick);
m_controllers.erase(it);
}
return true;
}
static float NormalizeS16(s16 value)
{
return static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
}
bool SDLInputSource::HandleGamepadAxisEvent(const SDL_GamepadAxisEvent* 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::HandleGamepadButtonEvent(const SDL_GamepadButtonEvent* 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, static_cast<float>(ev->down), 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_pad.size() && it->joy_axis_used_in_pad[ev->axis])
return false; // Will get handled by Gamepad event
const u32 axis = ev->axis + std::size(s_sdl_axis_setting_names); // Ensure we don't conflict with Gamepad 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_pad.size() && it->joy_button_used_in_pad[ev->button])
return false; // Will get handled by Gamepad event
const u32 button = ev->button + std::size(s_sdl_button_setting_names); // Ensure we don't conflict with Gamepad buttons
const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, button));
InputManager::InvokeEvents(key, static_cast<float>(ev->down));
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 u8 last_direction = it->last_hat_state[ev->hat];
it->last_hat_state[ev->hat] = ev->value;
u8 changed_direction = last_direction ^ ev->value;
while (changed_direction != 0)
{
const u8 pos = std::countr_zero(changed_direction);
const u8 mask = (1u << pos);
changed_direction &= ~mask;
const InputBindingKey key(
MakeGenericControllerHatKey(InputSourceType::SDL, it->player_id, ev->hat, pos, std::size(s_sdl_hat_direction_names)));
InputManager::InvokeEvents(key, (last_direction & mask) ? 0.0f : 1.0f);
}
return true;
}
std::vector<InputBindingKey> SDLInputSource::EnumerateMotors()
{
std::vector<InputBindingKey> ret;
InputBindingKey key = {};
key.source_type = InputSourceType::SDL;
for (ControllerData& cd : m_controllers)
{
key.source_index = cd.player_id;
if (cd.use_gamepad_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, InputManager::GenericInputBindingMapping* mapping)
{
if (!device.starts_with("SDL-"))
return false;
const std::optional<s32> player_id = StringUtil::FromChars<s32>(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->gamepad)
{
// 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_setting_names[i]));
if (positive != GenericInputBinding::Unknown)
mapping->emplace_back(positive, fmt::format("SDL-{}/+{}", pid, s_sdl_axis_setting_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_setting_names[i]));
}
if (it->use_gamepad_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;
}
}
InputLayout SDLInputSource::GetControllerLayout(u32 index)
{
auto it = GetControllerDataForPlayerId(index);
if (it == m_controllers.end())
return InputLayout::Unknown;
// Infer layout based on face button label to avoid having
// to maintain a long switch statement of gamepad types
// clang-format off
switch (SDL_GetGamepadButtonLabel(it->gamepad, SDL_GAMEPAD_BUTTON_EAST))
{
case SDL_GAMEPAD_BUTTON_LABEL_B: return InputLayout::Xbox;
case SDL_GAMEPAD_BUTTON_LABEL_A: return InputLayout::Nintendo;
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: return InputLayout::Playstation;
default: return InputLayout::Unknown;
}
// clang-format on
}
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<u16>(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<u16>(large_intensity * 65535.0f);
it->rumble_intensity[small_key.data] = static_cast<u16>(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_gamepad_rumble)
{
SDL_RumbleGamepad(cd->gamepad, cd->rumble_intensity[0], cd->rumble_intensity[1], DURATION);
return;
}
if (cd->haptic_left_right_effect >= 0)
{
if ((static_cast<u32>(cd->rumble_intensity[0]) + static_cast<u32>(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_UpdateHapticEffect(cd->haptic, cd->haptic_left_right_effect, &ef);
SDL_RunHapticEffect(cd->haptic, cd->haptic_left_right_effect, SDL_HAPTIC_INFINITY);
}
else
{
SDL_StopHapticEffect(cd->haptic, cd->haptic_left_right_effect);
}
}
else
{
const float strength = static_cast<float>(std::max(cd->rumble_intensity[0], cd->rumble_intensity[1])) * (1.0f / 65535.0f);
if (strength > 0.0f)
SDL_PlayHapticRumble(cd->haptic, strength, DURATION);
else
SDL_StopHapticRumble(cd->haptic);
}
}