/ src / Ryujinx.Input.SDL2 / SDL2Gamepad.cs
SDL2Gamepad.cs
  1  using Ryujinx.Common.Configuration.Hid;
  2  using Ryujinx.Common.Configuration.Hid.Controller;
  3  using Ryujinx.Common.Logging;
  4  using System;
  5  using System.Collections.Generic;
  6  using System.Numerics;
  7  using static SDL2.SDL;
  8  
  9  namespace Ryujinx.Input.SDL2
 10  {
 11      class SDL2Gamepad : IGamepad
 12      {
 13          private bool HasConfiguration => _configuration != null;
 14  
 15          private record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From);
 16  
 17          private StandardControllerInputConfig _configuration;
 18  
 19          private static readonly SDL_GameControllerButton[] _buttonsDriverMapping = new SDL_GameControllerButton[(int)GamepadButtonInputId.Count]
 20          {
 21              // Unbound, ignored.
 22              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 23  
 24              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A,
 25              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B,
 26              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X,
 27              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y,
 28              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK,
 29              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSTICK,
 30              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
 31              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
 32  
 33              // NOTE: The left and right trigger are axis, we handle those differently
 34              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 35              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 36  
 37              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_UP,
 38              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_DOWN,
 39              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_LEFT,
 40              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
 41              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_BACK,
 42              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START,
 43              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_GUIDE,
 44              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_MISC1,
 45              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1,
 46              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2,
 47              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3,
 48              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4,
 49              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_TOUCHPAD,
 50  
 51              // Virtual buttons are invalid, ignored.
 52              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 53              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 54              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 55              SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID,
 56          };
 57  
 58          private readonly object _userMappingLock = new();
 59  
 60          private readonly List<ButtonMappingEntry> _buttonsUserMapping;
 61  
 62          private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
 63          {
 64              StickInputId.Unbound,
 65              StickInputId.Left,
 66              StickInputId.Right,
 67          };
 68  
 69          public GamepadFeaturesFlag Features { get; }
 70  
 71          private IntPtr _gamepadHandle;
 72  
 73          private float _triggerThreshold;
 74  
 75          public SDL2Gamepad(IntPtr gamepadHandle, string driverId)
 76          {
 77              _gamepadHandle = gamepadHandle;
 78              _buttonsUserMapping = new List<ButtonMappingEntry>(20);
 79  
 80              Name = SDL_GameControllerName(_gamepadHandle);
 81              Id = driverId;
 82              Features = GetFeaturesFlag();
 83              _triggerThreshold = 0.0f;
 84  
 85              // Enable motion tracking
 86              if (Features.HasFlag(GamepadFeaturesFlag.Motion))
 87              {
 88                  if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, SDL_bool.SDL_TRUE) != 0)
 89                  {
 90                      Logger.Error?.Print(LogClass.Hid, $"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
 91                  }
 92  
 93                  if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, SDL_bool.SDL_TRUE) != 0)
 94                  {
 95                      Logger.Error?.Print(LogClass.Hid, $"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
 96                  }
 97              }
 98          }
 99  
