SDL3
This commit is contained in:
13
src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj
Normal file
13
src/Ryujinx.Input.SDL3/Ryujinx.Input.SDL3.csproj
Normal file
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
384
src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
Normal file
384
src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
Normal file
@@ -0,0 +1,384 @@
|
||||
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 System.Threading;
|
||||
using static SDL3.SDL;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
class SDL3Gamepad : 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_GamepadButton[] _buttonsDriverMapping =
|
||||
new SDL_GamepadButton[(int)GamepadButtonInputId.Count]
|
||||
{
|
||||
// Unbound, ignored.
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_STICK, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
|
||||
|
||||
// NOTE: The left and right trigger are axis, we handle those differently
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_UP, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_DOWN,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_LEFT, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_BACK, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_GUIDE, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_MISC1,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_TOUCHPAD,
|
||||
|
||||
// Virtual buttons are invalid, ignored.
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID,
|
||||
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID, SDL_GamepadButton.SDL_GAMEPAD_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 SDL3Gamepad(nint gamepadHandle, string driverId)
|
||||
{
|
||||
_gamepadHandle = gamepadHandle;
|
||||
_buttonsUserMapping = new List<ButtonMappingEntry>(20);
|
||||
|
||||
Name = SDL_GetGamepadName(_gamepadHandle);
|
||||
Id = driverId;
|
||||
Features = GetFeaturesFlag();
|
||||
_triggerThreshold = 0.0f;
|
||||
|
||||
// Enable motion tracking
|
||||
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
|
||||
{
|
||||
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Hid,
|
||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
|
||||
}
|
||||
|
||||
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Hid,
|
||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GamepadFeaturesFlag GetFeaturesFlag()
|
||||
{
|
||||
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
|
||||
|
||||
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
|
||||
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
|
||||
{
|
||||
result |= GamepadFeaturesFlag.Motion;
|
||||
}
|
||||
|
||||
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
|
||||
{
|
||||
result |= GamepadFeaturesFlag.Rumble;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _gamepadHandle != nint.Zero)
|
||||
{
|
||||
SDL_CloseGamepad(_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 (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
|
||||
{
|
||||
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;
|
||||
|
||||
float[] values = new float[ElementCount];
|
||||
unsafe
|
||||
{
|
||||
fixed (float* valuesPtr = &values[0])
|
||||
{
|
||||
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, valuesPtr, ElementCount))
|
||||
{
|
||||
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);
|
||||
|
||||
//TODO: miss constant SDL_STANDARD_GRAVITY 9.80665f
|
||||
private static Vector3 GsToMs2(Vector3 gs) => gs / 9.80665f;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
// 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_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
|
||||
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY)),
|
||||
StickInputId.Right => (
|
||||
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTX),
|
||||
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTY)),
|
||||
_ => throw new NotSupportedException($"Unsupported stick {inputId}")
|
||||
};
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
switch (inputId)
|
||||
{
|
||||
case GamepadButtonInputId.LeftTrigger:
|
||||
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
|
||||
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) > _triggerThreshold;
|
||||
case GamepadButtonInputId.RightTrigger:
|
||||
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
|
||||
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) > _triggerThreshold;
|
||||
}
|
||||
|
||||
if (_buttonsDriverMapping[(int)inputId] == SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SDL_GetGamepadButton(_gamepadHandle, _buttonsDriverMapping[(int)inputId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
231
src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs
Normal file
231
src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs
Normal file
@@ -0,0 +1,231 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Input.SDL3;
|
||||
using Ryujinx.SDL3.Common;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using static SDL3.SDL;
|
||||
|
||||
namespace Ryujinx.Input.SDl3
|
||||
{
|
||||
public class SDL3GamepadDriver : IGamepadDriver
|
||||
{
|
||||
private readonly Dictionary<uint, string> _gamepadsInstanceIdsMapping;
|
||||
private readonly List<string> _gamepadsIds;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _gamepadsIds.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string DriverName => "SDL3";
|
||||
|
||||
public event Action<string> OnGamepadConnected;
|
||||
public event Action<string> OnGamepadDisconnected;
|
||||
|
||||
public SDL3GamepadDriver()
|
||||
{
|
||||
_gamepadsInstanceIdsMapping = new Dictionary<uint, string>();
|
||||
_gamepadsIds = new List<string>();
|
||||
|
||||
SDL3Driver.Instance.Initialize();
|
||||
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
|
||||
SDL3Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
|
||||
SDL3Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
|
||||
|
||||
IntPtr joystickArray = SDL_GetJoysticks(out int count);
|
||||
|
||||
var joystickIDs = new int[count];
|
||||
Marshal.Copy(joystickArray, joystickIDs, 0, count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
HandleJoyStickConnected((uint)joystickIDs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateGamepadId(uint joystickIndex)
|
||||
{
|
||||
|
||||
int bufferSize = 33;
|
||||
Span<byte> pszGUID = stackalloc byte[bufferSize];
|
||||
SDL_GUIDToString(SDL_GetJoystickGUIDForID(joystickIndex), pszGUID, bufferSize);
|
||||
var guid = Encoding.UTF8.GetString(pszGUID);
|
||||
|
||||
// if (guid == new SDL_GUID())
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
string id;
|
||||
lock (_lock)
|
||||
{
|
||||
int guidIndex = 0;
|
||||
id = guidIndex + guid;
|
||||
|
||||
while (_gamepadsIds.Contains(id))
|
||||
{
|
||||
id = (++guidIndex) + "-" + guid;
|
||||
}
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private uint GetJoystickIndexByGamepadId(string id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _gamepadsInstanceIdsMapping.FirstOrDefault(x=>x.Value == id).Key;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJoyStickDisconnected(uint joystickInstanceId)
|
||||
{
|
||||
bool joyConPairDisconnected = false;
|
||||
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Remove(id);
|
||||
if (!SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
|
||||
{
|
||||
_gamepadsIds.Remove(SDL3JoyConPair.Id);
|
||||
joyConPairDisconnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
OnGamepadDisconnected?.Invoke(id);
|
||||
if (joyConPairDisconnected)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJoyStickConnected(uint joystickInstanceId)
|
||||
{
|
||||
bool joyConPairConnected = false;
|
||||
|
||||
|
||||
if (SDL_IsGamepad(joystickInstanceId))
|
||||
{
|
||||
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
|
||||
{
|
||||
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
|
||||
// so it is rejected to avoid doubling the entries.
|
||||
return;
|
||||
}
|
||||
|
||||
string id = GenerateGamepadId(joystickInstanceId);
|
||||
if (id == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_gamepadsInstanceIdsMapping.TryAdd(joystickInstanceId, id))
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (joystickInstanceId <= _gamepadsIds.FindLastIndex(_ => true))
|
||||
{
|
||||
// _gamepadsIds.Insert(joystickDeviceId, id);
|
||||
}
|
||||
else
|
||||
_gamepadsIds.Add(id);
|
||||
|
||||
if (SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
|
||||
{
|
||||
_gamepadsIds.Remove(SDL3JoyConPair.Id);
|
||||
_gamepadsIds.Add(SDL3JoyConPair.Id);
|
||||
joyConPairConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
OnGamepadConnected?.Invoke(id);
|
||||
if (joyConPairConnected)
|
||||
{
|
||||
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJoyBatteryUpdated(uint joystickDeviceId, SDL_JoyBatteryEvent joyBatteryEvent)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Hid,
|
||||
$"{SDL_GetGamepadNameForID(joystickDeviceId)}, Battery percent: {joyBatteryEvent.percent}");
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
SDL3Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
|
||||
SDL3Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
|
||||
|
||||
// Simulate a full disconnect when disposing
|
||||
foreach (string id in _gamepadsIds)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(id);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Clear();
|
||||
}
|
||||
|
||||
SDL3Driver.Instance.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
if (id == SDL3JoyConPair.Id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return SDL3JoyConPair.GetGamepad(_gamepadsInstanceIdsMapping);
|
||||
}
|
||||
}
|
||||
|
||||
var instanceId = GetJoystickIndexByGamepadId(id);
|
||||
|
||||
if (instanceId == nint.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
nint gamepadHandle = SDL_OpenGamepad(instanceId);
|
||||
|
||||
if (gamepadHandle == nint.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.WriteLine(SDL_GetGamepadName(gamepadHandle));
|
||||
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
|
||||
{
|
||||
return new SDL3JoyCon(gamepadHandle, id);
|
||||
}
|
||||
|
||||
return new SDL3Gamepad(gamepadHandle, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
420
src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
Normal file
420
src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
Normal file
@@ -0,0 +1,420 @@
|
||||
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 System.Threading;
|
||||
using static SDL3.SDL;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
internal class SDL3JoyCon : 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 readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
|
||||
{
|
||||
{ GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
|
||||
{ GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
|
||||
{ GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
|
||||
{ GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
|
||||
{ GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
|
||||
{ GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
|
||||
{ GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1 },
|
||||
{ GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2 },
|
||||
{ GamepadButtonInputId.SingleRightTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
|
||||
{ GamepadButtonInputId.SingleLeftTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
|
||||
};
|
||||
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
|
||||
{
|
||||
{ GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
|
||||
{ GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
|
||||
{ GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
|
||||
{ GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
|
||||
{ GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
|
||||
{ GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
|
||||
{ GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1 },
|
||||
{ GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2 },
|
||||
{ GamepadButtonInputId.SingleRightTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
|
||||
{ GamepadButtonInputId.SingleLeftTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER }
|
||||
};
|
||||
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _buttonsDriverMapping;
|
||||
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 enum JoyConType
|
||||
{
|
||||
Left, Right
|
||||
}
|
||||
|
||||
public const string Prefix = "Nintendo Switch Joy-Con";
|
||||
public const string LeftName = "Nintendo Switch Joy-Con (L)";
|
||||
public const string RightName = "Nintendo Switch Joy-Con (R)";
|
||||
|
||||
private readonly JoyConType _joyConType;
|
||||
|
||||
public SDL3JoyCon(nint gamepadHandle, string driverId)
|
||||
{
|
||||
_gamepadHandle = gamepadHandle;
|
||||
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
|
||||
|
||||
Name = SDL_GetGamepadName(_gamepadHandle);
|
||||
Id = driverId;
|
||||
Features = GetFeaturesFlag();
|
||||
|
||||
// Enable motion tracking
|
||||
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
|
||||
{
|
||||
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Hid,
|
||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
|
||||
}
|
||||
|
||||
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Hid,
|
||||
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
|
||||
}
|
||||
}
|
||||
|
||||
switch (Name)
|
||||
{
|
||||
case LeftName:
|
||||
{
|
||||
_buttonsDriverMapping = _leftButtonsDriverMapping;
|
||||
_joyConType = JoyConType.Left;
|
||||
break;
|
||||
}
|
||||
case RightName:
|
||||
{
|
||||
_buttonsDriverMapping = _rightButtonsDriverMapping;
|
||||
_joyConType = JoyConType.Right;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private GamepadFeaturesFlag GetFeaturesFlag()
|
||||
{
|
||||
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
|
||||
|
||||
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
|
||||
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
|
||||
{
|
||||
result |= GamepadFeaturesFlag.Motion;
|
||||
}
|
||||
|
||||
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
|
||||
{
|
||||
result |= GamepadFeaturesFlag.Rumble;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _gamepadHandle != nint.Zero)
|
||||
{
|
||||
SDL_CloseGamepad(_gamepadHandle);
|
||||
|
||||
_gamepadHandle = nint.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
|
||||
public void SetTriggerThreshold(float 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 (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
|
||||
{
|
||||
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];
|
||||
|
||||
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, values, ElementCount))
|
||||
{
|
||||
return Vector3.Zero;
|
||||
}
|
||||
|
||||
Vector3 value = _joyConType switch
|
||||
{
|
||||
JoyConType.Left => new Vector3(-values[2], values[1], values[0]),
|
||||
JoyConType.Right => new Vector3(values[2], values[1], -values[0])
|
||||
};
|
||||
|
||||
return inputId switch
|
||||
{
|
||||
MotionInputId.Gyroscope => RadToDegree(value),
|
||||
MotionInputId.Accelerometer => GsToMs2(value),
|
||||
_ => value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
|
||||
|
||||
//TODO: miss constant SDL_STANDARD_GRAVITY 9.80665f
|
||||
private static Vector3 GsToMs2(Vector3 gs) => gs / 9.80665f;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
switch (_joyConType)
|
||||
{
|
||||
case JoyConType.Left:
|
||||
_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));
|
||||
break;
|
||||
case JoyConType.Right:
|
||||
_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));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (inputId == StickInputId.Left && _joyConType == JoyConType.Right ||
|
||||
inputId == StickInputId.Right && _joyConType == JoyConType.Left)
|
||||
{
|
||||
return (0.0f, 0.0f);
|
||||
}
|
||||
|
||||
(short stickX, short stickY) = GetStickXY();
|
||||
|
||||
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 inputId switch
|
||||
{
|
||||
StickInputId.Left when _joyConType == JoyConType.Left => (resultY, -resultX),
|
||||
StickInputId.Right when _joyConType == JoyConType.Right => (-resultY, resultX),
|
||||
_ => (0.0f, 0.0f)
|
||||
};
|
||||
}
|
||||
|
||||
private (short, short) GetStickXY()
|
||||
{
|
||||
return (
|
||||
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
|
||||
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY));
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
if (!_buttonsDriverMapping.TryGetValue(inputId, out var button))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if (SDL_GetGamepadButton(_gamepadHandle, button))
|
||||
// {
|
||||
// Console.WriteLine(inputId+", "+button);
|
||||
// }
|
||||
return SDL_GetGamepadButton(_gamepadHandle, button);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs
Normal file
141
src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using static SDL3.SDL;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
internal class SDL3JoyConPair(IGamepad left, IGamepad right) : IGamepad
|
||||
{
|
||||
private StandardControllerInputConfig _configuration;
|
||||
|
||||
private readonly StickInputId[] _stickUserMapping =
|
||||
[
|
||||
StickInputId.Unbound,
|
||||
StickInputId.Left,
|
||||
StickInputId.Right
|
||||
];
|
||||
|
||||
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
|
||||
(right?.Features ?? GamepadFeaturesFlag.None);
|
||||
|
||||
public const string Id = "JoyConPair";
|
||||
string IGamepad.Id => Id;
|
||||
|
||||
public string Name => "Nintendo Switch Joy-Con (L/R)";
|
||||
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
left?.Dispose();
|
||||
right?.Dispose();
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
return GetStateSnapshot();
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
return inputId switch
|
||||
{
|
||||
MotionInputId.Accelerometer or
|
||||
MotionInputId.Gyroscope => left.GetMotionData(inputId),
|
||||
MotionInputId.SecondAccelerometer => right.GetMotionData(MotionInputId.Accelerometer),
|
||||
MotionInputId.SecondGyroscope => right.GetMotionData(MotionInputId.Gyroscope),
|
||||
_ => Vector3.Zero
|
||||
};
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
return IGamepad.GetStateSnapshot(this);
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
return inputId switch
|
||||
{
|
||||
StickInputId.Left => left.GetStick(StickInputId.Left),
|
||||
StickInputId.Right => right.GetStick(StickInputId.Right),
|
||||
_ => (0, 0)
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
return left.IsPressed(inputId) || right.IsPressed(inputId);
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
if (lowFrequency != 0)
|
||||
{
|
||||
right.Rumble(lowFrequency, lowFrequency, durationMs);
|
||||
}
|
||||
|
||||
if (highFrequency != 0)
|
||||
{
|
||||
left.Rumble(highFrequency, highFrequency, durationMs);
|
||||
}
|
||||
|
||||
if (lowFrequency == 0 && highFrequency == 0)
|
||||
{
|
||||
left.Rumble(0, 0, durationMs);
|
||||
right.Rumble(0, 0, durationMs);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
left.SetConfiguration(configuration);
|
||||
right.SetConfiguration(configuration);
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
left.SetTriggerThreshold(triggerThreshold);
|
||||
right.SetTriggerThreshold(triggerThreshold);
|
||||
}
|
||||
|
||||
public static bool IsCombinable(Dictionary<uint, string> gamepadsInstanceIdsMapping)
|
||||
{
|
||||
(uint leftIndex, uint rightIndex) = DetectJoyConPair(gamepadsInstanceIdsMapping);
|
||||
return leftIndex != 0 && rightIndex != 0;
|
||||
}
|
||||
|
||||
private static (uint leftInstance, uint rightInstance) DetectJoyConPair(
|
||||
Dictionary<uint, string> gamepadsInstanceIdsMapping)
|
||||
{
|
||||
var leftInstance = gamepadsInstanceIdsMapping
|
||||
.FirstOrDefault(item => SDL_GetGamepadNameForID(item.Key) == SDL3JoyCon.LeftName).Key;
|
||||
var rightInstance = gamepadsInstanceIdsMapping
|
||||
.FirstOrDefault(item => SDL_GetGamepadNameForID(item.Key) == SDL3JoyCon.RightName).Key;
|
||||
|
||||
return (leftInstance, rightInstance);
|
||||
}
|
||||
|
||||
public static IGamepad GetGamepad(Dictionary<uint, string> gamepadsInstanceIdsMapping)
|
||||
{
|
||||
(uint leftInstance, uint rightInstance) = DetectJoyConPair(gamepadsInstanceIdsMapping);
|
||||
if (leftInstance == 0 || rightInstance == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
nint leftGamepadHandle = SDL_OpenGamepad(leftInstance);
|
||||
nint rightGamepadHandle = SDL_OpenGamepad(rightInstance);
|
||||
|
||||
if (leftGamepadHandle == nint.Zero || rightGamepadHandle == nint.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsInstanceIdsMapping[leftInstance]),
|
||||
new SDL3JoyCon(rightGamepadHandle, gamepadsInstanceIdsMapping[rightInstance]));
|
||||
}
|
||||
}
|
||||
}
|
||||
405
src/Ryujinx.Input.SDL3/SDL3Keyboard.cs
Normal file
405
src/Ryujinx.Input.SDL3/SDL3Keyboard.cs
Normal file
@@ -0,0 +1,405 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using static SDL3.SDL;
|
||||
|
||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
class SDL3Keyboard : IKeyboard
|
||||
{
|
||||
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
|
||||
{
|
||||
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
|
||||
}
|
||||
|
||||
private readonly Lock _userMappingLock = new();
|
||||
|
||||
#pragma warning disable IDE0052 // Remove unread private member
|
||||
private readonly SDL3KeyboardDriver _driver;
|
||||
#pragma warning restore IDE0052
|
||||
private StandardKeyboardInputConfig _configuration;
|
||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
||||
|
||||
private static readonly SDL_Keycode[] _keysDriverMapping = new SDL_Keycode[(int)Key.Count]
|
||||
{
|
||||
// INVALID
|
||||
SDL_Keycode.SDLK_0,
|
||||
// Presented as modifiers, so invalid here.
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
|
||||
SDL_Keycode.SDLK_F1,
|
||||
SDL_Keycode.SDLK_F2,
|
||||
SDL_Keycode.SDLK_F3,
|
||||
SDL_Keycode.SDLK_F4,
|
||||
SDL_Keycode.SDLK_F5,
|
||||
SDL_Keycode.SDLK_F6,
|
||||
SDL_Keycode.SDLK_F7,
|
||||
SDL_Keycode.SDLK_F8,
|
||||
SDL_Keycode.SDLK_F9,
|
||||
SDL_Keycode.SDLK_F10,
|
||||
SDL_Keycode.SDLK_F11,
|
||||
SDL_Keycode.SDLK_F12,
|
||||
SDL_Keycode.SDLK_F13,
|
||||
SDL_Keycode.SDLK_F14,
|
||||
SDL_Keycode.SDLK_F15,
|
||||
SDL_Keycode.SDLK_F16,
|
||||
SDL_Keycode.SDLK_F17,
|
||||
SDL_Keycode.SDLK_F18,
|
||||
SDL_Keycode.SDLK_F19,
|
||||
SDL_Keycode.SDLK_F20,
|
||||
SDL_Keycode.SDLK_F21,
|
||||
SDL_Keycode.SDLK_F22,
|
||||
SDL_Keycode.SDLK_F23,
|
||||
SDL_Keycode.SDLK_F24,
|
||||
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_0,
|
||||
|
||||
SDL_Keycode.SDLK_UP,
|
||||
SDL_Keycode.SDLK_DOWN,
|
||||
SDL_Keycode.SDLK_LEFT,
|
||||
SDL_Keycode.SDLK_RIGHT,
|
||||
SDL_Keycode.SDLK_RETURN,
|
||||
SDL_Keycode.SDLK_ESCAPE,
|
||||
SDL_Keycode.SDLK_SPACE,
|
||||
SDL_Keycode.SDLK_TAB,
|
||||
SDL_Keycode.SDLK_BACKSPACE,
|
||||
SDL_Keycode.SDLK_INSERT,
|
||||
SDL_Keycode.SDLK_DELETE,
|
||||
SDL_Keycode.SDLK_PAGEUP,
|
||||
SDL_Keycode.SDLK_PAGEDOWN,
|
||||
SDL_Keycode.SDLK_HOME,
|
||||
SDL_Keycode.SDLK_END,
|
||||
SDL_Keycode.SDLK_CAPSLOCK,
|
||||
SDL_Keycode.SDLK_SCROLLLOCK,
|
||||
SDL_Keycode.SDLK_PRINTSCREEN,
|
||||
SDL_Keycode.SDLK_PAUSE,
|
||||
SDL_Keycode.SDLK_NUMLOCKCLEAR,
|
||||
SDL_Keycode.SDLK_CLEAR,
|
||||
SDL_Keycode.SDLK_KP_0,
|
||||
SDL_Keycode.SDLK_KP_1,
|
||||
SDL_Keycode.SDLK_KP_2,
|
||||
SDL_Keycode.SDLK_KP_3,
|
||||
SDL_Keycode.SDLK_KP_4,
|
||||
SDL_Keycode.SDLK_KP_5,
|
||||
SDL_Keycode.SDLK_KP_6,
|
||||
SDL_Keycode.SDLK_KP_7,
|
||||
SDL_Keycode.SDLK_KP_8,
|
||||
SDL_Keycode.SDLK_KP_9,
|
||||
SDL_Keycode.SDLK_KP_DIVIDE,
|
||||
SDL_Keycode.SDLK_KP_MULTIPLY,
|
||||
SDL_Keycode.SDLK_KP_MINUS,
|
||||
SDL_Keycode.SDLK_KP_PLUS,
|
||||
SDL_Keycode.SDLK_KP_DECIMAL,
|
||||
SDL_Keycode.SDLK_KP_ENTER,
|
||||
SDL_Keycode.SDLK_A,
|
||||
SDL_Keycode.SDLK_B,
|
||||
SDL_Keycode.SDLK_C,
|
||||
SDL_Keycode.SDLK_D,
|
||||
SDL_Keycode.SDLK_E,
|
||||
SDL_Keycode.SDLK_F,
|
||||
SDL_Keycode.SDLK_G,
|
||||
SDL_Keycode.SDLK_H,
|
||||
SDL_Keycode.SDLK_I,
|
||||
SDL_Keycode.SDLK_J,
|
||||
SDL_Keycode.SDLK_K,
|
||||
SDL_Keycode.SDLK_L,
|
||||
SDL_Keycode.SDLK_M,
|
||||
SDL_Keycode.SDLK_N,
|
||||
SDL_Keycode.SDLK_O,
|
||||
SDL_Keycode.SDLK_P,
|
||||
SDL_Keycode.SDLK_Q,
|
||||
SDL_Keycode.SDLK_R,
|
||||
SDL_Keycode.SDLK_S,
|
||||
SDL_Keycode.SDLK_T,
|
||||
SDL_Keycode.SDLK_U,
|
||||
SDL_Keycode.SDLK_V,
|
||||
SDL_Keycode.SDLK_W,
|
||||
SDL_Keycode.SDLK_X,
|
||||
SDL_Keycode.SDLK_Y,
|
||||
SDL_Keycode.SDLK_Z,
|
||||
SDL_Keycode.SDLK_0,
|
||||
SDL_Keycode.SDLK_1,
|
||||
SDL_Keycode.SDLK_2,
|
||||
SDL_Keycode.SDLK_3,
|
||||
SDL_Keycode.SDLK_4,
|
||||
SDL_Keycode.SDLK_5,
|
||||
SDL_Keycode.SDLK_6,
|
||||
SDL_Keycode.SDLK_7,
|
||||
SDL_Keycode.SDLK_8,
|
||||
SDL_Keycode.SDLK_9,
|
||||
SDL_Keycode.SDLK_GRAVE,
|
||||
SDL_Keycode.SDLK_GRAVE,
|
||||
SDL_Keycode.SDLK_MINUS,
|
||||
SDL_Keycode.SDLK_PLUS,
|
||||
SDL_Keycode.SDLK_LEFTBRACKET,
|
||||
SDL_Keycode.SDLK_RIGHTBRACKET,
|
||||
SDL_Keycode.SDLK_SEMICOLON,
|
||||
SDL_Keycode.SDLK_APOSTROPHE,
|
||||
SDL_Keycode.SDLK_COMMA,
|
||||
SDL_Keycode.SDLK_PERIOD,
|
||||
SDL_Keycode.SDLK_SLASH,
|
||||
SDL_Keycode.SDLK_BACKSLASH,
|
||||
|
||||
// Invalids
|
||||
SDL_Keycode.SDLK_0,
|
||||
};
|
||||
|
||||
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
|
||||
{
|
||||
_driver = driver;
|
||||
Id = id;
|
||||
Name = name;
|
||||
_buttonsUserMapping = new List<ButtonMappingEntry>();
|
||||
}
|
||||
|
||||
private bool HasConfiguration => _configuration != null;
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No operations
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ToSDL2Scancode(Key key)
|
||||
{
|
||||
if (key >= Key.Unknown && key <= Key.Menu)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
IntPtr modstate = (int)SDL_Keymod.SDL_KMOD_NONE;
|
||||
return (int)SDL_GetScancodeFromKey((uint)_keysDriverMapping[(int)key], modstate);
|
||||
}
|
||||
|
||||
private static SDL_Keymod GetKeyboardModifierMask(Key key)
|
||||
{
|
||||
return key switch
|
||||
{
|
||||
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
||||
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
||||
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
||||
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
||||
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
||||
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
||||
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
||||
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
||||
// NOTE: Menu key isn't supported by SDL2.
|
||||
_ => SDL_Keymod.SDL_KMOD_NONE,
|
||||
};
|
||||
}
|
||||
|
||||
public KeyboardStateSnapshot GetKeyboardStateSnapshot()
|
||||
{
|
||||
Span<SDLBool> rawKeyboardState;
|
||||
SDL_Keymod rawKeyboardModifierState = SDL_GetModState();
|
||||
|
||||
unsafe
|
||||
{
|
||||
rawKeyboardState = SDL_GetKeyboardState(out int numKeys);
|
||||
}
|
||||
|
||||
bool[] keysState = new bool[(int)Key.Count];
|
||||
|
||||
for (Key key = 0; key < Key.Count; key++)
|
||||
{
|
||||
int index = ToSDL2Scancode(key);
|
||||
if (index == -1)
|
||||
{
|
||||
SDL_Keymod modifierMask = GetKeyboardModifierMask(key);
|
||||
|
||||
if (modifierMask == SDL_Keymod.SDL_KMOD_NONE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
keysState[(int)key] = (rawKeyboardModifierState & modifierMask) == modifierMask;
|
||||
}
|
||||
else
|
||||
{
|
||||
keysState[(int)key] = rawKeyboardState[index];
|
||||
}
|
||||
}
|
||||
|
||||
return new KeyboardStateSnapshot(keysState);
|
||||
}
|
||||
|
||||
private static float ConvertRawStickValue(short value)
|
||||
{
|
||||
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
|
||||
|
||||
return value * ConvertRate;
|
||||
}
|
||||
|
||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
||||
{
|
||||
short stickX = 0;
|
||||
short stickY = 0;
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
||||
{
|
||||
stickY += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
||||
{
|
||||
stickY -= 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
||||
{
|
||||
stickX += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
||||
{
|
||||
stickX -= 1;
|
||||
}
|
||||
|
||||
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
||||
|
||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
||||
GamepadStateSnapshot result = default;
|
||||
|
||||
lock (_userMappingLock)
|
||||
{
|
||||
if (!HasConfiguration)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
||||
{
|
||||
if (entry.From == Key.Unknown || entry.From == Key.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));
|
||||
}
|
||||
}
|
||||
|
||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||
|
||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool IsPressed(Key key)
|
||||
{
|
||||
// We only implement GetKeyboardStateSnapshot.
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
lock (_userMappingLock)
|
||||
{
|
||||
_configuration = (StandardKeyboardInputConfig)configuration;
|
||||
|
||||
// First clear the buttons mapping
|
||||
_buttonsUserMapping.Clear();
|
||||
|
||||
// Then configure left joycon
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
||||
|
||||
// Finally configure right joycon
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
// No operations
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
// No operations
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
// No operations
|
||||
|
||||
return Vector3.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
src/Ryujinx.Input.SDL3/SDL3KeyboardDriver.cs
Normal file
55
src/Ryujinx.Input.SDL3/SDL3KeyboardDriver.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Ryujinx.SDL3.Common;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
public class SDL3KeyboardDriver : IGamepadDriver
|
||||
{
|
||||
public SDL3KeyboardDriver()
|
||||
{
|
||||
SDL3Driver.Instance.Initialize();
|
||||
}
|
||||
|
||||
public string DriverName => "SDL3";
|
||||
|
||||
private static readonly string[] _keyboardIdentifers = new string[1] { "0" };
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
||||
|
||||
public event Action<string> OnGamepadConnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<string> OnGamepadDisconnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
SDL3Driver.Instance.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
if (!_keyboardIdentifers[0].Equals(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Ryujinx.Input.SDL3/SDL3Mouse.cs
Normal file
90
src/Ryujinx.Input.SDL3/SDL3Mouse.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
public class SDL3Mouse : IMouse
|
||||
{
|
||||
private SDL3MouseDriver _driver;
|
||||
|
||||
public GamepadFeaturesFlag Features => throw new NotImplementedException();
|
||||
|
||||
public string Id => "0";
|
||||
|
||||
public string Name => "SDL3Mouse";
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public bool[] Buttons => _driver.PressedButtons;
|
||||
|
||||
Size IMouse.ClientSize => _driver.GetClientSize();
|
||||
|
||||
public SDL3Mouse(SDL3MouseDriver driver)
|
||||
{
|
||||
_driver = driver;
|
||||
}
|
||||
|
||||
public Vector2 GetPosition()
|
||||
{
|
||||
return _driver.CurrentPosition;
|
||||
}
|
||||
|
||||
public Vector2 GetScroll()
|
||||
{
|
||||
return _driver.Scroll;
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return _driver.IsButtonPressed(button);
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_driver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/Ryujinx.Input.SDL3/SDL3MouseDriver.cs
Normal file
179
src/Ryujinx.Input.SDL3/SDL3MouseDriver.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using static SDL3.SDL;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
public class SDL3MouseDriver : IGamepadDriver
|
||||
{
|
||||
private const int CursorHideIdleTime = 5; // seconds
|
||||
|
||||
private bool _isDisposed;
|
||||
private readonly HideCursorMode _hideCursorMode;
|
||||
private bool _isHidden;
|
||||
private long _lastCursorMoveTime;
|
||||
|
||||
public bool[] PressedButtons { get; }
|
||||
|
||||
public Vector2 CurrentPosition { get; private set; }
|
||||
public Vector2 Scroll { get; private set; }
|
||||
public Size ClientSize;
|
||||
|
||||
public SDL3MouseDriver(HideCursorMode hideCursorMode)
|
||||
{
|
||||
PressedButtons = new bool[(int)MouseButton.Count];
|
||||
_hideCursorMode = hideCursorMode;
|
||||
|
||||
if (_hideCursorMode == HideCursorMode.Always)
|
||||
{
|
||||
if (!SDL_HideCursor())
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
|
||||
}
|
||||
|
||||
_isHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static MouseButton DriverButtonToMouseButton(uint rawButton)
|
||||
{
|
||||
Debug.Assert(rawButton is > 0 and <= (int)MouseButton.Count);
|
||||
|
||||
return (MouseButton)(rawButton - 1);
|
||||
}
|
||||
|
||||
public void UpdatePosition()
|
||||
{
|
||||
_ = SDL_GetMouseState(out float posX, out float posY);
|
||||
Vector2 position = new(posX, posY);
|
||||
|
||||
if (CurrentPosition != position)
|
||||
{
|
||||
CurrentPosition = position;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
CheckIdle();
|
||||
}
|
||||
|
||||
private void CheckIdle()
|
||||
{
|
||||
if (_hideCursorMode != HideCursorMode.OnIdle)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
|
||||
|
||||
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
|
||||
{
|
||||
if (!_isHidden)
|
||||
{
|
||||
if (!SDL_HideCursor())
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
|
||||
}
|
||||
|
||||
_isHidden = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_isHidden)
|
||||
{
|
||||
if (!SDL_HideCursor())
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Failed to enable the cursor.");
|
||||
}
|
||||
|
||||
_isHidden = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(SDL_Event evnt)
|
||||
{
|
||||
var type = (SDL_EventType)evnt.type;
|
||||
switch (type)
|
||||
{
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
uint rawButton = evnt.button.button;
|
||||
|
||||
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
|
||||
{
|
||||
PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN;
|
||||
|
||||
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// NOTE: On Linux using Wayland mouse motion events won't be received at all.
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_MOTION:
|
||||
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_WHEEL:
|
||||
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetClientSize(int width, int height)
|
||||
{
|
||||
ClientSize = new Size(width, height);
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return PressedButtons[(int)button];
|
||||
}
|
||||
|
||||
public Size GetClientSize()
|
||||
{
|
||||
return ClientSize;
|
||||
}
|
||||
|
||||
public string DriverName => "SDL3";
|
||||
|
||||
public event Action<string> OnGamepadConnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<string> OnGamepadDisconnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
return new SDL3Mouse(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user