eden-miror/src/input_common/drivers/sdl_driver.cpp
John 873fde41d0
[common, controller] [AI] Controller assignment modes, Controllers tab, Steam Gamemode/Gamescope fixes (gyroscope, controller) (#4583)
## Summary

Automatic controller assignment for Eden, aimed at couch co-op and handheld PCs (Steam Deck,
Legion Go, ROG Ally, AYN Odin), plus a set of fixes for running Eden in Steam's Game Mode
(gamescope) and under Steam Input.

## Controller Assignment Handler

Settings > Controls gains a **Controllers** tab. The handler is a segmented choice:

| Handler | Behaviour |
|---|---|
| **Modern Console** | Automatic: each controller that connects becomes the next player (up to 8). |
| **Manual** (default) | Players are set in each player's tab, as before. Its Controller Applet choice: **Disabled**, **Custom Frontend** or **Real Hardware** (the console's own applet, LLE). |

Under Modern Console, **Portable Handheld Gaming Device with Built-in Controller** (on/off)
makes external controllers take the players first, with the handheld's own controls used only
when none is connected.

In the automatic modes:
- Players follow their controllers across reconnects: a controller that leaves keeps its player
  for 45 seconds and takes it back when it returns. Each controller keeps its own mappings when
  it changes player, for the session.
- At most eight players; a ninth controller waits until a player is free.
- The user's saved controller setup is never rewritten; remaps made in the Controls tab last for
  the session, and input profiles are what persist.
- Input profiles can be picked or removed per player, from the Controllers tab or each player tab.
- A controller whose player has no controller mappings (a fresh setup) gets its default mapping.
  A DSU (cemuhook) motion binding from the manual setup is kept, for handhelds whose own gyro
  needs it.

## Gameplay options

- **Pause When a Controller Disconnects** (on by default): the game pauses when a player's
  controller is turned off, or has been gone for 3 seconds, and resumes when a controller takes
  that player again. Pressing + on another controller goes on without it.
- **Use Any Controller as Joy-Cons**: a game that only takes Joy-Cons gets them automatically
  (dual, or a single sideways one with the buttons arranged to fit).
- **Button Layout**: By Position or By Label, for Xbox and PlayStation style controllers.
- **Player Lights**: controllers that have them show their player number.

## Controllers tab and player tabs

- Connected Players: each player with its controller and battery, profile picker, Swap, Config,
  Turn Off (Bluetooth), reorder arrows, drag to swap, and click to rumble. Controllers can be
  given a name for the session.
- Assign Handheld Controller: auto-detect, or pick/Identify the handheld's own pad.
- P1-P8 tabs show each player's battery and can be dragged or moved with arrows to change the
  player order.
- In Modern Console the Controllers and player tabs use a modern card layout that follows the
  light and dark themes, including the system's dark mode (macOS).

## On-screen display

- **Controller Overlay** (View menu, Ctrl+Shift+V): players, their controllers and batteries,
  and each player's recent buttons, including stick moves (LS/RS) and clicks (L3/R3). How many
  inputs are kept and for how long can be set.
- **Controller Hints**: names a controller in the bottom-left corner when it joins or leaves
  a player, and says when the game pauses for a disconnected one. Works without the overlay.

Both are drawn into the presented frame as crisp overlay panels (not scaled with the game, not
in screenshots), so they show inside gamescope.

## Steam Input and gamescope fixes

- **Native gyro under Steam Input.** Steam's virtual pads carry no motion sensors. Eden now opens
  the real motion-capable controller behind each virtual pad for its sensors only (Steam Deck,
  Steam Controller incl. the 2026 model, Sony, Nintendo), and turns on the Deck's IMU, which
  Steam keeps off unless the layout uses the gyro. No DSU server or Steam layout change needed.
- **Bypass Steam Input** (Controllers tab, shown when launched through Steam): Eden opens the
  real controllers itself instead of Steam's virtual pads.
- **Controller identity under Steam.** All virtual pads share one GUID and Steam moves physical
  controllers between them; controllers are tracked by their Steam handle and slot instead.
- **Handheld detection under Steam.** The Deck's own controls are recognised behind Steam Input
  without mistaking standalone controllers (e.g. the Steam Controller) for them.
- **Windows no longer rescale on hover in Game Mode.** gamescope scales a window to fit the
  screen together with any tooltip reaching past it, so the main window and Settings grew and
  shrank on hover. Under gamescope they now fill the screen.
- **VSync modes in Game Mode.** gamescope offers only some present modes, so Immediate and FIFO
  Relaxed vanished silently. They are now listed greyed out with a tooltip pointing to Steam's
  Disable Frame Limit and Allow Tearing settings.
- **Stick range testers on small screens.** The controller drawing scales down to fit on
  800-pixel screens instead of cutting off the stick range circles.
- **Midnight Blue themes.** A hovered menu-bar item is readable again, and hovered tabs no longer
  change size (which made the Settings tabs shake).

## Fixes

- Crash when HID received a controller update before NPad's applet resource was set up.

## Testing

Tested on a Steam Deck (Desktop and Game Mode, direct and Steam launches) with Xbox
controllers, plus a stress test with up to 10 virtual controllers connecting, disconnecting and
pressing at random in both automatic modes (no assignment errors, never more than 8 players,
disconnect pauses resume as expected).

## AI disclosure

Developed with Claude Opus 5.5 (implementation and debugging). I reviewed, tested and can
explain every change.

