Files
ryujinx/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs
Evan Husted 3b5f6170d1 misc: chore: move Rainbow updating to a separate task started/stopped as needed
update gommon & use the Event class from it to allow easily clearing all handlers when the apphost exits to avoid leftover invalid event handlers in the rainbow event handler list.
More robust config application logic to ensure what needs to happen only happens once
2025-01-24 23:06:59 -06:00

430 lines
19 KiB
C#

using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL2;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
class SDL2Gamepad : IGamepad
{
private bool HasConfiguration => _configuration != null;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private StandardControllerInputConfig _configuration;
private static readonly SDL_GameControllerButton[] _buttonsDriverMapping = new SDL_GameControllerButton[(int)GamepadButtonInputId.Count]
{
// Unbound, ignored.
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSTICK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
// NOTE: The left and right trigger are axis, we handle those differently
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_UP,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_LEFT,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_BACK,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_GUIDE,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_MISC1,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_TOUCHPAD,
// Virtual buttons are invalid, ignored.
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
};
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound,
StickInputId.Left,
StickInputId.Right,
};
public GamepadFeaturesFlag Features { get; }
private nint _gamepadHandle;
private float _triggerThreshold;
public SDL2Gamepad(nint gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>(20);
Name = SDL_GameControllerName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid, $"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid, $"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
}
public void SetLed(uint packedRgb)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Led)) return;
byte red = packedRgb > 0 ? (byte)(packedRgb >> 16) : (byte)0;
byte green = packedRgb > 0 ? (byte)(packedRgb >> 8) : (byte)0;
byte blue = packedRgb > 0 ? (byte)(packedRgb % 256) : (byte)0;
if (SDL_GameControllerSetLED(_gamepadHandle, red, green, blue) != 0)
Logger.Error?.Print(LogClass.Hid, "LED is not supported on this game controller.");
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_GameControllerHasRumble(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Rumble;
}
if (SDL_GameControllerHasLED(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Led;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
Rainbow.Updated -= RainbowColorChanged;
SDL_GameControllerClose(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
_triggerThreshold = triggerThreshold;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
return;
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
if (durationMs == uint.MaxValue)
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) != 0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
else if (durationMs > SDL_HAPTIC_INFINITY)
{
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
}
else
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = inputId switch
{
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
_ => SDL_SensorType.SDL_SENSOR_INVALID
};
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
return Vector3.Zero;
const int ElementCount = 3;
unsafe
{
float* values = stackalloc float[ElementCount];
int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (nint)values, ElementCount);
if (result != 0)
return Vector3.Zero;
Vector3 value = new(values[0], values[1], values[2]);
return inputId switch
{
MotionInputId.Gyroscope => RadToDegree(value),
MotionInputId.Accelerometer => GsToMs2(value),
_ => value
};
}
}
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
private void RainbowColorChanged(int packedRgb)
{
if (!_configuration.Led.UseRainbow) return;
SetLed((uint)packedRgb);
}
private bool _rainbowColorEnabled;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
if (Features.HasFlag(GamepadFeaturesFlag.Led) && _configuration.Led.EnableLed)
{
if (_configuration.Led.TurnOffLed)
(this as IGamepad).ClearLed();
else switch (_configuration.Led.UseRainbow)
{
case true when !_rainbowColorEnabled:
Rainbow.Updated += RainbowColorChanged;
_rainbowColorEnabled = true;
break;
case false when _rainbowColorEnabled:
Rainbow.Updated -= RainbowColorChanged;
_rainbowColorEnabled = false;
break;
}
if (!_configuration.Led.TurnOffLed && !_rainbowColorEnabled)
SetLed(_configuration.Led.LedColor);
}
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
// Then left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
// Finally right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
return rawState;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
continue;
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId> GetLogicalJoyStickConfig(StickInputId inputId)
{
switch (inputId)
{
case StickInputId.Left:
if (_configuration.RightJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Left)
return _configuration.RightJoyconStick;
else
return _configuration.LeftJoyconStick;
case StickInputId.Right:
if (_configuration.LeftJoyconStick.Joystick == Common.Configuration.Hid.Controller.StickInputId.Right)
return _configuration.LeftJoyconStick;
else
return _configuration.RightJoyconStick;
}
return null;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
return (0.0f, 0.0f);
(short stickX, short stickY) = GetStickXY(inputId);
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
if (joyconStickConfig != null)
{
if (joyconStickConfig.InvertStickX)
resultX = -resultX;
if (joyconStickConfig.InvertStickY)
resultY = -resultY;
if (joyconStickConfig.Rotate90CW)
{
float temp = resultX;
resultX = resultY;
resultY = -temp;
}
}
}
return (resultX, resultY);
}
// ReSharper disable once InconsistentNaming
private (short, short) GetStickXY(StickInputId inputId) =>
inputId switch
{
StickInputId.Left => (
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX),
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY)),
StickInputId.Right => (
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX),
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY)),
_ => throw new NotSupportedException($"Unsupported stick {inputId}")
};
public bool IsPressed(GamepadButtonInputId inputId)
{
switch (inputId)
{
case GamepadButtonInputId.LeftTrigger:
return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold;
case GamepadButtonInputId.RightTrigger:
return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold;
}
if (_buttonsDriverMapping[(int)inputId] == SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID)
{
return false;
}
return SDL_GameControllerGetButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]) == 1;
}
}
}