100          private GamepadFeaturesFlag GetFeaturesFlag()
101          {
102              GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
103  
104              if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
105                  SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
106              {
107                  result |= GamepadFeaturesFlag.Motion;
108              }
109  
110              int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
111  
112              if (error == 0)
113              {
114                  result |= GamepadFeaturesFlag.Rumble;
115              }
116  
117              return result;
118          }
119  
120          public string Id { get; }
121          public string Name { get; }
122  
123          public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
124  
125          protected virtual void Dispose(bool disposing)
126          {
127              if (disposing && _gamepadHandle != IntPtr.Zero)
128              {
129                  SDL_GameControllerClose(_gamepadHandle);
130  
131                  _gamepadHandle = IntPtr.Zero;
132              }
133          }
134  
135          public void Dispose()
136          {
137              Dispose(true);
138          }
139  
140          public void SetTriggerThreshold(float triggerThreshold)
141          {
142              _triggerThreshold = triggerThreshold;
143          }
144  
145          public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
146          {
147              if (Features.HasFlag(GamepadFeaturesFlag.Rumble))
148              {
149                  ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
150                  ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
151  
152                  if (durationMs == uint.MaxValue)
153                  {
154                      if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) != 0)
155                      {
156                          Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
157                      }
158                  }
159                  else if (durationMs > SDL_HAPTIC_INFINITY)
160                  {
161                      Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
162                  }
163                  else
164                  {
165                      if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0)
166                      {
167                          Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
168                      }
169                  }
170              }
171          }
172  
173          public Vector3 GetMotionData(MotionInputId inputId)
174          {
175              SDL_SensorType sensorType = SDL_SensorType.SDL_SENSOR_INVALID;
176  
177              if (inputId == MotionInputId.Accelerometer)
178              {
179                  sensorType = SDL_SensorType.SDL_SENSOR_ACCEL;
180              }
181              else if (inputId == MotionInputId.Gyroscope)
182              {
183                  sensorType = SDL_SensorType.SDL_SENSOR_GYRO;
184              }
185  
186              if (Features.HasFlag(GamepadFeaturesFlag.Motion) && sensorType != SDL_SensorType.SDL_SENSOR_INVALID)
187              {
188                  const int ElementCount = 3;
189  
190                  unsafe
191                  {
192                      float* values = stackalloc float[ElementCount];
193  
194                      int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (IntPtr)values, ElementCount);
195  
196                      if (result == 0)
197                      {
198                          Vector3 value = new(values[0], values[1], values[2]);
199  
200                          if (inputId == MotionInputId.Gyroscope)
201                          {
202                              return RadToDegree(value);
203                          }
204  
205                          if (inputId == MotionInputId.Accelerometer)
206                          {
207                              return GsToMs2(value);
208                          }
209  
210                          return value;
211                      }
212                  }
213              }
214  
215              return Vector3.Zero;
216          }
217  
218          private static Vector3 RadToDegree(Vector3 rad)
219          {
220              return rad * (180 / MathF.PI);
221          }
222  
223          private static Vector3 GsToMs2(Vector3 gs)
224          {
225              return gs / SDL_STANDARD_GRAVITY;
226          }
227  
228          public void SetConfiguration(InputConfig configuration)
229          {
230              lock (_userMappingLock)
231              {
232                  _configuration = (StandardControllerInputConfig)configuration;
233  
234                  _buttonsUserMapping.Clear();
235  
236                  // First update sticks
237                  _stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
238                  _stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
239  
240                  // Then left joycon
241                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
242                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
243                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
244                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
245                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
246                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
247                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
248                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
249                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
250                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
251  
252                  // Finally right joycon
253                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
254                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
255                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
256                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
257                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
258                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
259                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
260                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
261                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
262                  _buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
263  
264                  SetTriggerThreshold(_configuration.TriggerThreshold);
265              }
266          }
267  
268          public GamepadStateSnapshot GetStateSnapshot()
269          {
270              return IGamepad.GetStateSnapshot(this);
271          }
272  
273          public GamepadStateSnapshot GetMappedStateSnapshot()
274          {
275              GamepadStateSnapshot rawState = GetStateSnapshot();
276              GamepadStateSnapshot result = default;
277  
278              lock (_userMappingLock)
279              {
280                  if (_buttonsUserMapping.Count == 0)
281                  {
282                      return rawState;
283                  }
284  
285                  foreach (ButtonMappingEntry entry in _buttonsUserMapping)
286                  {
287                      if (entry.From == GamepadButtonInputId.Unbound || entry.To == GamepadButtonInputId.Unbound)
288                      {
289                          continue;
290                      }
291  
292                      // Do not touch state of button already pressed
293                      if (!result.IsPressed(entry.To))
294                      {
295                          result.SetPressed(entry.To, rawState.IsPressed(entry.From));
296                      }
297                  }
298  
299                  (float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
300                  (float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
301  
302                  result.SetStick(StickInputId.Left, leftStickX, leftStickY);
303                  result.SetStick(StickInputId.Right, rightStickX, rightStickY);
304              }
305  
306              return result;
307          }
308  
309          private static float ConvertRawStickValue(short value)
310          {
311              const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
312  
313              return value * ConvertRate;
314          }
315  
316          public (float, float) GetStick(StickInputId inputId)
317          {
318              if (inputId == StickInputId.Unbound)
319              {
320                  return (0.0f, 0.0f);
321              }
322  
323              short stickX;
324              short stickY;
325  
326              if (inputId == StickInputId.Left)
327              {
328                  stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX);
329                  stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY);
330              }
331              else if (inputId == StickInputId.Right)
332              {
333                  stickX = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX);
334                  stickY = SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY);
335              }
336              else
337              {
338                  throw new NotSupportedException($"Unsupported stick {inputId}");
339              }
340  
341              float resultX = ConvertRawStickValue(stickX);
342              float resultY = -ConvertRawStickValue(stickY);
343  
344              if (HasConfiguration)
345              {
346                  if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickX) ||
347                      (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickX))
348                  {
349                      resultX = -resultX;
350                  }
351  
352                  if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.InvertStickY) ||
353                      (inputId == StickInputId.Right && _configuration.RightJoyconStick.InvertStickY))
354                  {
355                      resultY = -resultY;
356                  }
357  
358                  if ((inputId == StickInputId.Left && _configuration.LeftJoyconStick.Rotate90CW) ||
359                      (inputId == StickInputId.Right && _configuration.RightJoyconStick.Rotate90CW))
360                  {
361                      float temp = resultX;
362                      resultX = resultY;
363                      resultY = -temp;
364                  }
365              }
366  
367              return (resultX, resultY);
368          }
369  
370          public bool IsPressed(GamepadButtonInputId inputId)
371          {
372              if (inputId == GamepadButtonInputId.LeftTrigger)
373              {
374                  return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) > _triggerThreshold;
375              }
376  
377              if (inputId == GamepadButtonInputId.RightTrigger)
378              {
379                  return ConvertRawStickValue(SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) > _triggerThreshold;
380              }
381  
382              if (_buttonsDriverMapping[(int)inputId] == SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID)
383              {
384                  return false;
385              }
386  
387              return SDL_GameControllerGetButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]) == 1;
388          }
389      }
390  }