diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props
index 67e3a15f4c..8b5e3b9f00 100644
--- a/Source/Core/DolphinLib.props
+++ b/Source/Core/DolphinLib.props
@@ -541,6 +541,7 @@
+
@@ -1203,6 +1204,7 @@
+
diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt
index ee144e32f4..d19a1ef3a2 100644
--- a/Source/Core/InputCommon/CMakeLists.txt
+++ b/Source/Core/InputCommon/CMakeLists.txt
@@ -177,6 +177,8 @@ if(ENABLE_SDL)
target_sources(inputcommon PRIVATE
ControllerInterface/SDL/SDL.cpp
ControllerInterface/SDL/SDL.h
+ ControllerInterface/SDL/SDLGamepad.cpp
+ ControllerInterface/SDL/SDLGamepad.h
)
target_link_libraries(inputcommon PRIVATE SDL2::SDL2)
target_compile_definitions(inputcommon PUBLIC HAVE_SDL2=1)
diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp
index 04981e1011..07a38b0289 100644
--- a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp
+++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp
@@ -4,421 +4,24 @@
#include "InputCommon/ControllerInterface/SDL/SDL.h"
#include
-#include
#include
-#include
-#include
-
-#include "Common/Event.h"
-#include "Common/Logging/Log.h"
-#include "Common/MathUtil.h"
-#include "Common/ScopeGuard.h"
-#include "InputCommon/ControllerInterface/ControllerInterface.h"
-
#ifdef _WIN32
#include
#endif
-namespace ciface::Core
-{
-class Device;
-}
+#include
-namespace
-{
-std::string GetLegacyButtonName(int index)
-{
- return "Button " + std::to_string(index);
-}
+#include "Common/Event.h"
+#include "Common/Logging/Log.h"
+#include "Common/ScopeGuard.h"
-std::string GetLegacyAxisName(int index, int range)
-{
- return "Axis " + std::to_string(index) + (range < 0 ? '-' : '+');
-}
-
-std::string GetLegacyHatName(int index, int direction)
-{
- return "Hat " + std::to_string(index) + ' ' + "NESW"[direction];
-}
-
-constexpr int GetDirectionFromHatMask(u8 mask)
-{
- return MathUtil::IntLog2(mask);
-}
-
-static_assert(GetDirectionFromHatMask(SDL_HAT_UP) == 0);
-static_assert(GetDirectionFromHatMask(SDL_HAT_LEFT) == 3);
-
-bool IsTriggerAxis(int index)
-{
- // First 4 axes are for the analog sticks, the rest are for the triggers
- return index >= 4;
-}
-
-ControlState GetBatteryValueFromSDLPowerLevel(SDL_JoystickPowerLevel sdl_power_level)
-{
- // Values come from comments in SDL_joystick.h
- // A proper percentage will be exposed in SDL3.
- ControlState result;
- switch (sdl_power_level)
- {
- case SDL_JOYSTICK_POWER_EMPTY:
- result = 0.025;
- break;
- case SDL_JOYSTICK_POWER_LOW:
- result = 0.125;
- break;
- case SDL_JOYSTICK_POWER_MEDIUM:
- result = 0.45;
- break;
- case SDL_JOYSTICK_POWER_FULL:
- result = 0.85;
- break;
- case SDL_JOYSTICK_POWER_WIRED:
- case SDL_JOYSTICK_POWER_MAX:
- result = 1.0;
- break;
- case SDL_JOYSTICK_POWER_UNKNOWN:
- default:
- result = 0.0;
- break;
- }
-
- return result * ciface::BATTERY_INPUT_MAX_VALUE;
-}
-
-} // namespace
+#include "InputCommon/ControllerInterface/ControllerInterface.h"
+#include "InputCommon/ControllerInterface/SDL/SDLGamepad.h"
namespace ciface::SDL
{
-class GameController : public Core::Device
-{
-private:
- // GameController inputs
- class Button : public Core::Device::Input
- {
- public:
- std::string GetName() const override;
- Button(SDL_GameController* gc, SDL_GameControllerButton button) : m_gc(gc), m_button(button) {}
- ControlState GetState() const override;
- bool IsMatchingName(std::string_view name) const override;
-
- private:
- SDL_GameController* const m_gc;
- const SDL_GameControllerButton m_button;
- };
-
- class Axis : public Core::Device::Input
- {
- public:
- std::string GetName() const override;
- Axis(SDL_GameController* gc, Sint16 range, SDL_GameControllerAxis axis)
- : m_gc(gc), m_range(range), m_axis(axis)
- {
- }
- ControlState GetState() const override;
-
- private:
- SDL_GameController* const m_gc;
- const Sint16 m_range;
- const SDL_GameControllerAxis m_axis;
- };
-
- // Legacy inputs
- class LegacyButton : public Core::Device::Input
- {
- public:
- std::string GetName() const override { return GetLegacyButtonName(m_index); }
- LegacyButton(SDL_Joystick* js, int index) : m_js(js), m_index(index) {}
- ControlState GetState() const override;
-
- private:
- SDL_Joystick* const m_js;
- const int m_index;
- };
-
- class LegacyAxis : public Core::Device::Input
- {
- public:
- std::string GetName() const override { return GetLegacyAxisName(m_index, m_range); }
- LegacyAxis(SDL_Joystick* js, int index, s16 range, bool is_handled_elsewhere)
- : m_js(js), m_index(index), m_range(range), m_is_handled_elsewhere(is_handled_elsewhere)
- {
- }
- ControlState GetState() const override;
- bool IsHidden() const override { return m_is_handled_elsewhere; }
- bool IsDetectable() const override { return !IsHidden(); }
-
- private:
- SDL_Joystick* const m_js;
- const int m_index;
- const s16 m_range;
- const bool m_is_handled_elsewhere;
- };
-
- class LegacyHat : public Input
- {
- public:
- std::string GetName() const override { return GetLegacyHatName(m_index, m_direction); }
- LegacyHat(SDL_Joystick* js, int index, u8 direction)
- : m_js(js), m_index(index), m_direction(direction)
- {
- }
- ControlState GetState() const override;
-
- private:
- SDL_Joystick* const m_js;
- const int m_index;
- const u8 m_direction;
- };
-
- class BatteryInput final : public Input
- {
- public:
- explicit BatteryInput(const ControlState* battery_value) : m_battery_value(*battery_value) {}
- std::string GetName() const override { return "Battery"; }
- ControlState GetState() const override { return m_battery_value; }
- bool IsDetectable() const override { return false; }
-
- private:
- const ControlState& m_battery_value;
- };
-
- // Rumble
- class Rumble : public Output
- {
- public:
- using UpdateCallback = void (GameController::*)(void);
-
- Rumble(const char* name, GameController& gc, Uint16* state, UpdateCallback update_callback)
- : m_name{name}, m_gc{gc}, m_state{*state}, m_update_callback{update_callback}
- {
- }
- std::string GetName() const override { return m_name; }
- void SetState(ControlState state) override
- {
- const auto new_state = state * std::numeric_limits::max();
- if (m_state == new_state)
- return;
-
- m_state = new_state;
- (m_gc.*m_update_callback)();
- }
-
- private:
- const char* const m_name;
- GameController& m_gc;
- Uint16& m_state;
- UpdateCallback const m_update_callback;
- };
-
- class CombinedMotor : public Output
- {
- public:
- CombinedMotor(GameController& gc, Uint16* low_state, Uint16* high_state)
- : m_gc{gc}, m_low_state{*low_state}, m_high_state{*high_state}
- {
- }
- std::string GetName() const override { return "Motor"; }
- void SetState(ControlState state) override
- {
- const auto new_state = state * std::numeric_limits::max();
- if (m_low_state == new_state && m_high_state == new_state)
- return;
-
- m_low_state = new_state;
- m_high_state = new_state;
- m_gc.UpdateRumble();
- }
-
- private:
- GameController& m_gc;
- Uint16& m_low_state;
- Uint16& m_high_state;
- };
-
- class HapticEffect : public Output
- {
- public:
- HapticEffect(SDL_Haptic* haptic);
- ~HapticEffect();
-
- protected:
- virtual bool UpdateParameters(s16 value) = 0;
- static void SetDirection(SDL_HapticDirection* dir);
-
- SDL_HapticEffect m_effect = {};
-
- static constexpr u16 DISABLED_EFFECT_TYPE = 0;
-
- private:
- virtual void SetState(ControlState state) override final;
- void UpdateEffect();
- SDL_Haptic* const m_haptic;
- int m_id = -1;
- };
-
- class ConstantEffect : public HapticEffect
- {
- public:
- ConstantEffect(SDL_Haptic* haptic);
- std::string GetName() const override;
-
- private:
- bool UpdateParameters(s16 value) override;
- };
-
- class RampEffect : public HapticEffect
- {
- public:
- RampEffect(SDL_Haptic* haptic);
- std::string GetName() const override;
-
- private:
- bool UpdateParameters(s16 value) override;
- };
-
- class PeriodicEffect : public HapticEffect
- {
- public:
- PeriodicEffect(SDL_Haptic* haptic, u16 waveform);
- std::string GetName() const override;
-
- private:
- bool UpdateParameters(s16 value) override;
-
- const u16 m_waveform;
- };
-
- class LeftRightEffect : public HapticEffect
- {
- public:
- enum class Motor : u8
- {
- Weak,
- Strong,
- };
-
- LeftRightEffect(SDL_Haptic* haptic, Motor motor);
- std::string GetName() const override;
-
- private:
- bool UpdateParameters(s16 value) override;
-
- const Motor m_motor;
- };
-
- class NormalizedInput : public Input
- {
- public:
- NormalizedInput(const char* name, const float* state) : m_name{std::move(name)}, m_state{*state}
- {
- }
-
- std::string GetName() const override { return std::string{m_name}; }
- ControlState GetState() const override { return m_state; }
-
- private:
- const char* const m_name;
- const float& m_state;
- };
-
- template
- class NonDetectableDirectionalInput : public Input
- {
- public:
- NonDetectableDirectionalInput(const char* name, const float* state)
- : m_name{std::move(name)}, m_state{*state}
- {
- }
-
- std::string GetName() const override { return std::string{m_name} + (Scale > 0 ? '+' : '-'); }
- bool IsDetectable() const override { return false; }
- ControlState GetState() const override { return m_state * Scale; }
-
- private:
- const char* const m_name;
- const float& m_state;
- };
-
- class MotionInput : public Input
- {
- public:
- MotionInput(std::string name, SDL_GameController* gc, SDL_SensorType type, int index,
- ControlState scale)
- : m_name(std::move(name)), m_gc(gc), m_type(type), m_index(index), m_scale(scale)
- {
- }
-
- std::string GetName() const override { return m_name; }
- bool IsDetectable() const override { return false; }
- ControlState GetState() const override;
-
- private:
- std::string m_name;
-
- SDL_GameController* const m_gc;
- SDL_SensorType const m_type;
- int const m_index;
-
- ControlState const m_scale;
- };
-
-public:
- GameController(SDL_GameController* const gamecontroller, SDL_Joystick* const joystick);
- ~GameController();
-
- std::string GetName() const override;
- std::string GetSource() const override;
- int GetSDLInstanceID() const;
- Core::DeviceRemoval UpdateInput() override
- {
- m_battery_value = GetBatteryValueFromSDLPowerLevel(SDL_JoystickCurrentPowerLevel(m_joystick));
-
- // We only support one touchpad and one finger.
- const int touchpad_index = 0;
- const int finger_index = 0;
-
- Uint8 state = 0;
- SDL_GameControllerGetTouchpadFinger(m_gamecontroller, touchpad_index, finger_index, &state,
- &m_touchpad_x, &m_touchpad_y, &m_touchpad_pressure);
- m_touchpad_x = m_touchpad_x * 2 - 1;
- m_touchpad_y = m_touchpad_y * 2 - 1;
-
- return Core::DeviceRemoval::Keep;
- }
-
-private:
- void UpdateRumble()
- {
- SDL_GameControllerRumble(m_gamecontroller, m_low_freq_rumble, m_high_freq_rumble,
- RUMBLE_LENGTH_MS);
- }
-
- void UpdateRumbleTriggers()
- {
- SDL_GameControllerRumbleTriggers(m_gamecontroller, m_trigger_l_rumble, m_trigger_r_rumble,
- RUMBLE_LENGTH_MS);
- }
-
- Uint16 m_low_freq_rumble = 0;
- Uint16 m_high_freq_rumble = 0;
-
- Uint16 m_trigger_l_rumble = 0;
- Uint16 m_trigger_r_rumble = 0;
-
- SDL_GameController* const m_gamecontroller;
- std::string m_name;
- SDL_Joystick* const m_joystick;
- SDL_Haptic* m_haptic = nullptr;
- ControlState m_battery_value;
- float m_touchpad_x = 0.f;
- float m_touchpad_y = 0.f;
- float m_touchpad_pressure = 0.f;
-};
-
class InputBackend final : public ciface::InputBackend
{
public:
@@ -443,60 +46,6 @@ std::unique_ptr CreateInputBackend(ControllerInterface* co
return std::make_unique(controller_interface);
}
-void InputBackend::OpenAndAddDevice(int index)
-{
- SDL_GameController* gc = SDL_GameControllerOpen(index);
- SDL_Joystick* js = SDL_JoystickOpen(index);
-
- if (js)
- {
- if (SDL_JoystickNumButtons(js) > 255 || SDL_JoystickNumAxes(js) > 255 ||
- SDL_JoystickNumHats(js) > 255 || SDL_JoystickNumBalls(js) > 255)
- {
- // This device is invalid, don't use it
- // Some crazy devices (HP webcam 2100) end up as HID devices
- // SDL tries parsing these as Joysticks
- return;
- }
- auto gamecontroller = std::make_shared(gc, js);
- if (!gamecontroller->Inputs().empty() || !gamecontroller->Outputs().empty())
- GetControllerInterface().AddDevice(std::move(gamecontroller));
- }
-}
-
-bool InputBackend::HandleEventAndContinue(const SDL_Event& e)
-{
- if (e.type == SDL_JOYDEVICEADDED)
- {
- // NOTE: SDL_JOYDEVICEADDED's `jdevice.which` is a device index in SDL2.
- // It will change to an "instance ID" in SDL3.
- // OpenAndAddDevice impl and calls will need refactoring when changing to SDL3.
- static_assert(!SDL_VERSION_ATLEAST(3, 0, 0), "Refactoring is needed for SDL3.");
- OpenAndAddDevice(e.jdevice.which);
- }
- else if (e.type == SDL_JOYDEVICEREMOVED)
- {
- // NOTE: SDL_JOYDEVICEREMOVED's `jdevice.which` is an "instance ID".
- GetControllerInterface().RemoveDevice([&e](const auto* device) {
- return device->GetSource() == "SDL" &&
- static_cast(device)->GetSDLInstanceID() == e.jdevice.which;
- });
- }
- else if (e.type == m_populate_event_type)
- {
- GetControllerInterface().PlatformPopulateDevices([this] {
- for (int i = 0; i < SDL_NumJoysticks(); ++i)
- OpenAndAddDevice(i);
- });
- }
- else if (e.type == m_stop_event_type)
- {
- return false;
- }
-
- return true;
-}
-
static void EnableSDLLogging()
{
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
@@ -672,565 +221,63 @@ void InputBackend::PopulateDevices()
SDL_PushEvent(&populate_event);
}
-struct SDLMotionAxis
-{
- std::string_view name;
- int index;
- ControlState scale;
-};
-using SDLMotionAxisList = std::array;
-
-// clang-format off
-static constexpr std::array s_sdl_button_names = {
- "Button S", // SDL_CONTROLLER_BUTTON_A
- "Button E", // SDL_CONTROLLER_BUTTON_B
- "Button W", // SDL_CONTROLLER_BUTTON_X
- "Button N", // SDL_CONTROLLER_BUTTON_Y
- "Back", // SDL_CONTROLLER_BUTTON_BACK
- "Guide", // SDL_CONTROLLER_BUTTON_GUIDE
- "Start", // SDL_CONTROLLER_BUTTON_START
- "Thumb L", // SDL_CONTROLLER_BUTTON_LEFTSTICK
- "Thumb R", // SDL_CONTROLLER_BUTTON_RIGHTSTICK
- "Shoulder L", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
- "Shoulder R", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
- "Pad N", // SDL_CONTROLLER_BUTTON_DPAD_UP
- "Pad S", // SDL_CONTROLLER_BUTTON_DPAD_DOWN
- "Pad W", // SDL_CONTROLLER_BUTTON_DPAD_LEFT
- "Pad E", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
- "Misc 1", // SDL_CONTROLLER_BUTTON_MISC1
- "Paddle 1", // SDL_CONTROLLER_BUTTON_PADDLE1
- "Paddle 2", // SDL_CONTROLLER_BUTTON_PADDLE2
- "Paddle 3", // SDL_CONTROLLER_BUTTON_PADDLE3
- "Paddle 4", // SDL_CONTROLLER_BUTTON_PADDLE4
- "Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
-};
-static constexpr std::array s_sdl_axis_names = {
- "Left X", // SDL_CONTROLLER_AXIS_LEFTX
- "Left Y", // SDL_CONTROLLER_AXIS_LEFTY
- "Right X", // SDL_CONTROLLER_AXIS_RIGHTX
- "Right Y", // SDL_CONTROLLER_AXIS_RIGHTY
- "Trigger L", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
- "Trigger R", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
-};
-static constexpr SDLMotionAxisList SDL_AXES_ACCELEROMETER = {{
- {"Up", 1, 1}, {"Down", 1, -1},
- {"Left", 0, -1}, {"Right", 0, 1},
- {"Forward", 2, -1}, {"Backward", 2, 1},
-}};
-static constexpr SDLMotionAxisList SDL_AXES_GYRO = {{
- {"Pitch Up", 0, 1}, {"Pitch Down", 0, -1},
- {"Roll Left", 2, 1}, {"Roll Right", 2, -1},
- {"Yaw Left", 1, 1}, {"Yaw Right", 1, -1},
-}};
-// clang-format on
-
-GameController::GameController(SDL_GameController* const gamecontroller,
- SDL_Joystick* const joystick)
- : m_gamecontroller(gamecontroller), m_joystick(joystick)
-{
- const char* name;
- if (gamecontroller)
- name = SDL_GameControllerName(gamecontroller);
- else
- name = SDL_JoystickName(joystick);
- m_name = name != nullptr ? name : "Unknown";
-
- // If a Joystick input has a GameController equivalent button/hat we don't add it.
- // "Equivalent" axes are still added as hidden/undetectable inputs to handle
- // loading of existing configs which may use "full surface" inputs.
- // Otherwise handling those would require dealing with gamepad specific quirks.
- std::unordered_set registered_buttons;
- std::unordered_set registered_hats;
- std::unordered_set registered_axes;
- const auto register_mapping = [&](const SDL_GameControllerButtonBind& bind) {
- switch (bind.bindType)
- {
- case SDL_CONTROLLER_BINDTYPE_BUTTON:
- registered_buttons.insert(bind.value.button);
- break;
- case SDL_CONTROLLER_BINDTYPE_HAT:
- registered_hats.insert(bind.value.hat.hat);
- break;
- case SDL_CONTROLLER_BINDTYPE_AXIS:
- registered_axes.insert(bind.value.axis);
- break;
- default:
- break;
- }
- };
-
- if (gamecontroller != nullptr)
- {
- // Inputs
-
- // Buttons
- for (u8 i = 0; i != size(s_sdl_button_names); ++i)
- {
- SDL_GameControllerButton button = static_cast(i);
- if (SDL_GameControllerHasButton(m_gamecontroller, button))
- {
- AddInput(new Button(gamecontroller, button));
-
- register_mapping(SDL_GameControllerGetBindForButton(gamecontroller, button));
- }
- }
-
- // Axes
- for (u8 i = 0; i != size(s_sdl_axis_names); ++i)
- {
- SDL_GameControllerAxis axis = static_cast(i);
- if (SDL_GameControllerHasAxis(m_gamecontroller, axis))
- {
- if (IsTriggerAxis(axis))
- {
- AddInput(new Axis(m_gamecontroller, 32767, axis));
- }
- else
- {
- // Each axis gets a negative and a positive input instance associated with it
- AddInput(new Axis(m_gamecontroller, -32768, axis));
- AddInput(new Axis(m_gamecontroller, 32767, axis));
- }
-
- register_mapping(SDL_GameControllerGetBindForAxis(gamecontroller, axis));
- }
- }
-
- // Rumble
- if (SDL_GameControllerHasRumble(m_gamecontroller))
- {
- AddOutput(new CombinedMotor(*this, &m_low_freq_rumble, &m_high_freq_rumble));
- AddOutput(new Rumble("Motor L", *this, &m_low_freq_rumble, &GameController::UpdateRumble));
- AddOutput(new Rumble("Motor R", *this, &m_high_freq_rumble, &GameController::UpdateRumble));
- }
- if (SDL_GameControllerHasRumbleTriggers(m_gamecontroller))
- {
- AddOutput(new Rumble("Trigger L", *this, &m_trigger_l_rumble,
- &GameController::UpdateRumbleTriggers));
- AddOutput(new Rumble("Trigger R", *this, &m_trigger_r_rumble,
- &GameController::UpdateRumbleTriggers));
- }
-
- // Touchpad
- if (SDL_GameControllerGetNumTouchpads(m_gamecontroller) > 0)
- {
- const char* const name_x = "Touchpad X";
- AddInput(new NonDetectableDirectionalInput<-1>(name_x, &m_touchpad_x));
- AddInput(new NonDetectableDirectionalInput<+1>(name_x, &m_touchpad_x));
- const char* const name_y = "Touchpad Y";
- AddInput(new NonDetectableDirectionalInput<-1>(name_y, &m_touchpad_y));
- AddInput(new NonDetectableDirectionalInput<+1>(name_y, &m_touchpad_y));
- AddInput(new NormalizedInput("Touchpad Pressure", &m_touchpad_pressure));
- }
-
- // Motion
- const auto add_sensor = [this](SDL_SensorType type, std::string_view sensor_name,
- const SDLMotionAxisList& axes) {
- if (SDL_GameControllerSetSensorEnabled(m_gamecontroller, type, SDL_TRUE) == 0)
- {
- for (const SDLMotionAxis& axis : axes)
- {
- AddInput(new MotionInput(fmt::format("{} {}", sensor_name, axis.name), m_gamecontroller,
- type, axis.index, axis.scale));
- }
- }
- };
-
- add_sensor(SDL_SENSOR_ACCEL, "Accel", SDL_AXES_ACCELEROMETER);
- add_sensor(SDL_SENSOR_GYRO, "Gyro", SDL_AXES_GYRO);
- add_sensor(SDL_SENSOR_ACCEL_L, "Accel L", SDL_AXES_ACCELEROMETER);
- add_sensor(SDL_SENSOR_GYRO_L, "Gyro L", SDL_AXES_GYRO);
- add_sensor(SDL_SENSOR_ACCEL_R, "Accel R", SDL_AXES_ACCELEROMETER);
- add_sensor(SDL_SENSOR_GYRO_R, "Gyro R", SDL_AXES_GYRO);
- }
-
- // Legacy inputs
-
- // Buttons
- int n_legacy_buttons = SDL_JoystickNumButtons(joystick);
- if (n_legacy_buttons < 0)
- {
- ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumButtons(): {}", SDL_GetError());
- n_legacy_buttons = 0;
- }
- for (int i = 0; i != n_legacy_buttons; ++i)
- {
- if (registered_buttons.contains(i))
- continue;
-
- AddInput(new LegacyButton(m_joystick, i));
- }
-
- // Axes
- int n_legacy_axes = SDL_JoystickNumAxes(joystick);
- if (n_legacy_axes < 0)
- {
- ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumAxes(): {}", SDL_GetError());
- n_legacy_axes = 0;
- }
- for (int i = 0; i != n_legacy_axes; ++i)
- {
- const bool is_registered = registered_axes.contains(i);
-
- // each axis gets a negative and a positive input instance associated with it
- AddFullAnalogSurfaceInputs(new LegacyAxis(m_joystick, i, -32768, is_registered),
- new LegacyAxis(m_joystick, i, 32767, is_registered));
- }
-
- // Hats
- int n_legacy_hats = SDL_JoystickNumHats(joystick);
- if (n_legacy_hats < 0)
- {
- ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumHats(): {}", SDL_GetError());
- n_legacy_hats = 0;
- }
- for (int i = 0; i != n_legacy_hats; ++i)
- {
- if (registered_hats.contains(i))
- continue;
-
- // each hat gets 4 input instances associated with it, (up down left right)
- for (u8 d = 0; d != 4; ++d)
- AddInput(new LegacyHat(m_joystick, i, d));
- }
-
- // Haptics
- if (SDL_JoystickIsHaptic(m_joystick))
- {
- m_haptic = SDL_HapticOpenFromJoystick(m_joystick);
- if (m_haptic)
- {
- const unsigned int supported_effects = SDL_HapticQuery(m_haptic);
-
- // Disable autocenter:
- if (supported_effects & SDL_HAPTIC_AUTOCENTER)
- SDL_HapticSetAutocenter(m_haptic, 0);
-
- // Constant
- if (supported_effects & SDL_HAPTIC_CONSTANT)
- AddOutput(new ConstantEffect(m_haptic));
-
- // Ramp
- if (supported_effects & SDL_HAPTIC_RAMP)
- AddOutput(new RampEffect(m_haptic));
-
- // Periodic
- for (auto waveform :
- {SDL_HAPTIC_SINE, SDL_HAPTIC_TRIANGLE, SDL_HAPTIC_SAWTOOTHUP, SDL_HAPTIC_SAWTOOTHDOWN})
- {
- if (supported_effects & waveform)
- AddOutput(new PeriodicEffect(m_haptic, waveform));
- }
-
- // LeftRight
- if (supported_effects & SDL_HAPTIC_LEFTRIGHT)
- {
- AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong));
- AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak));
- }
- }
- }
-
- // Needed to make the below power level not "UNKNOWN".
- SDL_JoystickUpdate();
-
- // Battery
- if (SDL_JoystickPowerLevel const power_level = SDL_JoystickCurrentPowerLevel(m_joystick);
- power_level != SDL_JOYSTICK_POWER_UNKNOWN)
- {
- m_battery_value = GetBatteryValueFromSDLPowerLevel(power_level);
- AddInput(new BatteryInput{&m_battery_value});
- }
-}
-
-GameController::~GameController()
-{
- if (m_haptic)
- {
- // stop/destroy all effects
- SDL_HapticStopAll(m_haptic);
- // close haptic before joystick
- SDL_HapticClose(m_haptic);
- m_haptic = nullptr;
- }
- if (m_gamecontroller)
- {
- // stop all rumble
- SDL_GameControllerRumble(m_gamecontroller, 0, 0, 0);
- // close game controller
- SDL_GameControllerClose(m_gamecontroller);
- }
- // close joystick
- SDL_JoystickClose(m_joystick);
-}
-
void InputBackend::UpdateInput(std::vector>& devices_to_remove)
{
SDL_GameControllerUpdate();
}
-std::string GameController::GetName() const
+void InputBackend::OpenAndAddDevice(int index)
{
- return m_name;
-}
+ SDL_GameController* gc = SDL_GameControllerOpen(index);
+ SDL_Joystick* js = SDL_JoystickOpen(index);
-std::string GameController::GetSource() const
-{
- return "SDL";
-}
-
-int GameController::GetSDLInstanceID() const
-{
- return SDL_JoystickInstanceID(m_joystick);
-}
-
-std::string GameController::Button::GetName() const
-{
- return s_sdl_button_names[m_button];
-}
-
-std::string GameController::Axis::GetName() const
-{
- if (IsTriggerAxis(m_axis))
- return std::string(s_sdl_axis_names[m_axis]);
-
- bool negative = m_range < 0;
-
- // Respect XInput: the vertical axes are inverted on SDL
- if (m_axis % 2 == 1)
- negative = !negative;
-
- return std::string(s_sdl_axis_names[m_axis]) + (negative ? '-' : '+');
-}
-
-ControlState GameController::Button::GetState() const
-{
- return SDL_GameControllerGetButton(m_gc, m_button);
-}
-
-ControlState GameController::Axis::GetState() const
-{
- return ControlState(SDL_GameControllerGetAxis(m_gc, m_axis)) / m_range;
-}
-
-bool GameController::Button::IsMatchingName(std::string_view name) const
-{
- if (GetName() == name)
- return true;
-
- // So that SDL can be a superset of XInput
- if (name == "Button A")
- return GetName() == "Button S";
- if (name == "Button B")
- return GetName() == "Button E";
- if (name == "Button X")
- return GetName() == "Button W";
- if (name == "Button Y")
- return GetName() == "Button N";
-
- // Match legacy names.
- const auto bind = SDL_GameControllerGetBindForButton(m_gc, m_button);
- switch (bind.bindType)
+ if (js)
+ {
+ if (SDL_JoystickNumButtons(js) > 255 || SDL_JoystickNumAxes(js) > 255 ||
+ SDL_JoystickNumHats(js) > 255 || SDL_JoystickNumBalls(js) > 255)
+ {
+ // This device is invalid, don't use it
+ // Some crazy devices (HP webcam 2100) end up as HID devices
+ // SDL tries parsing these as Joysticks
+ return;
+ }
+ auto gamecontroller = std::make_shared(gc, js);
+ if (!gamecontroller->Inputs().empty() || !gamecontroller->Outputs().empty())
+ GetControllerInterface().AddDevice(std::move(gamecontroller));
+ }
+}
+
+bool InputBackend::HandleEventAndContinue(const SDL_Event& e)
+{
+ if (e.type == SDL_JOYDEVICEADDED)
+ {
+ // NOTE: SDL_JOYDEVICEADDED's `jdevice.which` is a device index in SDL2.
+ // It will change to an "instance ID" in SDL3.
+ // OpenAndAddDevice impl and calls will need refactoring when changing to SDL3.
+ static_assert(!SDL_VERSION_ATLEAST(3, 0, 0), "Refactoring is needed for SDL3.");
+ OpenAndAddDevice(e.jdevice.which);
+ }
+ else if (e.type == SDL_JOYDEVICEREMOVED)
+ {
+ // NOTE: SDL_JOYDEVICEREMOVED's `jdevice.which` is an "instance ID".
+ GetControllerInterface().RemoveDevice([&e](const auto* device) {
+ return device->GetSource() == "SDL" &&
+ static_cast(device)->GetSDLInstanceID() == e.jdevice.which;
+ });
+ }
+ else if (e.type == m_populate_event_type)
+ {
+ GetControllerInterface().PlatformPopulateDevices([this] {
+ for (int i = 0; i < SDL_NumJoysticks(); ++i)
+ OpenAndAddDevice(i);
+ });
+ }
+ else if (e.type == m_stop_event_type)
{
- case SDL_CONTROLLER_BINDTYPE_BUTTON:
- return name == GetLegacyButtonName(bind.value.button);
- case SDL_CONTROLLER_BINDTYPE_HAT:
- return name == GetLegacyHatName(bind.value.hat.hat,
- GetDirectionFromHatMask(u8(bind.value.hat.hat_mask)));
- default:
return false;
}
+
+ return true;
}
-ControlState GameController::MotionInput::GetState() const
-{
- std::array data{};
- SDL_GameControllerGetSensorData(m_gc, m_type, data.data(), (int)data.size());
- return m_scale * data[m_index];
-}
-
-// Legacy input
-ControlState GameController::LegacyButton::GetState() const
-{
- return SDL_JoystickGetButton(m_js, m_index);
-}
-
-ControlState GameController::LegacyAxis::GetState() const
-{
- return ControlState(SDL_JoystickGetAxis(m_js, m_index)) / m_range;
-}
-
-ControlState GameController::LegacyHat::GetState() const
-{
- return (SDL_JoystickGetHat(m_js, m_index) & (1 << m_direction)) > 0;
-}
-
-void GameController::HapticEffect::UpdateEffect()
-{
- if (m_effect.type != DISABLED_EFFECT_TYPE)
- {
- if (m_id < 0)
- {
- // Upload and try to play the effect.
- m_id = SDL_HapticNewEffect(m_haptic, &m_effect);
-
- if (m_id >= 0)
- SDL_HapticRunEffect(m_haptic, m_id, 1);
- }
- else
- {
- // Effect is already playing. Update parameters.
- SDL_HapticUpdateEffect(m_haptic, m_id, &m_effect);
- }
- }
- else if (m_id >= 0)
- {
- // Stop and remove the effect.
- SDL_HapticStopEffect(m_haptic, m_id);
- SDL_HapticDestroyEffect(m_haptic, m_id);
- m_id = -1;
- }
-}
-
-GameController::HapticEffect::HapticEffect(SDL_Haptic* haptic) : m_haptic(haptic)
-{
- // FYI: type is set within UpdateParameters.
- m_effect.type = DISABLED_EFFECT_TYPE;
-}
-
-GameController::HapticEffect::~HapticEffect()
-{
- m_effect.type = DISABLED_EFFECT_TYPE;
- UpdateEffect();
-}
-
-void GameController::HapticEffect::SetDirection(SDL_HapticDirection* dir)
-{
- // Left direction (for wheels)
- dir->type = SDL_HAPTIC_CARTESIAN;
- dir->dir[0] = -1;
-}
-
-GameController::ConstantEffect::ConstantEffect(SDL_Haptic* haptic) : HapticEffect(haptic)
-{
- m_effect.constant = {};
- SetDirection(&m_effect.constant.direction);
- m_effect.constant.length = RUMBLE_LENGTH_MS;
-}
-
-GameController::RampEffect::RampEffect(SDL_Haptic* haptic) : HapticEffect(haptic)
-{
- m_effect.ramp = {};
- SetDirection(&m_effect.ramp.direction);
- m_effect.ramp.length = RUMBLE_LENGTH_MS;
-}
-
-GameController::PeriodicEffect::PeriodicEffect(SDL_Haptic* haptic, u16 waveform)
- : HapticEffect(haptic), m_waveform(waveform)
-{
- m_effect.periodic = {};
- SetDirection(&m_effect.periodic.direction);
- m_effect.periodic.length = RUMBLE_LENGTH_MS;
- m_effect.periodic.period = RUMBLE_PERIOD_MS;
- m_effect.periodic.offset = 0;
- m_effect.periodic.phase = 0;
-}
-
-GameController::LeftRightEffect::LeftRightEffect(SDL_Haptic* haptic, Motor motor)
- : HapticEffect(haptic), m_motor(motor)
-{
- m_effect.leftright = {};
- m_effect.leftright.length = RUMBLE_LENGTH_MS;
-}
-
-std::string GameController::ConstantEffect::GetName() const
-{
- return "Constant";
-}
-
-std::string GameController::RampEffect::GetName() const
-{
- return "Ramp";
-}
-
-std::string GameController::PeriodicEffect::GetName() const
-{
- switch (m_waveform)
- {
- case SDL_HAPTIC_SINE:
- return "Sine";
- case SDL_HAPTIC_TRIANGLE:
- return "Triangle";
- case SDL_HAPTIC_SAWTOOTHUP:
- return "Sawtooth Up";
- case SDL_HAPTIC_SAWTOOTHDOWN:
- return "Sawtooth Down";
- default:
- return "Unknown";
- }
-}
-
-std::string GameController::LeftRightEffect::GetName() const
-{
- return (Motor::Strong == m_motor) ? "Strong" : "Weak";
-}
-
-void GameController::HapticEffect::SetState(ControlState state)
-{
- // Maximum force value for all SDL effects:
- constexpr s16 MAX_FORCE_VALUE = 0x7fff;
-
- if (UpdateParameters(s16(state * MAX_FORCE_VALUE)))
- {
- UpdateEffect();
- }
-}
-
-bool GameController::ConstantEffect::UpdateParameters(s16 value)
-{
- s16& level = m_effect.constant.level;
- const s16 old_level = level;
-
- level = value;
-
- m_effect.type = level ? SDL_HAPTIC_CONSTANT : DISABLED_EFFECT_TYPE;
- return level != old_level;
-}
-
-bool GameController::RampEffect::UpdateParameters(s16 value)
-{
- s16& level = m_effect.ramp.start;
- const s16 old_level = level;
-
- level = value;
- // FYI: Setting end to same as start is odd,
- // but so is using Ramp effects for rumble simulation.
- m_effect.ramp.end = level;
-
- m_effect.type = level ? SDL_HAPTIC_RAMP : DISABLED_EFFECT_TYPE;
- return level != old_level;
-}
-
-bool GameController::PeriodicEffect::UpdateParameters(s16 value)
-{
- s16& level = m_effect.periodic.magnitude;
- const s16 old_level = level;
-
- level = value;
-
- m_effect.type = level ? m_waveform : DISABLED_EFFECT_TYPE;
- return level != old_level;
-}
-
-bool GameController::LeftRightEffect::UpdateParameters(s16 value)
-{
- u16& level = (Motor::Strong == m_motor) ? m_effect.leftright.large_magnitude :
- m_effect.leftright.small_magnitude;
- const u16 old_level = level;
-
- level = value;
-
- m_effect.type = level ? SDL_HAPTIC_LEFTRIGHT : DISABLED_EFFECT_TYPE;
- return level != old_level;
-}
} // namespace ciface::SDL
diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp
new file mode 100644
index 0000000000..78df8f7ed7
--- /dev/null
+++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.cpp
@@ -0,0 +1,524 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "InputCommon/ControllerInterface/SDL/SDLGamepad.h"
+
+#include
+#include
+
+#include "Common/Logging/Log.h"
+
+namespace ciface::SDL
+{
+
+bool IsTriggerAxis(int index)
+{
+ // First 4 axes are for the analog sticks, the rest are for the triggers
+ return index >= 4;
+}
+
+GameController::GameController(SDL_GameController* const gamecontroller,
+ SDL_Joystick* const joystick)
+ : m_gamecontroller(gamecontroller), m_joystick(joystick)
+{
+ const char* name;
+ if (gamecontroller)
+ name = SDL_GameControllerName(gamecontroller);
+ else
+ name = SDL_JoystickName(joystick);
+ m_name = name != nullptr ? name : "Unknown";
+
+ // If a Joystick input has a GameController equivalent button/hat we don't add it.
+ // "Equivalent" axes are still added as hidden/undetectable inputs to handle
+ // loading of existing configs which may use "full surface" inputs.
+ // Otherwise handling those would require dealing with gamepad specific quirks.
+ std::unordered_set registered_buttons;
+ std::unordered_set registered_hats;
+ std::unordered_set registered_axes;
+ const auto register_mapping = [&](const SDL_GameControllerButtonBind& bind) {
+ switch (bind.bindType)
+ {
+ case SDL_CONTROLLER_BINDTYPE_BUTTON:
+ registered_buttons.insert(bind.value.button);
+ break;
+ case SDL_CONTROLLER_BINDTYPE_HAT:
+ registered_hats.insert(bind.value.hat.hat);
+ break;
+ case SDL_CONTROLLER_BINDTYPE_AXIS:
+ registered_axes.insert(bind.value.axis);
+ break;
+ default:
+ break;
+ }
+ };
+
+ if (gamecontroller != nullptr)
+ {
+ // Inputs
+
+ // Buttons
+ for (u8 i = 0; i != size(s_sdl_button_names); ++i)
+ {
+ SDL_GameControllerButton button = static_cast(i);
+ if (SDL_GameControllerHasButton(m_gamecontroller, button))
+ {
+ AddInput(new Button(gamecontroller, button));
+
+ register_mapping(SDL_GameControllerGetBindForButton(gamecontroller, button));
+ }
+ }
+
+ // Axes
+ for (u8 i = 0; i != size(s_sdl_axis_names); ++i)
+ {
+ SDL_GameControllerAxis axis = static_cast(i);
+ if (SDL_GameControllerHasAxis(m_gamecontroller, axis))
+ {
+ if (IsTriggerAxis(axis))
+ {
+ AddInput(new Axis(m_gamecontroller, 32767, axis));
+ }
+ else
+ {
+ // Each axis gets a negative and a positive input instance associated with it
+ AddInput(new Axis(m_gamecontroller, -32768, axis));
+ AddInput(new Axis(m_gamecontroller, 32767, axis));
+ }
+
+ register_mapping(SDL_GameControllerGetBindForAxis(gamecontroller, axis));
+ }
+ }
+
+ // Rumble
+ if (SDL_GameControllerHasRumble(m_gamecontroller))
+ {
+ AddOutput(new CombinedMotor(*this, &m_low_freq_rumble, &m_high_freq_rumble));
+ AddOutput(new Rumble("Motor L", *this, &m_low_freq_rumble, &GameController::UpdateRumble));
+ AddOutput(new Rumble("Motor R", *this, &m_high_freq_rumble, &GameController::UpdateRumble));
+ }
+ if (SDL_GameControllerHasRumbleTriggers(m_gamecontroller))
+ {
+ AddOutput(new Rumble("Trigger L", *this, &m_trigger_l_rumble,
+ &GameController::UpdateRumbleTriggers));
+ AddOutput(new Rumble("Trigger R", *this, &m_trigger_r_rumble,
+ &GameController::UpdateRumbleTriggers));
+ }
+
+ // Touchpad
+ if (SDL_GameControllerGetNumTouchpads(m_gamecontroller) > 0)
+ {
+ const char* const name_x = "Touchpad X";
+ AddInput(new NonDetectableDirectionalInput<-1>(name_x, &m_touchpad_x));
+ AddInput(new NonDetectableDirectionalInput<+1>(name_x, &m_touchpad_x));
+ const char* const name_y = "Touchpad Y";
+ AddInput(new NonDetectableDirectionalInput<-1>(name_y, &m_touchpad_y));
+ AddInput(new NonDetectableDirectionalInput<+1>(name_y, &m_touchpad_y));
+ AddInput(new NormalizedInput("Touchpad Pressure", &m_touchpad_pressure));
+ }
+
+ // Motion
+ const auto add_sensor = [this](SDL_SensorType type, std::string_view sensor_name,
+ const SDLMotionAxisList& axes) {
+ if (SDL_GameControllerSetSensorEnabled(m_gamecontroller, type, SDL_TRUE) == 0)
+ {
+ for (const SDLMotionAxis& axis : axes)
+ {
+ AddInput(new MotionInput(fmt::format("{} {}", sensor_name, axis.name), m_gamecontroller,
+ type, axis.index, axis.scale));
+ }
+ }
+ };
+
+ add_sensor(SDL_SENSOR_ACCEL, "Accel", SDL_AXES_ACCELEROMETER);
+ add_sensor(SDL_SENSOR_GYRO, "Gyro", SDL_AXES_GYRO);
+ add_sensor(SDL_SENSOR_ACCEL_L, "Accel L", SDL_AXES_ACCELEROMETER);
+ add_sensor(SDL_SENSOR_GYRO_L, "Gyro L", SDL_AXES_GYRO);
+ add_sensor(SDL_SENSOR_ACCEL_R, "Accel R", SDL_AXES_ACCELEROMETER);
+ add_sensor(SDL_SENSOR_GYRO_R, "Gyro R", SDL_AXES_GYRO);
+ }
+
+ // Legacy inputs
+
+ // Buttons
+ int n_legacy_buttons = SDL_JoystickNumButtons(joystick);
+ if (n_legacy_buttons < 0)
+ {
+ ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumButtons(): {}", SDL_GetError());
+ n_legacy_buttons = 0;
+ }
+ for (int i = 0; i != n_legacy_buttons; ++i)
+ {
+ if (registered_buttons.contains(i))
+ continue;
+
+ AddInput(new LegacyButton(m_joystick, i));
+ }
+
+ // Axes
+ int n_legacy_axes = SDL_JoystickNumAxes(joystick);
+ if (n_legacy_axes < 0)
+ {
+ ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumAxes(): {}", SDL_GetError());
+ n_legacy_axes = 0;
+ }
+ for (int i = 0; i != n_legacy_axes; ++i)
+ {
+ const bool is_registered = registered_axes.contains(i);
+
+ // each axis gets a negative and a positive input instance associated with it
+ AddFullAnalogSurfaceInputs(new LegacyAxis(m_joystick, i, -32768, is_registered),
+ new LegacyAxis(m_joystick, i, 32767, is_registered));
+ }
+
+ // Hats
+ int n_legacy_hats = SDL_JoystickNumHats(joystick);
+ if (n_legacy_hats < 0)
+ {
+ ERROR_LOG_FMT(CONTROLLERINTERFACE, "Error in SDL_JoystickNumHats(): {}", SDL_GetError());
+ n_legacy_hats = 0;
+ }
+ for (int i = 0; i != n_legacy_hats; ++i)
+ {
+ if (registered_hats.contains(i))
+ continue;
+
+ // each hat gets 4 input instances associated with it, (up down left right)
+ for (u8 d = 0; d != 4; ++d)
+ AddInput(new LegacyHat(m_joystick, i, d));
+ }
+
+ // Haptics
+ if (SDL_JoystickIsHaptic(m_joystick))
+ {
+ m_haptic = SDL_HapticOpenFromJoystick(m_joystick);
+ if (m_haptic)
+ {
+ const unsigned int supported_effects = SDL_HapticQuery(m_haptic);
+
+ // Disable autocenter:
+ if (supported_effects & SDL_HAPTIC_AUTOCENTER)
+ SDL_HapticSetAutocenter(m_haptic, 0);
+
+ // Constant
+ if (supported_effects & SDL_HAPTIC_CONSTANT)
+ AddOutput(new ConstantEffect(m_haptic));
+
+ // Ramp
+ if (supported_effects & SDL_HAPTIC_RAMP)
+ AddOutput(new RampEffect(m_haptic));
+
+ // Periodic
+ for (auto waveform :
+ {SDL_HAPTIC_SINE, SDL_HAPTIC_TRIANGLE, SDL_HAPTIC_SAWTOOTHUP, SDL_HAPTIC_SAWTOOTHDOWN})
+ {
+ if (supported_effects & waveform)
+ AddOutput(new PeriodicEffect(m_haptic, waveform));
+ }
+
+ // LeftRight
+ if (supported_effects & SDL_HAPTIC_LEFTRIGHT)
+ {
+ AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Strong));
+ AddOutput(new LeftRightEffect(m_haptic, LeftRightEffect::Motor::Weak));
+ }
+ }
+ }
+
+ // Needed to make the below power level not "UNKNOWN".
+ SDL_JoystickUpdate();
+
+ // Battery
+ if (SDL_JoystickPowerLevel const power_level = SDL_JoystickCurrentPowerLevel(m_joystick);
+ power_level != SDL_JOYSTICK_POWER_UNKNOWN)
+ {
+ m_battery_value = GetBatteryValueFromSDLPowerLevel(power_level);
+ AddInput(new BatteryInput{&m_battery_value});
+ }
+}
+
+GameController::~GameController()
+{
+ if (m_haptic)
+ {
+ // stop/destroy all effects
+ SDL_HapticStopAll(m_haptic);
+ // close haptic before joystick
+ SDL_HapticClose(m_haptic);
+ m_haptic = nullptr;
+ }
+ if (m_gamecontroller)
+ {
+ // stop all rumble
+ SDL_GameControllerRumble(m_gamecontroller, 0, 0, 0);
+ // close game controller
+ SDL_GameControllerClose(m_gamecontroller);
+ }
+ // close joystick
+ SDL_JoystickClose(m_joystick);
+}
+
+std::string GameController::GetName() const
+{
+ return m_name;
+}
+
+std::string GameController::GetSource() const
+{
+ return "SDL";
+}
+
+int GameController::GetSDLInstanceID() const
+{
+ return SDL_JoystickInstanceID(m_joystick);
+}
+
+std::string GameController::Button::GetName() const
+{
+ return s_sdl_button_names[m_button];
+}
+
+std::string GameController::Axis::GetName() const
+{
+ if (IsTriggerAxis(m_axis))
+ return std::string(s_sdl_axis_names[m_axis]);
+
+ bool negative = m_range < 0;
+
+ // Respect XInput: the vertical axes are inverted on SDL
+ if (m_axis % 2 == 1)
+ negative = !negative;
+
+ return std::string(s_sdl_axis_names[m_axis]) + (negative ? '-' : '+');
+}
+
+ControlState GameController::Button::GetState() const
+{
+ return SDL_GameControllerGetButton(m_gc, m_button);
+}
+
+ControlState GameController::Axis::GetState() const
+{
+ return ControlState(SDL_GameControllerGetAxis(m_gc, m_axis)) / m_range;
+}
+
+bool GameController::Button::IsMatchingName(std::string_view name) const
+{
+ if (GetName() == name)
+ return true;
+
+ // So that SDL can be a superset of XInput
+ if (name == "Button A")
+ return GetName() == "Button S";
+ if (name == "Button B")
+ return GetName() == "Button E";
+ if (name == "Button X")
+ return GetName() == "Button W";
+ if (name == "Button Y")
+ return GetName() == "Button N";
+
+ // Match legacy names.
+ const auto bind = SDL_GameControllerGetBindForButton(m_gc, m_button);
+ switch (bind.bindType)
+ {
+ case SDL_CONTROLLER_BINDTYPE_BUTTON:
+ return name == GetLegacyButtonName(bind.value.button);
+ case SDL_CONTROLLER_BINDTYPE_HAT:
+ return name == GetLegacyHatName(bind.value.hat.hat,
+ GetDirectionFromHatMask(u8(bind.value.hat.hat_mask)));
+ default:
+ return false;
+ }
+}
+
+ControlState GameController::MotionInput::GetState() const
+{
+ std::array data{};
+ SDL_GameControllerGetSensorData(m_gc, m_type, data.data(), (int)data.size());
+ return m_scale * data[m_index];
+}
+
+// Legacy input
+ControlState GameController::LegacyButton::GetState() const
+{
+ return SDL_JoystickGetButton(m_js, m_index);
+}
+
+ControlState GameController::LegacyAxis::GetState() const
+{
+ return ControlState(SDL_JoystickGetAxis(m_js, m_index)) / m_range;
+}
+
+ControlState GameController::LegacyHat::GetState() const
+{
+ return (SDL_JoystickGetHat(m_js, m_index) & (1 << m_direction)) > 0;
+}
+
+void GameController::HapticEffect::UpdateEffect()
+{
+ if (m_effect.type != DISABLED_EFFECT_TYPE)
+ {
+ if (m_id < 0)
+ {
+ // Upload and try to play the effect.
+ m_id = SDL_HapticNewEffect(m_haptic, &m_effect);
+
+ if (m_id >= 0)
+ SDL_HapticRunEffect(m_haptic, m_id, 1);
+ }
+ else
+ {
+ // Effect is already playing. Update parameters.
+ SDL_HapticUpdateEffect(m_haptic, m_id, &m_effect);
+ }
+ }
+ else if (m_id >= 0)
+ {
+ // Stop and remove the effect.
+ SDL_HapticStopEffect(m_haptic, m_id);
+ SDL_HapticDestroyEffect(m_haptic, m_id);
+ m_id = -1;
+ }
+}
+
+GameController::HapticEffect::HapticEffect(SDL_Haptic* haptic) : m_haptic(haptic)
+{
+ // FYI: type is set within UpdateParameters.
+ m_effect.type = DISABLED_EFFECT_TYPE;
+}
+
+GameController::HapticEffect::~HapticEffect()
+{
+ m_effect.type = DISABLED_EFFECT_TYPE;
+ UpdateEffect();
+}
+
+void GameController::HapticEffect::SetDirection(SDL_HapticDirection* dir)
+{
+ // Left direction (for wheels)
+ dir->type = SDL_HAPTIC_CARTESIAN;
+ dir->dir[0] = -1;
+}
+
+GameController::ConstantEffect::ConstantEffect(SDL_Haptic* haptic) : HapticEffect(haptic)
+{
+ m_effect.constant = {};
+ SetDirection(&m_effect.constant.direction);
+ m_effect.constant.length = RUMBLE_LENGTH_MS;
+}
+
+GameController::RampEffect::RampEffect(SDL_Haptic* haptic) : HapticEffect(haptic)
+{
+ m_effect.ramp = {};
+ SetDirection(&m_effect.ramp.direction);
+ m_effect.ramp.length = RUMBLE_LENGTH_MS;
+}
+
+GameController::PeriodicEffect::PeriodicEffect(SDL_Haptic* haptic, u16 waveform)
+ : HapticEffect(haptic), m_waveform(waveform)
+{
+ m_effect.periodic = {};
+ SetDirection(&m_effect.periodic.direction);
+ m_effect.periodic.length = RUMBLE_LENGTH_MS;
+ m_effect.periodic.period = RUMBLE_PERIOD_MS;
+ m_effect.periodic.offset = 0;
+ m_effect.periodic.phase = 0;
+}
+
+GameController::LeftRightEffect::LeftRightEffect(SDL_Haptic* haptic, Motor motor)
+ : HapticEffect(haptic), m_motor(motor)
+{
+ m_effect.leftright = {};
+ m_effect.leftright.length = RUMBLE_LENGTH_MS;
+}
+
+std::string GameController::ConstantEffect::GetName() const
+{
+ return "Constant";
+}
+
+std::string GameController::RampEffect::GetName() const
+{
+ return "Ramp";
+}
+
+std::string GameController::PeriodicEffect::GetName() const
+{
+ switch (m_waveform)
+ {
+ case SDL_HAPTIC_SINE:
+ return "Sine";
+ case SDL_HAPTIC_TRIANGLE:
+ return "Triangle";
+ case SDL_HAPTIC_SAWTOOTHUP:
+ return "Sawtooth Up";
+ case SDL_HAPTIC_SAWTOOTHDOWN:
+ return "Sawtooth Down";
+ default:
+ return "Unknown";
+ }
+}
+
+std::string GameController::LeftRightEffect::GetName() const
+{
+ return (Motor::Strong == m_motor) ? "Strong" : "Weak";
+}
+
+void GameController::HapticEffect::SetState(ControlState state)
+{
+ // Maximum force value for all SDL effects:
+ constexpr s16 MAX_FORCE_VALUE = 0x7fff;
+
+ if (UpdateParameters(s16(state * MAX_FORCE_VALUE)))
+ {
+ UpdateEffect();
+ }
+}
+
+bool GameController::ConstantEffect::UpdateParameters(s16 value)
+{
+ s16& level = m_effect.constant.level;
+ const s16 old_level = level;
+
+ level = value;
+
+ m_effect.type = level ? SDL_HAPTIC_CONSTANT : DISABLED_EFFECT_TYPE;
+ return level != old_level;
+}
+
+bool GameController::RampEffect::UpdateParameters(s16 value)
+{
+ s16& level = m_effect.ramp.start;
+ const s16 old_level = level;
+
+ level = value;
+ // FYI: Setting end to same as start is odd,
+ // but so is using Ramp effects for rumble simulation.
+ m_effect.ramp.end = level;
+
+ m_effect.type = level ? SDL_HAPTIC_RAMP : DISABLED_EFFECT_TYPE;
+ return level != old_level;
+}
+
+bool GameController::PeriodicEffect::UpdateParameters(s16 value)
+{
+ s16& level = m_effect.periodic.magnitude;
+ const s16 old_level = level;
+
+ level = value;
+
+ m_effect.type = level ? m_waveform : DISABLED_EFFECT_TYPE;
+ return level != old_level;
+}
+
+bool GameController::LeftRightEffect::UpdateParameters(s16 value)
+{
+ u16& level = (Motor::Strong == m_motor) ? m_effect.leftright.large_magnitude :
+ m_effect.leftright.small_magnitude;
+ const u16 old_level = level;
+
+ level = value;
+
+ m_effect.type = level ? SDL_HAPTIC_LEFTRIGHT : DISABLED_EFFECT_TYPE;
+ return level != old_level;
+}
+} // namespace ciface::SDL
diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h
new file mode 100644
index 0000000000..e71e3f4e52
--- /dev/null
+++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDLGamepad.h
@@ -0,0 +1,458 @@
+// Copyright 2025 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+
+#include
+#include
+
+#include "Common/MathUtil.h"
+
+#include "InputCommon/ControllerInterface/CoreDevice.h"
+
+namespace
+{
+std::string GetLegacyButtonName(int index)
+{
+ return "Button " + std::to_string(index);
+}
+
+std::string GetLegacyAxisName(int index, int range)
+{
+ return "Axis " + std::to_string(index) + (range < 0 ? '-' : '+');
+}
+
+std::string GetLegacyHatName(int index, int direction)
+{
+ return "Hat " + std::to_string(index) + ' ' + "NESW"[direction];
+}
+
+constexpr int GetDirectionFromHatMask(u8 mask)
+{
+ return MathUtil::IntLog2(mask);
+}
+
+static_assert(GetDirectionFromHatMask(SDL_HAT_UP) == 0);
+static_assert(GetDirectionFromHatMask(SDL_HAT_LEFT) == 3);
+
+ControlState GetBatteryValueFromSDLPowerLevel(SDL_JoystickPowerLevel sdl_power_level)
+{
+ // Values come from comments in SDL_joystick.h
+ // A proper percentage will be exposed in SDL3.
+ ControlState result;
+ switch (sdl_power_level)
+ {
+ case SDL_JOYSTICK_POWER_EMPTY:
+ result = 0.025;
+ break;
+ case SDL_JOYSTICK_POWER_LOW:
+ result = 0.125;
+ break;
+ case SDL_JOYSTICK_POWER_MEDIUM:
+ result = 0.45;
+ break;
+ case SDL_JOYSTICK_POWER_FULL:
+ result = 0.85;
+ break;
+ case SDL_JOYSTICK_POWER_WIRED:
+ case SDL_JOYSTICK_POWER_MAX:
+ result = 1.0;
+ break;
+ case SDL_JOYSTICK_POWER_UNKNOWN:
+ default:
+ result = 0.0;
+ break;
+ }
+
+ return result * ciface::BATTERY_INPUT_MAX_VALUE;
+}
+
+} // namespace
+
+namespace ciface::SDL
+{
+
+class GameController : public Core::Device
+{
+private:
+ // GameController inputs
+ class Button : public Core::Device::Input
+ {
+ public:
+ std::string GetName() const override;
+ Button(SDL_GameController* gc, SDL_GameControllerButton button) : m_gc(gc), m_button(button) {}
+ ControlState GetState() const override;
+ bool IsMatchingName(std::string_view name) const override;
+
+ private:
+ SDL_GameController* const m_gc;
+ const SDL_GameControllerButton m_button;
+ };
+
+ class Axis : public Core::Device::Input
+ {
+ public:
+ std::string GetName() const override;
+ Axis(SDL_GameController* gc, Sint16 range, SDL_GameControllerAxis axis)
+ : m_gc(gc), m_range(range), m_axis(axis)
+ {
+ }
+ ControlState GetState() const override;
+
+ private:
+ SDL_GameController* const m_gc;
+ const Sint16 m_range;
+ const SDL_GameControllerAxis m_axis;
+ };
+
+ // Legacy inputs
+ class LegacyButton : public Core::Device::Input
+ {
+ public:
+ std::string GetName() const override { return GetLegacyButtonName(m_index); }
+ LegacyButton(SDL_Joystick* js, int index) : m_js(js), m_index(index) {}
+ ControlState GetState() const override;
+
+ private:
+ SDL_Joystick* const m_js;
+ const int m_index;
+ };
+
+ class LegacyAxis : public Core::Device::Input
+ {
+ public:
+ std::string GetName() const override { return GetLegacyAxisName(m_index, m_range); }
+ LegacyAxis(SDL_Joystick* js, int index, s16 range, bool is_handled_elsewhere)
+ : m_js(js), m_index(index), m_range(range), m_is_handled_elsewhere(is_handled_elsewhere)
+ {
+ }
+ ControlState GetState() const override;
+ bool IsHidden() const override { return m_is_handled_elsewhere; }
+ bool IsDetectable() const override { return !IsHidden(); }
+
+ private:
+ SDL_Joystick* const m_js;
+ const int m_index;
+ const s16 m_range;
+ const bool m_is_handled_elsewhere;
+ };
+
+ class LegacyHat : public Input
+ {
+ public:
+ std::string GetName() const override { return GetLegacyHatName(m_index, m_direction); }
+ LegacyHat(SDL_Joystick* js, int index, u8 direction)
+ : m_js(js), m_index(index), m_direction(direction)
+ {
+ }
+ ControlState GetState() const override;
+
+ private:
+ SDL_Joystick* const m_js;
+ const int m_index;
+ const u8 m_direction;
+ };
+
+ class BatteryInput final : public Input
+ {
+ public:
+ explicit BatteryInput(const ControlState* battery_value) : m_battery_value(*battery_value) {}
+ std::string GetName() const override { return "Battery"; }
+ ControlState GetState() const override { return m_battery_value; }
+ bool IsDetectable() const override { return false; }
+
+ private:
+ const ControlState& m_battery_value;
+ };
+
+ // Rumble
+ class Rumble : public Output
+ {
+ public:
+ using UpdateCallback = void (GameController::*)(void);
+
+ Rumble(const char* name, GameController& gc, Uint16* state, UpdateCallback update_callback)
+ : m_name{name}, m_gc{gc}, m_state{*state}, m_update_callback{update_callback}
+ {
+ }
+ std::string GetName() const override { return m_name; }
+ void SetState(ControlState state) override
+ {
+ const auto new_state = state * std::numeric_limits::max();
+ if (m_state == new_state)
+ return;
+
+ m_state = new_state;
+ (m_gc.*m_update_callback)();
+ }
+
+ private:
+ const char* const m_name;
+ GameController& m_gc;
+ Uint16& m_state;
+ UpdateCallback const m_update_callback;
+ };
+
+ class CombinedMotor : public Output
+ {
+ public:
+ CombinedMotor(GameController& gc, Uint16* low_state, Uint16* high_state)
+ : m_gc{gc}, m_low_state{*low_state}, m_high_state{*high_state}
+ {
+ }
+ std::string GetName() const override { return "Motor"; }
+ void SetState(ControlState state) override
+ {
+ const auto new_state = state * std::numeric_limits::max();
+ if (m_low_state == new_state && m_high_state == new_state)
+ return;
+
+ m_low_state = new_state;
+ m_high_state = new_state;
+ m_gc.UpdateRumble();
+ }
+
+ private:
+ GameController& m_gc;
+ Uint16& m_low_state;
+ Uint16& m_high_state;
+ };
+
+ class HapticEffect : public Output
+ {
+ public:
+ HapticEffect(SDL_Haptic* haptic);
+ ~HapticEffect();
+
+ protected:
+ virtual bool UpdateParameters(s16 value) = 0;
+ static void SetDirection(SDL_HapticDirection* dir);
+
+ SDL_HapticEffect m_effect = {};
+
+ static constexpr u16 DISABLED_EFFECT_TYPE = 0;
+
+ private:
+ virtual void SetState(ControlState state) override final;
+ void UpdateEffect();
+ SDL_Haptic* const m_haptic;
+ int m_id = -1;
+ };
+
+ class ConstantEffect : public HapticEffect
+ {
+ public:
+ ConstantEffect(SDL_Haptic* haptic);
+ std::string GetName() const override;
+
+ private:
+ bool UpdateParameters(s16 value) override;
+ };
+
+ class RampEffect : public HapticEffect
+ {
+ public:
+ RampEffect(SDL_Haptic* haptic);
+ std::string GetName() const override;
+
+ private:
+ bool UpdateParameters(s16 value) override;
+ };
+
+ class PeriodicEffect : public HapticEffect
+ {
+ public:
+ PeriodicEffect(SDL_Haptic* haptic, u16 waveform);
+ std::string GetName() const override;
+
+ private:
+ bool UpdateParameters(s16 value) override;
+
+ const u16 m_waveform;
+ };
+
+ class LeftRightEffect : public HapticEffect
+ {
+ public:
+ enum class Motor : u8
+ {
+ Weak,
+ Strong,
+ };
+
+ LeftRightEffect(SDL_Haptic* haptic, Motor motor);
+ std::string GetName() const override;
+
+ private:
+ bool UpdateParameters(s16 value) override;
+
+ const Motor m_motor;
+ };
+
+ class NormalizedInput : public Input
+ {
+ public:
+ NormalizedInput(const char* name, const float* state) : m_name{std::move(name)}, m_state{*state}
+ {
+ }
+
+ std::string GetName() const override { return std::string{m_name}; }
+ ControlState GetState() const override { return m_state; }
+
+ private:
+ const char* const m_name;
+ const float& m_state;
+ };
+
+ template
+ class NonDetectableDirectionalInput : public Input
+ {
+ public:
+ NonDetectableDirectionalInput(const char* name, const float* state)
+ : m_name{std::move(name)}, m_state{*state}
+ {
+ }
+
+ std::string GetName() const override { return std::string{m_name} + (Scale > 0 ? '+' : '-'); }
+ bool IsDetectable() const override { return false; }
+ ControlState GetState() const override { return m_state * Scale; }
+
+ private:
+ const char* const m_name;
+ const float& m_state;
+ };
+
+ class MotionInput : public Input
+ {
+ public:
+ MotionInput(std::string name, SDL_GameController* gc, SDL_SensorType type, int index,
+ ControlState scale)
+ : m_name(std::move(name)), m_gc(gc), m_type(type), m_index(index), m_scale(scale)
+ {
+ }
+
+ std::string GetName() const override { return m_name; }
+ bool IsDetectable() const override { return false; }
+ ControlState GetState() const override;
+
+ private:
+ std::string m_name;
+
+ SDL_GameController* const m_gc;
+ SDL_SensorType const m_type;
+ int const m_index;
+
+ ControlState const m_scale;
+ };
+
+public:
+ GameController(SDL_GameController* const gamecontroller, SDL_Joystick* const joystick);
+ ~GameController();
+
+ std::string GetName() const override;
+ std::string GetSource() const override;
+ int GetSDLInstanceID() const;
+ Core::DeviceRemoval UpdateInput() override
+ {
+ m_battery_value = GetBatteryValueFromSDLPowerLevel(SDL_JoystickCurrentPowerLevel(m_joystick));
+
+ // We only support one touchpad and one finger.
+ const int touchpad_index = 0;
+ const int finger_index = 0;
+
+ Uint8 state = 0;
+ SDL_GameControllerGetTouchpadFinger(m_gamecontroller, touchpad_index, finger_index, &state,
+ &m_touchpad_x, &m_touchpad_y, &m_touchpad_pressure);
+ m_touchpad_x = m_touchpad_x * 2 - 1;
+ m_touchpad_y = m_touchpad_y * 2 - 1;
+
+ return Core::DeviceRemoval::Keep;
+ }
+
+private:
+ void UpdateRumble()
+ {
+ SDL_GameControllerRumble(m_gamecontroller, m_low_freq_rumble, m_high_freq_rumble,
+ RUMBLE_LENGTH_MS);
+ }
+
+ void UpdateRumbleTriggers()
+ {
+ SDL_GameControllerRumbleTriggers(m_gamecontroller, m_trigger_l_rumble, m_trigger_r_rumble,
+ RUMBLE_LENGTH_MS);
+ }
+
+ Uint16 m_low_freq_rumble = 0;
+ Uint16 m_high_freq_rumble = 0;
+
+ Uint16 m_trigger_l_rumble = 0;
+ Uint16 m_trigger_r_rumble = 0;
+
+ SDL_GameController* const m_gamecontroller;
+ std::string m_name;
+ SDL_Joystick* const m_joystick;
+ SDL_Haptic* m_haptic = nullptr;
+ ControlState m_battery_value;
+ float m_touchpad_x = 0.f;
+ float m_touchpad_y = 0.f;
+ float m_touchpad_pressure = 0.f;
+};
+
+struct SDLMotionAxis
+{
+ std::string_view name;
+ int index;
+ ControlState scale;
+};
+using SDLMotionAxisList = std::array;
+
+static constexpr std::array s_sdl_button_names = {
+ "Button S", // SDL_CONTROLLER_BUTTON_A
+ "Button E", // SDL_CONTROLLER_BUTTON_B
+ "Button W", // SDL_CONTROLLER_BUTTON_X
+ "Button N", // SDL_CONTROLLER_BUTTON_Y
+ "Back", // SDL_CONTROLLER_BUTTON_BACK
+ "Guide", // SDL_CONTROLLER_BUTTON_GUIDE
+ "Start", // SDL_CONTROLLER_BUTTON_START
+ "Thumb L", // SDL_CONTROLLER_BUTTON_LEFTSTICK
+ "Thumb R", // SDL_CONTROLLER_BUTTON_RIGHTSTICK
+ "Shoulder L", // SDL_CONTROLLER_BUTTON_LEFTSHOULDER
+ "Shoulder R", // SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
+ "Pad N", // SDL_CONTROLLER_BUTTON_DPAD_UP
+ "Pad S", // SDL_CONTROLLER_BUTTON_DPAD_DOWN
+ "Pad W", // SDL_CONTROLLER_BUTTON_DPAD_LEFT
+ "Pad E", // SDL_CONTROLLER_BUTTON_DPAD_RIGHT
+ "Misc 1", // SDL_CONTROLLER_BUTTON_MISC1
+ "Paddle 1", // SDL_CONTROLLER_BUTTON_PADDLE1
+ "Paddle 2", // SDL_CONTROLLER_BUTTON_PADDLE2
+ "Paddle 3", // SDL_CONTROLLER_BUTTON_PADDLE3
+ "Paddle 4", // SDL_CONTROLLER_BUTTON_PADDLE4
+ "Touchpad", // SDL_CONTROLLER_BUTTON_TOUCHPAD
+};
+static constexpr std::array s_sdl_axis_names = {
+ "Left X", // SDL_CONTROLLER_AXIS_LEFTX
+ "Left Y", // SDL_CONTROLLER_AXIS_LEFTY
+ "Right X", // SDL_CONTROLLER_AXIS_RIGHTX
+ "Right Y", // SDL_CONTROLLER_AXIS_RIGHTY
+ "Trigger L", // SDL_CONTROLLER_AXIS_TRIGGERLEFT
+ "Trigger R", // SDL_CONTROLLER_AXIS_TRIGGERRIGHT
+};
+static constexpr SDLMotionAxisList SDL_AXES_ACCELEROMETER = {{
+ {"Up", 1, 1},
+ {"Down", 1, -1},
+ {"Left", 0, -1},
+ {"Right", 0, 1},
+ {"Forward", 2, -1},
+ {"Backward", 2, 1},
+}};
+static constexpr SDLMotionAxisList SDL_AXES_GYRO = {{
+ {"Pitch Up", 0, 1},
+ {"Pitch Down", 0, -1},
+ {"Roll Left", 2, 1},
+ {"Roll Right", 2, -1},
+ {"Yaw Left", 1, 1},
+ {"Yaw Right", 1, -1},
+}};
+} // namespace ciface::SDL