Fix Dual Joy-Con driver and InputView (ryubing/ryujinx!259)

See merge request ryubing/ryujinx!259
This commit is contained in:
LotP 2026-01-31 23:12:29 -06:00
parent 081cdcab0c
commit 1b3bf1473d
4 changed files with 168 additions and 61 deletions

View file

@ -9,10 +9,20 @@ using static SDL.SDL3;
namespace Ryujinx.Input.SDL3 namespace Ryujinx.Input.SDL3
{ {
public unsafe class SDL3GamepadDriver : IGamepadDriver public unsafe class SDL3GamepadDriver : IGamepadDriver
{ {
private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping; private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping;
private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds; private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds;
/// <summary>
/// Unlinked joy-cons
/// </summary>
private readonly Dictionary<SDL_JoystickID, string> _joyConsIds;
/// <summary>
/// Linked joy-cons, remove dual joy-con from <c>_gamepadsIds</c> when a linked joy-con is removed
/// </summary>
private readonly Dictionary<SDL_JoystickID,string> _linkedJoyConsIds;
private readonly Lock _lock = new(); private readonly Lock _lock = new();
public ReadOnlySpan<string> GamepadsIds public ReadOnlySpan<string> GamepadsIds
@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3
{ {
lock (_lock) lock (_lock)
{ {
return _gamepadsIds.Values.ToArray(); List<string> temp = [];
temp.AddRange(_gamepadsIds.Values);
temp.AddRange(_joyConsIds.Values);
temp.AddRange(_linkedJoyConsIds.Values);
return temp.ToArray();
} }
} }
} }
@ -35,6 +49,8 @@ namespace Ryujinx.Input.SDL3
{ {
_gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>(); _gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>();
_gamepadsIds = []; _gamepadsIds = [];
_joyConsIds = [];
_linkedJoyConsIds = [];
SDL3Driver.Instance.Initialize(); SDL3Driver.Instance.Initialize();
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected; SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3
int guidIndex = 0; int guidIndex = 0;
id = guidIndex + "-" + guidString; id = guidIndex + "-" + guidString;
while (_gamepadsIds.ContainsValue(id)) while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id))
{ {
id = (++guidIndex) + "-" + guidString; id = (++guidIndex) + "-" + guidString;
} }
@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3
private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId) private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId)
{ {
bool joyConPairDisconnected = false; bool joyConPairDisconnected = false;
string fakeId = null;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id)) if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return; return;
lock (_lock) lock (_lock)
{ {
_gamepadsIds.Remove(joystickInstanceId); if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId))
if (!SDL3JoyConPair.IsCombinable(_gamepadsIds))
{ {
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id)); if (!_joyConsIds.Remove(joystickInstanceId))
{
_gamepadsIds.Remove(joystickInstanceId);
}
}
else
{
foreach (string matchId in _gamepadsIds.Values)
{
if (matchId.Contains(id))
{
fakeId = matchId;
break;
}
}
string leftId = fakeId!.Split('_')[0];
string rightId = fakeId!.Split('_')[1];
if (leftId == id)
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId));
_joyConsIds.Add(GetInstanceIdFromId(rightId), rightId);
}
else
{
_linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId));
_joyConsIds.Add(GetInstanceIdFromId(leftId), leftId);
}
_linkedJoyConsIds.Remove(joystickInstanceId);
_gamepadsIds.Remove(GetInstanceIdFromId(fakeId));
joyConPairDisconnected = true; joyConPairDisconnected = true;
} }
} }
@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3
OnGamepadDisconnected?.Invoke(id); OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected) if (joyConPairDisconnected)
{ {
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id); OnGamepadDisconnected?.Invoke(fakeId);
} }
} }
private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId) private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId)
{ {
bool joyConPairConnected = false; bool joyConPairConnected = false;
string fakeId = null;
if (SDL_IsGamepad(joystickInstanceId)) if (SDL_IsGamepad(joystickInstanceId))
{ {
@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3
{ {
lock (_lock) lock (_lock)
{ {
if (!SDL3JoyCon.IsJoyCon(joystickInstanceId))
_gamepadsIds.Add(joystickInstanceId, id);
if (SDL3JoyConPair.IsCombinable(_gamepadsIds))
{ {
// TODO - It appears that you can only have one joy con pair connected at a time? _gamepadsIds.Add(joystickInstanceId, id);
// This was also the behavior before SDL3 }
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id)); else
uint fakeInstanceID = uint.MaxValue; {
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id)) if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match))
{ {
fakeInstanceID--; _joyConsIds.Remove(match, out string matchId);
_linkedJoyConsIds.Add(joystickInstanceId, id);
_linkedJoyConsIds.Add(match, matchId);
uint fakeInstanceId = uint.MaxValue;
fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId)
? $"{id}_{matchId}"
: $"{matchId}_{id}";
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId))
{
fakeInstanceId--;
}
_gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId);
joyConPairConnected = true;
}
else
{
_joyConsIds.Add(joystickInstanceId, id);
} }
joyConPairConnected = true;
} }
} }
OnGamepadConnected?.Invoke(id); OnGamepadConnected?.Invoke(id);
if (joyConPairConnected) if (joyConPairConnected)
{ {
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id); OnGamepadConnected?.Invoke(fakeId);
} }
} }
} }
@ -194,9 +255,21 @@ namespace Ryujinx.Input.SDL3
OnGamepadDisconnected?.Invoke(gamepad.Value); OnGamepadDisconnected?.Invoke(gamepad.Value);
} }
foreach (var gamepad in _joyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
foreach (var gamepad in _linkedJoyConsIds)
{
OnGamepadDisconnected?.Invoke(gamepad.Value);
}
lock (_lock) lock (_lock)
{ {
_gamepadsIds.Clear(); _gamepadsIds.Clear();
_joyConsIds.Clear();
_linkedJoyConsIds.Clear();
} }
SDL3Driver.Instance.Dispose(); SDL3Driver.Instance.Dispose();
@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3
public IGamepad GetGamepad(string id) public IGamepad GetGamepad(string id)
{ {
if (id == SDL3JoyConPair.Id) // joy-con pair ids is the combined ids of its parts which are split using a '_'
if (id.Contains('_'))
{ {
lock (_lock) lock (_lock)
{ {
return SDL3JoyConPair.GetGamepad(_gamepadsIds); string leftId = id.Split('_')[0];
string rightId = id.Split('_')[1];
SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId);
SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId);
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId);
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId),
new SDL3JoyCon(rightGamepadHandle, rightId));
} }
} }
@ -232,7 +321,7 @@ namespace Ryujinx.Input.SDL3
return null; return null;
} }
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix)) if (SDL3JoyCon.IsJoyCon(instanceId))
{ {
return new SDL3JoyCon(gamepadHandle, id); return new SDL3JoyCon(gamepadHandle, id);
} }
@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3
yield return GetGamepad(gamepad.Value); yield return GetGamepad(gamepad.Value);
} }
} }
lock (_joyConsIds)
{
foreach (var gamepad in _joyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
lock (_linkedJoyConsIds)
{
foreach (var gamepad in _linkedJoyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
} }
} }
} }

