eden-miror/src/input_common/main.h
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

192 lines
6.4 KiB
C++

// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "common/common_types.h"
#include "common/container/unordered_map.h"
namespace Common {
class ParamPackage;
}
namespace Common::Input {
enum class ButtonNames;
}
namespace Settings::NativeAnalog {
enum Values : int;
}
namespace Settings::NativeButton {
enum Values : int;
}
namespace Settings::NativeMotion {
enum Values : int;
}
namespace InputCommon {
class Android;
class Camera;
class Keyboard;
class Mouse;
class TouchScreen;
class VirtualAmiibo;
class VirtualGamepad;
struct MappingData;
} // namespace InputCommon
namespace InputCommon::TasInput {
class Tas;
} // namespace InputCommon::TasInput
namespace InputCommon {
namespace Polling {
/// Type of input desired for mapping purposes
enum class InputType { None, Button, Stick, Motion, Touch };
} // namespace Polling
/**
* Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
* mapping for the device.
*/
using AnalogMapping = ::Common::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
using ButtonMapping = ::Common::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
using MotionMapping = ::Common::unordered_map<Settings::NativeMotion::Values, Common::ParamPackage>;
class InputSubsystem {
public:
explicit InputSubsystem();
~InputSubsystem();
InputSubsystem(const InputSubsystem&) = delete;
InputSubsystem& operator=(const InputSubsystem&) = delete;
InputSubsystem(InputSubsystem&&) = delete;
InputSubsystem& operator=(InputSubsystem&&) = delete;
/// Initializes and registers all built-in input device factories.
void Initialize();
/// Unregisters all built-in input device factories and shuts them down.
void Shutdown();
/// Retrieves the underlying keyboard device.
[[nodiscard]] Keyboard* GetKeyboard();
/// Retrieves the underlying keyboard device.
[[nodiscard]] const Keyboard* GetKeyboard() const;
/// Retrieves the underlying mouse device.
[[nodiscard]] Mouse* GetMouse();
/// Retrieves the underlying mouse device.
[[nodiscard]] const Mouse* GetMouse() const;
/// Retrieves the underlying touch screen device.
[[nodiscard]] TouchScreen* GetTouchScreen();
/// Retrieves the underlying touch screen device.
[[nodiscard]] const TouchScreen* GetTouchScreen() const;
/// Retrieves the underlying tas input device.
[[nodiscard]] TasInput::Tas* GetTas();
/// Retrieves the underlying tas input device.
[[nodiscard]] const TasInput::Tas* GetTas() const;
/// Retrieves the underlying camera input device.
[[nodiscard]] Camera* GetCamera();
/// Retrieves the underlying camera input device.
[[nodiscard]] const Camera* GetCamera() const;
/// Retrieves the underlying android input device.
[[nodiscard]] Android* GetAndroid();
/// Retrieves the underlying android input device.
[[nodiscard]] const Android* GetAndroid() const;
/// Retrieves the underlying virtual amiibo input device.
[[nodiscard]] VirtualAmiibo* GetVirtualAmiibo();
/// Retrieves the underlying virtual amiibo input device.
[[nodiscard]] const VirtualAmiibo* GetVirtualAmiibo() const;
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] VirtualGamepad* GetVirtualGamepad();
/// Retrieves the underlying virtual gamepad input device.
[[nodiscard]] const VirtualGamepad* GetVirtualGamepad() const;
/**
* Returns all available input devices that this Factory can create a new device with.
* Each returned ParamPackage should have a `display` field used for display, a `engine` field
* for backends to determine if this backend is meant to service the request and any other
* information needed to identify this in the backend later.
*/
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const;
/// Retrieves the analog mappings for the given device.
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the button mappings for the given device.
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const;
/// Retrieves the motion mappings for the given device.
[[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const;
/// Returns an enum containing the name to be displayed from the input engine.
[[nodiscard]] Common::Input::ButtonNames GetButtonName(
const Common::ParamPackage& params) const;
/// Returns true if device is a controller.
[[nodiscard]] bool IsController(const Common::ParamPackage& params) const;
/// 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();
/// Start polling from all backends for a desired input type.
void BeginMapping(Polling::InputType type);
/// Returns an input event with mapping information.
[[nodiscard]] Common::ParamPackage GetNextInput() const;
/// Stop polling from all backends.
void StopMapping() const;
/// Signals SDL driver for new input events
void PumpEvents() const;
private:
struct Impl;
std::unique_ptr<Impl> impl;
};
/// Generates a serialized param package for creating a keyboard button device.
std::string GenerateKeyboardParam(int key_code);
/// Generates a serialized param package for creating an analog device taking input from keyboard.
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
int key_modifier, float modifier_scale);
} // namespace InputCommon