diff options
Diffstat (limited to 'src/Ryujinx.Input.SDL2/SDL2Gamepad.cs')
-rw-r--r-- | src/Ryujinx.Input.SDL2/SDL2Gamepad.cs | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs new file mode 100644 index 00000000..92552673 --- /dev/null +++ b/src/Ryujinx.Input.SDL2/SDL2Gamepad.cs @@ -0,0 +1,376 @@ +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Logging; +using System; +using System.Collections.Generic; +using System.Numerics; +using static SDL2.SDL; + +namespace Ryujinx.Input.SDL2 +{ + class SDL2Gamepad : IGamepad + { + private bool HasConfiguration => _configuration != null; + + private record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From); + + 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 object _userMappingLock = new object(); + + private List<ButtonMappingEntry> _buttonsUserMapping; + + private StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count] + { + StickInputId.Unbound, + StickInputId.Left, + StickInputId.Right + }; + + public GamepadFeaturesFlag Features { get; } + + private IntPtr _gamepadHandle; + + private float _triggerThreshold; + + public SDL2Gamepad(IntPtr 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)) + { + SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, SDL_bool.SDL_TRUE); + SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, SDL_bool.SDL_TRUE); + } + } + + 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; + } + + int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100); + + if (error == 0) + { + result |= GamepadFeaturesFlag.Rumble; + } + + 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 != IntPtr.Zero) + { + SDL_GameControllerClose(_gamepadHandle); + + _gamepadHandle = IntPtr.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)) + { + ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); + ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); + + if (durationMs == uint.MaxValue) + { + SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY); + } + else if (durationMs > SDL_HAPTIC_INFINITY) + { + Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}"); + } + else + { + SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs); + } + } + } + + public Vector3 GetMotionData(MotionInputId inputId) + { + SDL_SensorType sensorType = SDL_SensorType.SDL_SENSOR_INVALID; + + if (inputId == MotionInputId.Accelerometer) + { + sensorType = SDL_SensorType.SDL_SENSOR_ACCEL; + } + else if (inputId == MotionInputId.Gyroscope) + { + sensorType = SDL_SensorType.SDL_SENSOR_GYRO; + } + + if (Features.HasFlag(GamepadFeaturesFlag.Motion) && sensorType != SDL_SensorType.SDL_SENSOR_INVALID) + { + const int ElementCount = 3; + + unsafe + { + float* values = stackalloc float[ElementCount]; + + int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (IntPtr)values, ElementCount); + + if (result == 0) + { + Vector3 value = new Vector3(values[0], values[1], values[2]); + + if (inputId == MotionInputId.Gyroscope) + { + return RadToDegree(value); + } + else if (inputId == MotionInputId.Accelerometer) + { + return GsToMs2(value); + } + + return value; + } + } + } + + return Vector3.Zero; + } + + private static Vector3 RadToDegree(Vector3 rad) + { + return rad * (180 / MathF.PI); + } + + private static Vector3 GsToMs2(Vector3 gs) + { + return gs / SDL_STANDARD_GRAVITY; + } + + public void SetConfiguration(InputConfig configuration) + { + lock (_userMappingLock) + { + _configuration = (StandardControllerInputConfig)configuration; + + _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; + } + + foreach (ButtonMappingEntry entry in _buttonsUserMapping) + { + if (entry.From == GamepadButtonInputId.Unbound || entry.To == GamepadButtonInputId.Unbound) + { + 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; + } + + public (float, float) GetStick(StickInputId inputId) + { + if (inputId == StickInputId.Unbound) + { + return (0.0f, 0.0f); + } + + short stickX; + short stickY; + + if (inputId == StickInputId.Left) + { + stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX); + stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY); + } + else if (inputId == StickInputId.Right) + { + stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX); + stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY); + } + else + { + throw new NotSupportedException($"Unsupported stick {inputId}"); + } + + float resultX = ConvertRawStickValue(stickX); + float resultY = -ConvertRawStickValue(stickY); + + if (HasConfiguration) + { + if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickX) || + (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickX)) + { + resultX = -resultX; + } + + if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickY) || + (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickY)) + { + resultY = -resultY; + } + + if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.Rotate90CW) || + (inputId == StickInputId.Right && _configuration.RightJoyconStick.Rotate90CW)) + { + float temp = resultX; + resultX = resultY; + resultY = -temp; + } + } + + return (resultX, resultY); + } + + public bool IsPressed(GamepadButtonInputId inputId) + { + if (inputId == GamepadButtonInputId.LeftTrigger) + { + return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold; + } + else if (inputId == GamepadButtonInputId.RightTrigger) + { + return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold; + } + else if (_buttonsDriverMapping[(int)inputId] == SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID) + { + return false; + } + else + { + return SDL_GameControllerGetButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]) == 1; + } + } + } +} |