View file

@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3
return SDL_GetGamepadButton(_gamepadHandle, button); return SDL_GetGamepadButton(_gamepadHandle, button);
} }
public static bool IsJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName;
}
public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId)
{
return SDL_GetGamepadNameForID(gamepadsId) is LeftName;
}
} }
} }

View file

@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3
public const string Id = "JoyConPair"; public const string Id = "JoyConPair";
string IGamepad.Id => Id; string IGamepad.Id => Id;
public string Name => "* Nintendo Switch Joy-Con (L/R)"; public string Name => "Nintendo Switch Dual Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true }; public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose() public void Dispose()
@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3
right.SetTriggerThreshold(triggerThreshold); right.SetTriggerThreshold(triggerThreshold);
} }
public static bool IsCombinable(Dictionary<SDL_JoystickID, string> gamepadsIds) public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary<SDL_JoystickID, string> joyConIds, out SDL_JoystickID match)
{ {
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds); bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1);
return leftIndex >= 0 && rightIndex >= 0; string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName;
} match = 0;
private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary<SDL_JoystickID, string> gamepadsIds) foreach (var joyConId in joyConIds.Keys)
{
Dictionary<string, SDL_JoystickID> gamepadNames = gamepadsIds
.Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName)
.Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key))
.ToDictionary();
SDL_JoystickID idx;
int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1;
int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1;
return (leftIndex, rightIndex);
}
public unsafe static IGamepad GetGamepad(Dictionary<SDL_JoystickID, string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
if (leftIndex <= 0 || rightIndex <= 0)
{ {
return null; if (SDL_GetGamepadNameForID(joyConId) == matchName)
{
match = joyConId;
return true;
}
} }
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex); return false;
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex);
if (leftGamepadHandle == null || rightGamepadHandle == null)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]),
new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex]));
} }
} }
} }

View file

@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_controller = 0; _controller = 0;
} }
if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1) if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
{ {
ControllerType controller = Controllers[_controller].Type; ControllerType controller = Controllers[_controller].Type;
@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
{ {
Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
// Avalonia bug: setting a newly instanced ComboBox to 0
// causes the selected item to show up blank
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
Controller = 1;
}
Controller = controllerIndex;
} }
else else
{ {
@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
DeviceList.Clear(); DeviceList.Clear();
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
int controllerNumber = 0;
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
{ {
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (gamepad != null) if (gamepad != null)
{ {
int controllerNumber = 0;
string name = GetUniqueGamepadName(gamepad, ref controllerNumber); string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
Devices.Add((DeviceType.Controller, id, name)); Devices.Add((DeviceType.Controller, id, name));
} }
@ -950,8 +961,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId); Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
LoadDevice(); _isLoaded = false;
LoadConfiguration(); LoadConfiguration();
LoadDevice();
_isLoaded = true;
OnPropertyChanged(); OnPropertyChanged();
IsModified = false; IsModified = false;