[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>
This commit is contained in:
John 2026-11-07 02:26:31 +01:00 • committed by crueter
parent 885b333c08
commit 873fde41d0
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
57 changed files with 11334 additions and 90 deletions

View file

@ -3,8 +3,19 @@
// 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"
@ -450,6 +461,29 @@ std::shared_ptr<SDLJoystick> SDLDriver::GetSDLJoystickBySDLID(SDL_JoystickID sdl
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) {
@ -535,6 +569,11 @@ void SDLDriver::InitJoystick(SDL_JoystickID joystick_id) {
[](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(),
@ -563,6 +602,10 @@ void SDLDriver::CloseJoystick(SDL_Joystick* 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);
}
}
@ -583,6 +626,7 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
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);
@ -590,6 +634,9 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
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);
@ -597,8 +644,20 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
break;
}
case SDL_EVENT_JOYSTICK_AXIS_MOTION: {
if (const auto joystick = GetSDLJoystickBySDLID(event.jaxis.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
// 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;
@ -640,6 +699,152 @@ void SDLDriver::CloseJoysticks() {
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.
@ -657,6 +862,24 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en
// 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");
@ -727,8 +950,17 @@ std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
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());
fmt::format("{} {}{}", joystick->GetControllerName(), joystick->GetPort(),
steam_virtual ? " (Steam)" : "");
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
@ -744,7 +976,8 @@ std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
// Add dual controllers
for (const auto& [key, value] : joystick_map) {
for (const auto& joystick : value) {
if (joystick->IsJoyconRight()) {
if (joystick->IsJoyconRight() && joystick->GetSDLJoystick() &&
!IsUnhiddenController(joystick->GetSDLJoystick())) {
if (!joycon_pairs.contains(joystick->GetPort())) {
continue;
}

View file

@ -7,8 +7,12 @@
#pragma once
#include <atomic>
#include <chrono>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>
#include "common/container/unordered_map.h"
#include <SDL3/SDL.h>
@ -45,6 +49,15 @@ public:
std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
std::shared_ptr<SDLJoystick> GetSDLJoystickByGamepadID(SDL_JoystickID sdl_id);
/// The guid and port that bindings use to address this SDL instance, if the driver has it
/// open. Must not be called while holding SDL_LockJoysticks (the driver locks its map).
std::optional<std::pair<std::string, int>> GetBindingForSDLID(SDL_JoystickID sdl_id);
/// When this SDL instance last had a deliberate input: a button or d-pad press, or a
/// stick or trigger past half travel (not drift, not motion)
std::optional<std::chrono::steady_clock::time_point> GetLastActivity(
SDL_JoystickID sdl_id) const;
/**
* Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
* tie it to a SDLJoystick with the same guid and that port
@ -117,6 +130,10 @@ private:
::Common::unordered_map<Common::UUID, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
std::mutex joystick_map_mutex;
void MarkActivity(SDL_JoystickID sdl_id);
std::unordered_map<SDL_JoystickID, std::chrono::steady_clock::time_point> last_activity;
mutable std::mutex activity_mutex;
std::atomic<bool> initialized = false;
bool start_thread;
};

View file

@ -25,10 +25,25 @@ public:
using Button = std::unique_ptr<Common::Input::InputDevice>;
Stick(Button up_, Button down_, Button left_, Button right_, Button modifier_, Button updater_,
float modifier_scale_, float modifier_angle_)
Button stick_, float modifier_scale_, float modifier_angle_)
: up(std::move(up_)), down(std::move(down_)), left(std::move(left_)),
right(std::move(right_)), modifier(std::move(modifier_)), updater(std::move(updater_)),
modifier_scale(modifier_scale_), modifier_angle(modifier_angle_) {
stick(std::move(stick_)), modifier_scale(modifier_scale_),
modifier_angle(modifier_angle_) {
// A real stick alongside the buttons: it moves the stick while no button is held
stick_status.x.properties = properties;
stick_status.y.properties = properties;
if (stick) {
stick->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
stick_status = callback_.stick_status;
if (amplitude == 0) {
SoftUpdate();
}
},
});
}
up->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback_) {
@ -220,6 +235,9 @@ public:
left->ForceUpdate();
right->ForceUpdate();
modifier->ForceUpdate();
if (stick) {
stick->ForceUpdate();
}
}
void SoftUpdate() {
@ -237,6 +255,9 @@ public:
}
Common::Input::StickStatus GetStatus() const {
if (stick && amplitude == 0) {
return stick_status;
}
Common::Input::StickStatus status{};
status.x.properties = properties;
status.y.properties = properties;
@ -270,6 +291,8 @@ private:
Button right;
Button modifier;
Button updater;
Button stick;
Common::Input::StickStatus stick_status{};
float modifier_scale{};
float modifier_angle{};
float angle{};
@ -296,9 +319,13 @@ std::unique_ptr<Common::Input::InputDevice> StickFromButton::Create(
auto updater = Common::Input::CreateInputDeviceFromString("engine:updater,button:0");
auto modifier_scale = params.Get("modifier_scale", 0.5f);
auto modifier_angle = params.Get("modifier_angle", 5.5f);
// (Optional: a real stick the buttons move alongside)
Stick::Button stick = params.Has("stick")
? Common::Input::CreateInputDeviceFromString(params.Get("stick", ""))
: nullptr;
return std::make_unique<Stick>(std::move(up), std::move(down), std::move(left),
std::move(right), std::move(modifier), std::move(updater),
modifier_scale, modifier_angle);
std::move(stick), modifier_scale, modifier_angle);
}
} // namespace InputCommon

View file

@ -1,3 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -557,9 +559,9 @@ private:
class InputFromMotion final : public Common::Input::InputDevice {
public:
explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_,
InputEngine* input_engine_)
int turn_, InputEngine* input_engine_)
: identifier(identifier_), motion_sensor(motion_sensor_), gyro_threshold(gyro_threshold_),
input_engine(input_engine_) {
turn(turn_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
@ -589,6 +591,17 @@ public:
status.gyro.x = {.raw_value = basic_motion.gyro_x, .properties = properties};
status.gyro.y = {.raw_value = basic_motion.gyro_y, .properties = properties};
status.gyro.z = {.raw_value = basic_motion.gyro_z, .properties = properties};
// Turned a quarter about the face for a controller standing in for a sideways Joy-Con
// (1: clockwise, as a right one; -1: the other way): the Joy-Con's x is the
// controller's -y, its y the controller's x (the left one: y, and -x)
if (turn != 0) {
for (auto* axes : {&status.accel, &status.gyro}) {
const float x = axes->x.raw_value;
const float y = axes->y.raw_value;
axes->x.raw_value = turn > 0 ? -y : y;
axes->y.raw_value = turn > 0 ? x : -x;
}
}
status.delta_timestamp = basic_motion.delta_timestamp;
return status;
}
@ -606,6 +619,7 @@ private:
const PadIdentifier identifier;
const int motion_sensor;
const float gyro_threshold;
const int turn;
int callback_key;
InputEngine* input_engine;
};
@ -1069,9 +1083,10 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateMotionDevice(
if (params.Has("motion")) {
const auto motion_sensor = params.Get("motion", 0);
const auto gyro_threshold = params.Get("threshold", 0.007f);
const auto turn = std::clamp(params.Get("turn", 0), -1, 1);
input_engine->PreSetController(identifier);
input_engine->PreSetMotion(identifier, motion_sensor);
return std::make_unique<InputFromMotion>(identifier, motion_sensor, gyro_threshold,
return std::make_unique<InputFromMotion>(identifier, motion_sensor, gyro_threshold, turn,
input_engine.get());
}

View file

@ -442,6 +442,23 @@ std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
return impl->GetInputDevices();
}
std::optional<std::pair<std::string, int>> InputSubsystem::GetSDLBinding(u32 sdl_instance) const {
#ifdef HAVE_SDL3
return impl->sdl->GetBindingForSDLID(static_cast<SDL_JoystickID>(sdl_instance));
#else
return std::nullopt;
#endif
}
std::optional<std::chrono::steady_clock::time_point> InputSubsystem::GetSDLLastActivity(
u32 sdl_instance) const {
#ifdef HAVE_SDL3
return impl->sdl->GetLastActivity(static_cast<SDL_JoystickID>(sdl_instance));
#else
return std::nullopt;
#endif
}
AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const {
return impl->GetAnalogMappingForDevice(device);
}

View file

@ -6,10 +6,14 @@
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include "common/container/unordered_map.h"
#include <utility>
#include <vector>
#include "common/common_types.h"
#include "common/container/unordered_map.h"
namespace Common {
class ParamPackage;
@ -152,6 +156,13 @@ public:
/// Returns true if axis of a stick aren't mapped in the correct direction
[[nodiscard]] bool IsStickInverted(const Common::ParamPackage& device) const;
/// The SDL guid and port that bindings use for an SDL joystick instance, if SDL has it open.
[[nodiscard]] std::optional<std::pair<std::string, int>> GetSDLBinding(u32 sdl_instance) const;
/// When an SDL joystick last had a deliberate input (a stick or trigger past half travel)
[[nodiscard]] std::optional<std::chrono::steady_clock::time_point> GetSDLLastActivity(
u32 sdl_instance) const;
/// Reloads the input devices.
void ReloadInputDevices();