- [x] I have read and followed the [Contribution Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/CONTRIBUTING.md#code-contributions).
- [x] I have read and followed the [AI Policy](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/AI.md).
- [x] I have read and followed the [Coding Guidelines](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/policies/Coding.md) to the best of my ability.

---

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4583
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2026-11-07 02:26:31 +01:00

1481 lines
57 KiB
C++

// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <numbers>
#include <set>
#include <sstream>
#include <string>
#include "common/controller_console_state.h"
#include "common/logging.h"
#include "common/math_util.h"
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "common/vector_math.h"
#include "input_common/drivers/sdl_driver.h"
namespace InputCommon {
namespace {
Common::UUID GetGUID(SDL_Joystick* joystick) {
const SDL_GUID guid = SDL_GetJoystickGUID(joystick);
std::array<u8, 16> data{};
std::memcpy(data.data(), guid.data, sizeof(data));
// Clear controller name crc
std::memset(data.data() + 2, 0, sizeof(u16));
return Common::UUID{data};
}
using GamepadBindings = std::vector<SDL_GamepadBinding>;
SDL_GamepadBinding EmptyBinding() {
SDL_GamepadBinding binding{};
binding.input_type = SDL_GAMEPAD_BINDTYPE_NONE;
return binding;
}
GamepadBindings GetBindings(SDL_Gamepad* controller) {
if (controller == nullptr) {
return {};
}
int binding_count = 0;
SDL_GamepadBinding** bindings = SDL_GetGamepadBindings(controller, &binding_count);
if (bindings == nullptr) {
return {};
}
GamepadBindings cached_bindings{};
cached_bindings.reserve(static_cast<std::size_t>(binding_count));
for (int i = 0; i < binding_count; ++i) {
if (const auto* current = bindings[i]) {
cached_bindings.emplace_back(*current);
}
}
SDL_free(bindings);
return cached_bindings;
}
template <typename Predicate>
SDL_GamepadBinding FindBinding(const GamepadBindings& bindings, Predicate matches) {
const auto it = std::find_if(bindings.begin(), bindings.end(), matches);
return it != bindings.end() ? *it : EmptyBinding();
}
SDL_GamepadBinding GetBindingForButton(const GamepadBindings& bindings, SDL_GamepadButton button) {
return FindBinding(bindings, [button](const SDL_GamepadBinding& current) {
return current.output_type == SDL_GAMEPAD_BINDTYPE_BUTTON &&
current.output.button == static_cast<SDL_GamepadButton>(button);
});
}
SDL_GamepadBinding GetBindingForAxis(const GamepadBindings& bindings, SDL_GamepadAxis axis) {
return FindBinding(bindings, [axis](const SDL_GamepadBinding& current) {
return current.output_type == SDL_GAMEPAD_BINDTYPE_AXIS &&
current.output.axis.axis == static_cast<SDL_GamepadAxis>(axis);
});
}
} // Anonymous namespace
static bool SDLEventWatcher(void* user_data, SDL_Event* event) {
auto* const sdl_state = static_cast<SDLDriver*>(user_data);
sdl_state->HandleGameControllerEvent(*event);
return true;
}
class SDLJoystick {
public:
SDLJoystick(Common::UUID guid_, int port_, SDL_Joystick* joystick,
SDL_Gamepad* game_controller)
: guid{guid_}, port{port_}, sdl_joystick{joystick, &SDL_CloseJoystick},
sdl_controller{game_controller, &SDL_CloseGamepad} {
EnableMotion();
}
void EnableMotion() {
if (!sdl_controller) {
return;
}
SDL_Gamepad* controller = sdl_controller.get();
if (HasMotion()) {
SDL_SetGamepadSensorEnabled(controller, SDL_SENSOR_ACCEL, false);
SDL_SetGamepadSensorEnabled(controller, SDL_SENSOR_GYRO, false);
}
has_accel = SDL_GamepadHasSensor(controller, SDL_SENSOR_ACCEL);
has_gyro = SDL_GamepadHasSensor(controller, SDL_SENSOR_GYRO);
if (has_accel) {
if (!SDL_SetGamepadSensorEnabled(controller, SDL_SENSOR_ACCEL, true)) {
LOG_WARNING(Input, "Failed to enable accelerometer sensor: {}", SDL_GetError());
}
}
if (has_gyro) {
if (!SDL_SetGamepadSensorEnabled(controller, SDL_SENSOR_GYRO, true)) {
LOG_WARNING(Input, "Failed to enable gyroscope sensor: {}", SDL_GetError());
}
}
LOG_INFO(Input, "Controller motion capabilities: accel={} gyro={}", has_accel, has_gyro);
}
bool HasMotion() const {
return has_gyro || has_accel;
}
bool UpdateMotion(SDL_GamepadSensorEvent event) {
constexpr float gravity_constant = 9.80665f;
std::scoped_lock lock{mutex};
const u64 sensor_timestamp = event.sensor_timestamp != 0 ? event.sensor_timestamp
: event.timestamp;
if (last_motion_update == 0) {
last_motion_update = sensor_timestamp;
return false;
}
if (sensor_timestamp < last_motion_update) {
return false;
}
// SDL3 reports sensor timestamps in nanoseconds, while the input stack expects
// delta timestamps in microseconds.
const u64 time_difference = (sensor_timestamp - last_motion_update) / 1000;
last_motion_update = sensor_timestamp;
switch (event.sensor) {
case SDL_SENSOR_ACCEL: {
motion.accel_x = -event.data[0] / gravity_constant;
motion.accel_y = event.data[2] / gravity_constant;
motion.accel_z = -event.data[1] / gravity_constant;
break;
}
case SDL_SENSOR_GYRO: {
motion.gyro_x = event.data[0] / (f32(std::numbers::pi_v<float>) * 2);
motion.gyro_y = -event.data[2] / (f32(std::numbers::pi_v<float>) * 2);
motion.gyro_z = event.data[1] / (f32(std::numbers::pi_v<float>) * 2);
break;
}
}
// Ignore duplicated timestamps
if (time_difference == 0) {
return false;
}
// Motion data is invalid
if (motion.accel_x == 0 && motion.gyro_x == 0 && motion.accel_y == 0 &&
motion.gyro_y == 0 && motion.accel_z == 0 && motion.gyro_z == 0) {
if (motion_error_count++ < 200) {
return false;
}
// Try restarting the sensor
motion_error_count = 0;
EnableMotion();
return false;
}
motion_error_count = 0;
motion.delta_timestamp = time_difference;
return true;
}
const BasicMotion& GetMotion() const {
return motion;
}
bool RumblePlay(const Common::Input::VibrationStatus vibration) {
constexpr u32 rumble_max_duration_ms = 2000;
constexpr f32 low_start_sensitivity_limit = 140.0f;
constexpr f32 low_width_sensitivity_limit = 400.0f;
constexpr f32 high_start_sensitivity_limit = 200.0f;
constexpr f32 high_width_sensitivity_limit = 700.0f;
// Try to provide some feeling of the frequency by reducing the amplitude depending on it.
f32 low_frequency_scale = 1.0f;
if (vibration.low_frequency > low_start_sensitivity_limit) {
low_frequency_scale =
(std::max)(1.0f - (vibration.low_frequency - low_start_sensitivity_limit) /
low_width_sensitivity_limit,
0.3f);
}
f32 low_amplitude = vibration.low_amplitude * low_frequency_scale;
f32 high_frequency_scale = 1.0f;
if (vibration.high_frequency > high_start_sensitivity_limit) {
high_frequency_scale =
(std::max)(1.0f - (vibration.high_frequency - high_start_sensitivity_limit) /
high_width_sensitivity_limit,
0.3f);
}
f32 high_amplitude = vibration.high_amplitude * high_frequency_scale;
if (sdl_controller) {
return SDL_RumbleGamepad(sdl_controller.get(), static_cast<u16>(low_amplitude),
static_cast<u16>(high_amplitude), rumble_max_duration_ms);
} else if (sdl_joystick) {
return SDL_RumbleJoystick(sdl_joystick.get(), static_cast<u16>(low_amplitude),
static_cast<u16>(high_amplitude),
rumble_max_duration_ms);
}
return false;
}
bool HasHDRumble() const {
constexpr Uint16 valve_vendor_id = 0x28DE;
const auto is_known_hd_type = [](SDL_GamepadType type) {
return type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO ||
type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT ||
type == SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT ||
type == SDL_GAMEPAD_TYPE_PS5;
};
// Valve hardware doesn't have any enums in SDL, so we have to support it manually.
// Since they have HD rumble, we can assume that all their hardware supports it, even if we can't detect the exact type.
if (sdl_controller) {
if (is_known_hd_type(SDL_GetGamepadType(sdl_controller.get())) ||
SDL_GetGamepadVendor(sdl_controller.get()) == valve_vendor_id) {
return true;
}
}
if (sdl_joystick) {
if (SDL_GetJoystickVendor(sdl_joystick.get()) == valve_vendor_id) {
return true;
}
}
return false;
}
void EnableVibration(bool is_enabled) {
has_vibration = is_enabled;
is_vibration_tested = true;
}
bool HasVibration() const {
return has_vibration;
}
bool IsVibrationTested() const {
return is_vibration_tested;
}
/**
* The Pad identifier of the joystick
*/
const PadIdentifier GetPadIdentifier() const {
return {
.guid = guid,
.port = static_cast<std::size_t>(port),
.pad = 0,
};
}
/**
* The guid of the joystick
*/
const Common::UUID& GetGUID() const {
return guid;
}
/**
* The number of joystick from the same type that were connected before this joystick
*/
int GetPort() const {
return port;
}
SDL_Joystick* GetSDLJoystick() const {
return sdl_joystick.get();
}
SDL_Gamepad* GetSDLGameController() const {
return sdl_controller.get();
}
void SetSDLJoystick(SDL_Joystick* joystick, SDL_Gamepad* controller) {
sdl_joystick.reset(joystick);
sdl_controller.reset(controller);
}
bool IsJoyconLeft() const {
const std::string controller_name = GetControllerName();
if (std::strstr(controller_name.c_str(), "Joy-Con Left") != nullptr) {
return true;
}
if (std::strstr(controller_name.c_str(), "Joy-Con (L)") != nullptr) {
return true;
}
return false;
}
bool IsJoyconRight() const {
const std::string controller_name = GetControllerName();
if (std::strstr(controller_name.c_str(), "Joy-Con Right") != nullptr) {
return true;
}
if (std::strstr(controller_name.c_str(), "Joy-Con (R)") != nullptr) {
return true;
}
return false;
}
Common::Input::BatteryLevel GetBatteryLevel(SDL_PowerState battery_level, int percent) {
if (battery_level == SDL_POWERSTATE_CHARGING) {
return Common::Input::BatteryLevel::Charging;
}
if (percent >= 0 && percent <= 100) {
if (percent <= 5) {
return Common::Input::BatteryLevel::Empty;
}
if (percent <= 20) {
return Common::Input::BatteryLevel::Critical;
}
if (percent <= 40) {
return Common::Input::BatteryLevel::Low;
}
if (percent <= 70) {
return Common::Input::BatteryLevel::Medium;
}
return Common::Input::BatteryLevel::Full;
}
switch (battery_level) {
case SDL_POWERSTATE_ON_BATTERY:
return Common::Input::BatteryLevel::Medium;
case SDL_POWERSTATE_NO_BATTERY:
return Common::Input::BatteryLevel::None;
case SDL_POWERSTATE_CHARGED:
return Common::Input::BatteryLevel::Full;
case SDL_POWERSTATE_ERROR:
case SDL_POWERSTATE_UNKNOWN:
default:
return Common::Input::BatteryLevel::None;
}
}
std::string GetControllerName() const {
if (sdl_controller) {
switch (SDL_GetGamepadType(sdl_controller.get())) {
case SDL_GAMEPAD_TYPE_XBOX360:
return "Xbox 360 Controller";
case SDL_GAMEPAD_TYPE_XBOXONE:
return "Xbox One Controller";
case SDL_GAMEPAD_TYPE_PS3:
return "DualShock 3 Controller";
case SDL_GAMEPAD_TYPE_PS4:
return "DualShock 4 Controller";
case SDL_GAMEPAD_TYPE_PS5:
return "DualSense Controller";
default:
break;
}
const auto name = SDL_GetGamepadName(sdl_controller.get());
if (name) {
return name;
}
}
if (sdl_joystick) {
const auto name = SDL_GetJoystickName(sdl_joystick.get());
if (name) {
return name;
}
}
return "Unknown";
}
private:
Common::UUID guid;
int port;
std::unique_ptr<SDL_Joystick, decltype(&SDL_CloseJoystick)> sdl_joystick;
std::unique_ptr<SDL_Gamepad, decltype(&SDL_CloseGamepad)> sdl_controller;
mutable std::mutex mutex;
u64 last_motion_update{};
std::size_t motion_error_count{};
bool has_gyro{false};
bool has_accel{false};
bool has_vibration{false};
bool is_vibration_tested{false};
BasicMotion motion;
};
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const Common::UUID& guid, int port) {
std::scoped_lock lock{joystick_map_mutex};
const auto it = joystick_map.find(guid);
if (it != joystick_map.end()) {
while (it->second.size() <= static_cast<std::size_t>(port)) {
auto joystick = std::make_shared<SDLJoystick>(guid, static_cast<int>(it->second.size()),
nullptr, nullptr);
it->second.emplace_back(std::move(joystick));
}
return it->second[static_cast<std::size_t>(port)];
}
auto joystick = std::make_shared<SDLJoystick>(guid, 0, nullptr, nullptr);
return joystick_map[guid].emplace_back(std::move(joystick));
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGUID(const std::string& guid, int port) {
return GetSDLJoystickByGUID(Common::UUID{guid}, port);
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickBySDLID(SDL_JoystickID sdl_id) {
auto sdl_joystick = SDL_GetJoystickFromID(sdl_id);
if (sdl_joystick == nullptr) {
return nullptr;
}
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
const auto map_it = joystick_map.find(guid);
if (map_it == joystick_map.end()) {
return nullptr;
}
const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (vec_it == map_it->second.end()) {
return nullptr;
}
return *vec_it;
}
std::optional<std::pair<std::string, int>> SDLDriver::GetBindingForSDLID(SDL_JoystickID sdl_id) {
const auto joystick = GetSDLJoystickBySDLID(sdl_id);
if (!joystick) {
return std::nullopt;
}
return std::pair{joystick->GetGUID().RawString(), joystick->GetPort()};
}
void SDLDriver::MarkActivity(SDL_JoystickID sdl_id) {
std::scoped_lock lock{activity_mutex};
last_activity[sdl_id] = std::chrono::steady_clock::now();
}
std::optional<std::chrono::steady_clock::time_point> SDLDriver::GetLastActivity(
SDL_JoystickID sdl_id) const {
std::scoped_lock lock{activity_mutex};
const auto it = last_activity.find(sdl_id);
if (it == last_activity.end()) {
return std::nullopt;
}
return it->second;
}
std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickByGamepadID(SDL_JoystickID sdl_id) {
auto* const sdl_gamepad = SDL_GetGamepadFromID(sdl_id);
if (sdl_gamepad == nullptr) {
return nullptr;
}
auto* const sdl_joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (sdl_joystick == nullptr) {
return nullptr;
}
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
const auto map_it = joystick_map.find(guid);
if (map_it == joystick_map.end()) {
return nullptr;
}
const auto vec_it = std::find_if(map_it->second.begin(), map_it->second.end(),
[sdl_joystick, sdl_gamepad](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick ||
joystick->GetSDLGameController() == sdl_gamepad;
});
if (vec_it == map_it->second.end()) {
return nullptr;
}
return *vec_it;
}
void SDLDriver::InitJoystick(SDL_JoystickID joystick_id) {
SDL_Joystick* sdl_joystick = SDL_OpenJoystick(joystick_id);
SDL_Gamepad* sdl_gamecontroller = nullptr;
int battery_percent = -1;
SDL_PowerState battery_state = SDL_POWERSTATE_UNKNOWN;
if (SDL_IsGamepad(joystick_id)) {
sdl_gamecontroller = SDL_OpenGamepad(joystick_id);
}
if (!sdl_joystick) {
LOG_ERROR(Input, "Failed to open joystick {}", joystick_id);
return;
}
battery_state = SDL_GetJoystickPowerInfo(sdl_joystick, &battery_percent);
const auto guid = GetGUID(sdl_joystick);
if (Settings::values.enable_joycon_driver) {
if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e &&
(guid.uuid[8] == 0x06 || guid.uuid[8] == 0x07)) {
LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_id);
SDL_CloseJoystick(sdl_joystick);
return;
}
}
if (Settings::values.enable_procon_driver) {
if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e && guid.uuid[8] == 0x09) {
LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_id);
SDL_CloseJoystick(sdl_joystick);
return;
}
}
std::scoped_lock lock{joystick_map_mutex};
if (joystick_map.find(guid) == joystick_map.end()) {
auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
joystick->EnableMotion();
SetBattery(joystick->GetPadIdentifier(),
joystick->GetBatteryLevel(battery_state, battery_percent));
joystick_map[guid].emplace_back(std::move(joystick));
return;
}
auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it =
std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[](const auto& joystick) { return !joystick->GetSDLJoystick(); });
if (joystick_it != joystick_guid_list.end()) {
const int port = static_cast<int>(joystick_it - joystick_guid_list.begin());
if (Common::ControllerProbes()) {
LOG_INFO(Input, "input: attach guid={} port={} name={} (reusing a freed slot)",
guid.RawString(), port, SDL_GetJoystickName(sdl_joystick));
}
(*joystick_it)->SetSDLJoystick(sdl_joystick, sdl_gamecontroller);
(*joystick_it)->EnableMotion();
SetBattery((*joystick_it)->GetPadIdentifier(),
(*joystick_it)->GetBatteryLevel(battery_state, battery_percent));
return;
}
const int port = static_cast<int>(joystick_guid_list.size());
auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
joystick->EnableMotion();
SetBattery(joystick->GetPadIdentifier(),
joystick->GetBatteryLevel(battery_state, battery_percent));
joystick_guid_list.emplace_back(std::move(joystick));
}
void SDLDriver::CloseJoystick(SDL_Joystick* sdl_joystick) {
const auto guid = GetGUID(sdl_joystick);
std::scoped_lock lock{joystick_map_mutex};
// This call to guid is safe since the joystick is guaranteed to be in the map
const auto& joystick_guid_list = joystick_map[guid];
const auto joystick_it = std::find_if(joystick_guid_list.begin(), joystick_guid_list.end(),
[&sdl_joystick](const auto& joystick) {
return joystick->GetSDLJoystick() == sdl_joystick;
});
if (joystick_it != joystick_guid_list.end()) {
if (Common::ControllerProbes()) {
LOG_INFO(Input, "input: detach guid={} port={}", guid.RawString(),
static_cast<int>(joystick_it - joystick_guid_list.begin()));
}
(*joystick_it)->SetSDLJoystick(nullptr, nullptr);
}
}
void SDLDriver::PumpEvents() const {
if (initialized) {
SDL_PumpEvents();
}
}
void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_JOYSTICK_BUTTON_UP: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, false);
}
break;
}
case SDL_EVENT_JOYSTICK_BUTTON_DOWN: {
MarkActivity(event.jbutton.which);
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, true);
}
break;
}
case SDL_EVENT_JOYSTICK_HAT_MOTION: {
if (event.jhat.value != SDL_HAT_CENTERED) {
MarkActivity(event.jhat.which);
}
if (const auto joystick = GetSDLJoystickBySDLID(event.jhat.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetHatButton(identifier, event.jhat.hat, event.jhat.value);
}
break;
}
case SDL_EVENT_JOYSTICK_AXIS_MOTION: {
// Half travel, so resting drift never counts. A trigger rests at one end of its range
// (-32768 here), so for it half travel is past the middle.
Sint16 rest = 0;
const auto moved = GetSDLJoystickBySDLID(event.jaxis.which);
SDL_Joystick* const moved_js = moved ? moved->GetSDLJoystick() : nullptr;
const bool trigger =
moved_js && SDL_GetJoystickAxisInitialState(moved_js, event.jaxis.axis, &rest) &&
rest < -16384;
if (trigger ? event.jaxis.value > 0
: event.jaxis.value > 16384 || event.jaxis.value < -16384) {
MarkActivity(event.jaxis.which);
}
if (moved) {
const PadIdentifier identifier = moved->GetPadIdentifier();
SetAxis(identifier, event.jaxis.axis, event.jaxis.value / 32767.0f);
}
break;
}
case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: {
auto joystick = GetSDLJoystickByGamepadID(event.gsensor.which);
if (!joystick) {
joystick = GetSDLJoystickBySDLID(event.gsensor.which);
}
if (joystick) {
if (joystick->UpdateMotion(event.gsensor)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetMotion(identifier, 0, joystick->GetMotion());
}
}
break;
}
case SDL_EVENT_JOYSTICK_BATTERY_UPDATED: {
if (auto joystick = GetSDLJoystickBySDLID(event.jbattery.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetBattery(identifier,
joystick->GetBatteryLevel(event.jbattery.state, event.jbattery.percent));
}
break;
}
case SDL_EVENT_JOYSTICK_REMOVED:
LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
CloseJoystick(SDL_GetJoystickFromID(event.jdevice.which));
break;
case SDL_EVENT_JOYSTICK_ADDED:
LOG_DEBUG(Input, "Controller connected with device index {}", event.jdevice.which);
InitJoystick(event.jdevice.which);
break;
}
}
void SDLDriver::CloseJoysticks() {
std::scoped_lock lock{joystick_map_mutex};
joystick_map.clear();
}
namespace {
/// Controllers with motion sensors that Steam Input hides behind its sensorless virtual pads
bool IsMotionController(unsigned vendor, unsigned product) {
switch (vendor) {
case 0x28DE:
// The Steam Deck only: SDL's driver would talk to a Steam Controller alongside Steam
// (Bypass Steam Input gives SDL Steam Controllers, motion included)
return product == 0x1205;
case 0x054C: // Sony
case 0x057E: // Nintendo
return true;
default:
return false;
}
}
/// Controllers Steam Input hid that Eden opened again for their motion (vendor << 16 | product).
/// Motion sources only: never offered as input devices.
std::set<u32>& UnhiddenControllers() {
static std::set<u32> unhidden;
return unhidden;
}
/// Logs the system's motion sensors and HID devices once (Debug Probes), to identify handhelds
/// from users' logs: a built-in gyro is often a system sensor (IIO) that SDL never sees.
void LogMotionHardware() {
#ifdef __linux__
if (!Common::ControllerProbes()) {
return;
}
namespace fs = std::filesystem;
const auto read_line = [](const fs::path& path) {
std::ifstream file{path};
std::string line;
std::getline(file, line);
return line;
};
std::error_code error;
std::string sensors;
// (No exceptions: a device can disappear while it is read)
for (fs::directory_iterator it{"/sys/bus/iio/devices", error}, end; !error && it != end;
it.increment(error)) {
const fs::path dir = it->path();
std::error_code ignored;
const bool gyro = fs::exists(dir / "in_anglvel_x_raw", ignored);
const bool accel = fs::exists(dir / "in_accel_x_raw", ignored);
if (!gyro && !accel) {
continue;
}
std::string mount = read_line(dir / "in_anglvel_mount_matrix");
if (mount.empty()) {
mount = read_line(dir / "in_accel_mount_matrix");
}
const fs::path device = fs::canonical(dir / "device", ignored);
sensors += fmt::format("\n {} name={} gyro={} accel={} mount=[{}] at={}",
dir.filename().string(), read_line(dir / "name"), gyro, accel, mount,
ignored ? std::string{"?"} : device.string());
}
LOG_INFO(Input, "Motion sensors of the system (IIO):{}",
sensors.empty() ? std::string{" none"} : sensors);
std::string hid;
error.clear();
for (fs::directory_iterator it{"/sys/class/hidraw", error}, end; !error && it != end;
it.increment(error)) {
const auto& entry = *it;
std::ifstream uevent{entry.path() / "device" / "uevent"};
std::string line;
std::string id;
std::string name;
while (std::getline(uevent, line)) {
if (line.starts_with("HID_ID=")) {
// bus:vendor:product, each zero-padded to 8 hex digits
unsigned bus = 0, vendor = 0, product = 0;
if (std::sscanf(line.c_str() + 7, "%x:%x:%x", &bus, &vendor, &product) == 3) {
id = fmt::format("{:04x}:{:04x} bus={}", vendor, product, bus);
}
} else if (line.starts_with("HID_NAME=")) {
name = line.substr(9);
}
}
hid += fmt::format("\n {} {} name={}", entry.path().filename().string(), id, name);
}
LOG_INFO(Input, "HID devices:{}", hid.empty() ? std::string{" none"} : hid);
#endif
}
/// A controller Eden opened again under Steam Input for its motion alone
bool IsUnhiddenController(SDL_Joystick* joystick) {
const u32 id =
static_cast<u32>(SDL_GetJoystickVendor(joystick)) << 16 | SDL_GetJoystickProduct(joystick);
return UnhiddenControllers().contains(id);
}
/// Under Steam, takes the motion controllers off Steam's hide list, so SDL also opens the real
/// devices next to Steam's sensorless virtual pads
void UnhideMotionControllers() {
if (!Common::RunningUnderSteam()) {
return;
}
const char* env = std::getenv("SDL_GAMECONTROLLER_IGNORE_DEVICES");
std::string list = env ? env : "";
if (list.starts_with('@')) {
std::ifstream file{list.substr(1)};
list.assign(std::istreambuf_iterator<char>{file}, std::istreambuf_iterator<char>{});
}
std::string kept;
std::string dropped;
std::stringstream entries{list};
for (std::string entry; std::getline(entries, entry, ',');) {
entry.erase(std::remove_if(entry.begin(), entry.end(),
[](unsigned char c) { return std::isspace(c); }),
entry.end());
unsigned vendor = 0;
unsigned product = 0;
if (entry.empty()) {
continue;
}
if (std::sscanf(entry.c_str(), "%x/%x", &vendor, &product) == 2 &&
IsMotionController(vendor, product)) {
dropped += entry + ',';
UnhiddenControllers().insert(vendor << 16 | product);
{
auto& state = Common::GetControllerConsoleState();
std::scoped_lock lock{state.mutex};
state.steam_hidden_models.insert(vendor << 16 | product);
}
continue;
}
kept += entry + ',';
}
if (!kept.empty()) {
kept.pop_back();
}
if (Common::ControllerProbes()) {
LOG_INFO(Input, "Steam Input: opening motion controllers for their sensors: {}",
dropped.empty() ? std::string{"none hidden"} : dropped);
}
SDL_SetHintWithPriority(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, kept.c_str(),
SDL_HINT_OVERRIDE);
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_HIDAPI_STEAMDECK, "1", SDL_HINT_OVERRIDE);
}
} // Anonymous namespace
SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
// Set our application name. Currently passed to DBus by SDL and visible to the user through
// their desktop environment.
SDL_SetHint(SDL_HINT_APP_NAME, "Eden");
// Disable raw input. When enabled this setting causes SDL to die when a web applet opens
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, Settings::values.enable_raw_input ? "1" : "0");
#ifdef _WIN32
if (Settings::values.disable_wgi_xinput) {
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_RAWINPUT_CORRELATE_XINPUT, "0", SDL_HINT_OVERRIDE);
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_OVERRIDE);
}
#endif
// SDL3 defaults Steam Controller Bluetooth HIDAPI support to off, which can disable gyro.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
// Bypass Steam Input (Modern Console, under Steam): SDL takes every controller Steam hides,
// and the engine uses those instead of Steam's virtual pads
const auto mode = Settings::values.controller_applet_mode.GetValue();
const bool automatic = mode == Settings::AppletMode::ConsoleMode ||
mode == Settings::AppletMode::HandheldConsoleMode;
if (Common::RunningUnderSteam() && automatic &&
Settings::values.console_bypass_steam.GetValue()) {
SDL_SetHintWithPriority(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, "", SDL_HINT_OVERRIDE);
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_HIDAPI_STEAMDECK, "1", SDL_HINT_OVERRIDE);
Common::GetControllerConsoleState().steam_bypassed = true;
if (Common::ControllerProbes()) {
LOG_INFO(Input, "Steam Input bypassed: SDL opens every controller");
}
} else if (automatic) {
// (Only the automatic modes read their motion)
UnhideMotionControllers();
}
LogMotionHardware();
SDL_SetHint(SDL_HINT_GAMECONTROLLER_SENSOR_FUSION, "1");
SDL_SetHint(SDL_HINT_AUTO_UPDATE_SENSORS, "1");
// Enable HIDAPI rumble. This prevents SDL from disabling motion on PS4 and PS5 controllers
SDL_SetHint(SDL_HINT_JOYSTICK_ENHANCED_REPORTS, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// Disable hidapi drivers for joycon controllers when the custom joycon driver is enabled
if (Settings::values.enable_joycon_driver) {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "0");
} else {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOYCON_HOME_LED, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, "1");
}
// Disable hidapi drivers for pro controllers when the custom joycon driver is enabled
if (Settings::values.enable_procon_driver) {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0");
} else {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0");
}
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, "1");
// Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native
// driver on Linux.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
// If the frontend is going to manage the event loop, then we don't start one here
start_thread = SDL_WasInit(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD) == 0;
if (start_thread && !SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) {
LOG_CRITICAL(Input, "SDL_Init failed with: {}", SDL_GetError());
return;
}
SDL_AddEventWatch(&SDLEventWatcher, this);
initialized = true;
// Because the events for joystick connection happens before we have our event watcher added, we
// can just open all the joysticks right here
int joystick_count = 0;
SDL_JoystickID* joysticks = SDL_GetJoysticks(&joystick_count);
if (joysticks != nullptr) {
for (int i = 0; i < joystick_count; ++i) {
InitJoystick(joysticks[i]);
}
SDL_free(joysticks);
}
}
SDLDriver::~SDLDriver() {
CloseJoysticks();
SDL_RemoveEventWatch(&SDLEventWatcher, this);
initialized = false;
if (start_thread) {
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD);
}
}
std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
::Common::unordered_map<int, std::shared_ptr<SDLJoystick>> joycon_pairs;
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (!joystick->GetSDLJoystick()) {
continue;
}
// Steam's virtual pads carry the real controller's name: mark them. Their GUID
// keeps Steam's ids (Valve 0x28DE, virtual gamepad 0x11FF, at bytes 4 and 8).
const SDL_GUID sdl_guid = SDL_GetJoystickGUID(joystick->GetSDLJoystick());
const bool steam_virtual = sdl_guid.data[4] == 0xDE && sdl_guid.data[5] == 0x28 &&
sdl_guid.data[8] == 0xFF && sdl_guid.data[9] == 0x11;
if (!steam_virtual && IsUnhiddenController(joystick->GetSDLJoystick())) {
continue;
}
const std::string name =
fmt::format("{} {}{}", joystick->GetControllerName(), joystick->GetPort(),
steam_virtual ? " (Steam)" : "");
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", joystick->GetGUID().RawString()},
{"port", std::to_string(joystick->GetPort())},
});
if (joystick->IsJoyconLeft()) {
joycon_pairs.insert_or_assign(joystick->GetPort(), joystick);
}
}
}
// Add dual controllers
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (joystick->IsJoyconRight() && joystick->GetSDLJoystick() &&
!IsUnhiddenController(joystick->GetSDLJoystick())) {
if (!joycon_pairs.contains(joystick->GetPort())) {
continue;
}
const auto joystick2 = joycon_pairs.at(joystick->GetPort());
const std::string name =
fmt::format("{} {}", "Nintendo Dual Joy-Con", joystick->GetPort());
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"guid", joystick->GetGUID().RawString()},
{"guid2", joystick2->GetGUID().RawString()},
{"port", std::to_string(joystick->GetPort())},
});
}
}
}
return devices;
}
Common::Input::DriverResult SDLDriver::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto joystick =
GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port));
const auto process_amplitude_exp = [](f32 amplitude, f32 factor) {
return (amplitude + std::pow(amplitude, factor)) * 0.5f * 0xFFFF;
};
// Default exponential curve for rumble
f32 factor = 0.35f;
// If vibration is set as a linear output use a flatter value
if (vibration.type == Common::Input::VibrationAmplificationType::Linear) {
factor = 0.5f;
}
// Amplitude for HD rumble needs no modification
if (joystick->HasHDRumble()) {
factor = 1.0f;
}
const Common::Input::VibrationStatus new_vibration{
.low_amplitude = process_amplitude_exp(vibration.low_amplitude, factor),
.low_frequency = vibration.low_frequency,
.high_amplitude = process_amplitude_exp(vibration.high_amplitude, factor),
.high_frequency = vibration.high_frequency,
.type = Common::Input::VibrationAmplificationType::Exponential,
};
joystick->RumblePlay(new_vibration);
return Common::Input::DriverResult::Success;
}
bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) {
const auto joystick =
GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port));
static constexpr Common::Input::VibrationStatus test_vibration{
.low_amplitude = 1,
.low_frequency = 160.0f,
.high_amplitude = 1,
.high_frequency = 320.0f,
.type = Common::Input::VibrationAmplificationType::Exponential,
};
static constexpr Common::Input::VibrationStatus zero_vibration{
.low_amplitude = 0,
.low_frequency = 160.0f,
.high_amplitude = 0,
.high_frequency = 320.0f,
.type = Common::Input::VibrationAmplificationType::Exponential,
};
if (joystick->IsVibrationTested()) {
return joystick->HasVibration();
}
// First vibration might fail
joystick->RumblePlay(test_vibration);
// Wait for about 15ms to ensure the controller is ready for the stop command
std::this_thread::sleep_for(std::chrono::milliseconds(15));
if (!joystick->RumblePlay(zero_vibration)) {
joystick->EnableVibration(false);
return false;
}
joystick->EnableVibration(true);
return true;
}
Common::ParamPackage SDLDriver::BuildAnalogParamPackageForButton(int port, const Common::UUID& guid,
s32 axis, float value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("axis", axis);
params.Set("threshold", "0.5");
params.Set("invert", value < 0 ? "-" : "+");
return params;
}
Common::ParamPackage SDLDriver::BuildButtonParamPackageForButton(int port, const Common::UUID& guid,
s32 button) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("button", button);
return params;
}
Common::ParamPackage SDLDriver::BuildHatParamPackageForButton(int port, const Common::UUID& guid,
s32 hat, u8 value) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("port", port);
params.Set("guid", guid.RawString());
params.Set("hat", hat);
params.Set("direction", GetHatButtonName(value));
return params;
}
Common::ParamPackage SDLDriver::BuildMotionParam(int port, const Common::UUID& guid) const {
Common::ParamPackage params{};
params.Set("engine", GetEngineName());
params.Set("motion", 0);
params.Set("port", port);
params.Set("guid", guid.RawString());
return params;
}
Common::ParamPackage SDLDriver::BuildParamPackageForBinding(
int port, const Common::UUID& guid, const SDL_GamepadBinding& binding) const {
switch (binding.input_type) {
case SDL_GAMEPAD_BINDTYPE_NONE:
break;
case SDL_GAMEPAD_BINDTYPE_AXIS:
return BuildAnalogParamPackageForButton(port, guid, binding.input.axis.axis);
case SDL_GAMEPAD_BINDTYPE_BUTTON:
return BuildButtonParamPackageForButton(port, guid, binding.input.button);
case SDL_GAMEPAD_BINDTYPE_HAT:
return BuildHatParamPackageForButton(port, guid, binding.input.hat.hat,
static_cast<u8>(binding.input.hat.hat_mask));
}
return {};
}
Common::ParamPackage SDLDriver::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
int axis_y, float offset_x,
float offset_y) const {
Common::ParamPackage params;
params.Set("engine", GetEngineName());
params.Set("port", static_cast<int>(identifier.port));
params.Set("guid", identifier.guid.RawString());
params.Set("axis_x", axis_x);
params.Set("axis_y", axis_y);
params.Set("offset_x", offset_x);
params.Set("offset_y", offset_y);
params.Set("invert_x", "+");
params.Set("invert_y", "+");
return params;
}
ButtonMapping SDLDriver::GetButtonMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
// This list is missing ZL/ZR since those are not considered buttons in SDL GameController.
// We will add those afterwards
ButtonBindings switch_to_sdl_button = GetDefaultButtonBinding(joystick);
// Add the missing bindings for ZL/ZR
static constexpr ZButtonBindings switch_to_sdl_axis{{
{Settings::NativeButton::ZL, SDL_GAMEPAD_AXIS_LEFT_TRIGGER},
{Settings::NativeButton::ZR, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER},
}};
if (auto* controller = joystick->GetSDLGameController(); controller) {
// Parameters contain two joysticks return dual
if (params.Has("guid2")) {
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
if (joystick2->GetSDLGameController())
return GetDualControllerMapping(joystick, joystick2, switch_to_sdl_button, switch_to_sdl_axis);
}
return GetSingleControllerMapping(joystick, switch_to_sdl_button, switch_to_sdl_axis);
}
// Default mappings for when the controller doesn't have an associated gamepad
// This fixes issues where SDL uses a backend which doesn't support gamepad remappings
ButtonMapping mapping;
for (const auto& pair : switch_to_sdl_button) {
SDL_GamepadBinding binding{};
binding.input_type = SDL_GAMEPAD_BINDTYPE_BUTTON;
binding.input.button = pair.second;
mapping.insert_or_assign(pair.first, BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& pair : switch_to_sdl_axis) {
SDL_GamepadBinding binding{};
binding.input_type = SDL_GAMEPAD_BINDTYPE_AXIS;
binding.input.axis.axis = pair.second;
mapping.insert_or_assign(pair.first, BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
ButtonBindings SDLDriver::GetDefaultButtonBinding(
const std::shared_ptr<SDLJoystick>& joystick) const {
// Default SL/SR mapping for other controllers
auto sll_button = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
auto srl_button = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
auto slr_button = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER;
auto srr_button = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER;
if (joystick->IsJoyconLeft()) {
sll_button = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1;
srl_button = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2;
}
if (joystick->IsJoyconRight()) {
slr_button = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2;
srr_button = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1;
}
return {
std::pair{Settings::NativeButton::A, SDL_GAMEPAD_BUTTON_EAST},
{Settings::NativeButton::B, SDL_GAMEPAD_BUTTON_SOUTH},
{Settings::NativeButton::X, SDL_GAMEPAD_BUTTON_NORTH},
{Settings::NativeButton::Y, SDL_GAMEPAD_BUTTON_WEST},
{Settings::NativeButton::LStick, SDL_GAMEPAD_BUTTON_LEFT_STICK},
{Settings::NativeButton::RStick, SDL_GAMEPAD_BUTTON_RIGHT_STICK},
{Settings::NativeButton::L, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER},
{Settings::NativeButton::R, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER},
{Settings::NativeButton::Plus, SDL_GAMEPAD_BUTTON_START},
{Settings::NativeButton::Minus, SDL_GAMEPAD_BUTTON_BACK},
{Settings::NativeButton::DLeft, SDL_GAMEPAD_BUTTON_DPAD_LEFT},
{Settings::NativeButton::DUp, SDL_GAMEPAD_BUTTON_DPAD_UP},
{Settings::NativeButton::DRight, SDL_GAMEPAD_BUTTON_DPAD_RIGHT},
{Settings::NativeButton::DDown, SDL_GAMEPAD_BUTTON_DPAD_DOWN},
{Settings::NativeButton::SLLeft, sll_button},
{Settings::NativeButton::SRLeft, srl_button},
{Settings::NativeButton::SLRight, slr_button},
{Settings::NativeButton::SRRight, srr_button},
{Settings::NativeButton::Home, SDL_GAMEPAD_BUTTON_GUIDE},
{Settings::NativeButton::Screenshot, SDL_GAMEPAD_BUTTON_MISC1},
};
}
ButtonMapping SDLDriver::GetSingleControllerMapping(
const std::shared_ptr<SDLJoystick>& joystick, const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
const auto bindings = GetBindings(controller);
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
const auto binding = GetBindingForButton(bindings, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
const auto binding = GetBindingForAxis(bindings, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
ButtonMapping SDLDriver::GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
const std::shared_ptr<SDLJoystick>& joystick2,
const ButtonBindings& switch_to_sdl_button,
const ZButtonBindings& switch_to_sdl_axis) const {
ButtonMapping mapping;
mapping.reserve(switch_to_sdl_button.size() + switch_to_sdl_axis.size());
auto* controller = joystick->GetSDLGameController();
auto* controller2 = joystick2->GetSDLGameController();
const auto bindings = GetBindings(controller);
const auto bindings2 = GetBindings(controller2);
for (const auto& [switch_button, sdl_button] : switch_to_sdl_button) {
if (IsButtonOnLeftSide(switch_button)) {
const auto binding = GetBindingForButton(bindings2, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto binding = GetBindingForButton(bindings, sdl_button);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
for (const auto& [switch_button, sdl_axis] : switch_to_sdl_axis) {
if (IsButtonOnLeftSide(switch_button)) {
const auto binding = GetBindingForAxis(bindings2, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick2->GetPort(), joystick2->GetGUID(), binding));
continue;
}
const auto binding = GetBindingForAxis(bindings, sdl_axis);
mapping.insert_or_assign(
switch_button,
BuildParamPackageForBinding(joystick->GetPort(), joystick->GetGUID(), binding));
}
return mapping;
}
bool SDLDriver::IsButtonOnLeftSide(Settings::NativeButton::Values button) const {
switch (button) {
case Settings::NativeButton::DDown:
case Settings::NativeButton::DLeft:
case Settings::NativeButton::DRight:
case Settings::NativeButton::DUp:
case Settings::NativeButton::L:
case Settings::NativeButton::LStick:
case Settings::NativeButton::Minus:
case Settings::NativeButton::Screenshot:
case Settings::NativeButton::ZL:
return true;
default:
return false;
}
}
AnalogMapping SDLDriver::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
AnalogMapping mapping = {};
const auto bindings = GetBindings(controller);
const auto binding_left_x = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_LEFTX);
const auto binding_left_y = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_LEFTY);
if (params.Has("guid2")) {
const auto identifier = joystick2->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.input.axis.axis);
PreSetAxis(identifier, binding_left_y.input.axis.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.input.axis.axis);
const auto left_offset_y = GetAxis(identifier, binding_left_y.input.axis.axis);
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, binding_left_x.input.axis.axis,
binding_left_y.input.axis.axis,
left_offset_x, left_offset_y));
} else {
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_left_x.input.axis.axis);
PreSetAxis(identifier, binding_left_y.input.axis.axis);
const auto left_offset_x = -GetAxis(identifier, binding_left_x.input.axis.axis);
const auto left_offset_y = GetAxis(identifier, binding_left_y.input.axis.axis);
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
BuildParamPackageForAnalog(identifier, binding_left_x.input.axis.axis,
binding_left_y.input.axis.axis,
left_offset_x, left_offset_y));
}
const auto binding_right_x = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_RIGHTX);
const auto binding_right_y = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_RIGHTY);
const auto identifier = joystick->GetPadIdentifier();
PreSetController(identifier);
PreSetAxis(identifier, binding_right_x.input.axis.axis);
PreSetAxis(identifier, binding_right_y.input.axis.axis);
const auto right_offset_x = -GetAxis(identifier, binding_right_x.input.axis.axis);
const auto right_offset_y = GetAxis(identifier, binding_right_y.input.axis.axis);
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
BuildParamPackageForAnalog(identifier, binding_right_x.input.axis.axis,
binding_right_y.input.axis.axis, right_offset_x,
right_offset_y));
return mapping;
}
MotionMapping SDLDriver::GetMotionMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return {};
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
const auto joystick2 = GetSDLJoystickByGUID(params.Get("guid2", ""), params.Get("port", 0));
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return {};
}
MotionMapping mapping = {};
joystick->EnableMotion();
if (joystick->HasMotion()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionRight,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
if (params.Has("guid2")) {
joystick2->EnableMotion();
if (joystick2->HasMotion()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick2->GetPort(), joystick2->GetGUID()));
}
} else {
if (joystick->HasMotion()) {
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft,
BuildMotionParam(joystick->GetPort(), joystick->GetGUID()));
}
}
return mapping;
}
Common::Input::ButtonNames SDLDriver::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
// TODO(German77): Find how to substitute the values for real button names
return Common::Input::ButtonNames::Value;
}
if (params.Has("hat")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
std::string SDLDriver::GetHatButtonName(u8 direction_value) const {
switch (direction_value) {
case SDL_HAT_UP:
return "up";
case SDL_HAT_DOWN:
return "down";
case SDL_HAT_LEFT:
return "left";
case SDL_HAT_RIGHT:
return "right";
default:
return {};
}
}
u8 SDLDriver::GetHatButtonId(const std::string& direction_name) const {
Uint8 direction;
if (direction_name == "up") {
direction = SDL_HAT_UP;
} else if (direction_name == "down") {
direction = SDL_HAT_DOWN;
} else if (direction_name == "left") {
direction = SDL_HAT_LEFT;
} else if (direction_name == "right") {
direction = SDL_HAT_RIGHT;
} else {
direction = 0;
}
return direction;
}
bool SDLDriver::IsStickInverted(const Common::ParamPackage& params) {
if (!params.Has("guid") || !params.Has("port")) {
return false;
}
const auto joystick = GetSDLJoystickByGUID(params.Get("guid", ""), params.Get("port", 0));
if (joystick == nullptr) {
return false;
}
auto* controller = joystick->GetSDLGameController();
if (controller == nullptr) {
return false;
}
const auto& axis_x = params.Get("axis_x", 0);
const auto& axis_y = params.Get("axis_y", 0);
const auto bindings = GetBindings(controller);
const auto binding_left_x = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_LEFTX);
const auto binding_right_x = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_RIGHTX);
const auto binding_left_y = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_LEFTY);
const auto binding_right_y = GetBindingForAxis(bindings, SDL_GAMEPAD_AXIS_RIGHTY);
if (axis_x != binding_left_y.input.axis.axis && axis_x != binding_right_y.input.axis.axis) {
return false;
}
if (axis_y != binding_left_x.input.axis.axis && axis_y != binding_right_x.input.axis.axis) {
return false;
}
return true;
}
} // namespace InputCommon