From 1b3bf1473d7513d51e2c32ddea4d9a18f9e38b14 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 31 Jan 2026 23:12:29 -0600 Subject: [PATCH] Fix Dual Joy-Con driver and InputView (ryubing/ryujinx!259) See merge request ryubing/ryujinx!259 --- src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs | 147 +++++++++++++++--- src/Ryujinx.Input.SDL3/SDL3JoyCon.cs | 10 ++ src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs | 49 ++---- .../UI/ViewModels/Input/InputViewModel.cs | 23 ++- 4 files changed, 168 insertions(+), 61 deletions(-) diff --git a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs index b1100384f..897966689 100644 --- a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs @@ -9,10 +9,20 @@ using static SDL.SDL3; namespace Ryujinx.Input.SDL3 { + + public unsafe class SDL3GamepadDriver : IGamepadDriver { private readonly Dictionary _gamepadsInstanceIdsMapping; private readonly Dictionary _gamepadsIds; + /// + /// Unlinked joy-cons + /// + private readonly Dictionary _joyConsIds; + /// + /// Linked joy-cons, remove dual joy-con from _gamepadsIds when a linked joy-con is removed + /// + private readonly Dictionary _linkedJoyConsIds; private readonly Lock _lock = new(); public ReadOnlySpan GamepadsIds @@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3 { lock (_lock) { - return _gamepadsIds.Values.ToArray(); + List 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(); _gamepadsIds = []; + _joyConsIds = []; + _linkedJoyConsIds = []; SDL3Driver.Instance.Initialize(); SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected; @@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3 int guidIndex = 0; id = guidIndex + "-" + guidString; - while (_gamepadsIds.ContainsValue(id)) + while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id)) { id = (++guidIndex) + "-" + guidString; } @@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3 private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId) { bool joyConPairDisconnected = false; + string fakeId = null; if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id)) return; lock (_lock) { - _gamepadsIds.Remove(joystickInstanceId); - if (!SDL3JoyConPair.IsCombinable(_gamepadsIds)) + if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId)) { - _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; } } @@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3 OnGamepadDisconnected?.Invoke(id); if (joyConPairDisconnected) { - OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id); + OnGamepadDisconnected?.Invoke(fakeId); } } private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId) { bool joyConPairConnected = false; + string fakeId = null; if (SDL_IsGamepad(joystickInstanceId)) { @@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3 { lock (_lock) { - - _gamepadsIds.Add(joystickInstanceId, id); - - if (SDL3JoyConPair.IsCombinable(_gamepadsIds)) + if (!SDL3JoyCon.IsJoyCon(joystickInstanceId)) { - // TODO - It appears that you can only have one joy con pair connected at a time? - // This was also the behavior before SDL3 - _gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id)); - uint fakeInstanceID = uint.MaxValue; - while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id)) + _gamepadsIds.Add(joystickInstanceId, id); + } + else + { + 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); if (joyConPairConnected) { - OnGamepadConnected?.Invoke(SDL3JoyConPair.Id); + OnGamepadConnected?.Invoke(fakeId); } } } @@ -193,10 +254,22 @@ namespace Ryujinx.Input.SDL3 { OnGamepadDisconnected?.Invoke(gamepad.Value); } + + foreach (var gamepad in _joyConsIds) + { + OnGamepadDisconnected?.Invoke(gamepad.Value); + } + + foreach (var gamepad in _linkedJoyConsIds) + { + OnGamepadDisconnected?.Invoke(gamepad.Value); + } lock (_lock) { _gamepadsIds.Clear(); + _joyConsIds.Clear(); + _linkedJoyConsIds.Clear(); } SDL3Driver.Instance.Dispose(); @@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3 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) { - 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; } - if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix)) + if (SDL3JoyCon.IsJoyCon(instanceId)) { return new SDL3JoyCon(gamepadHandle, id); } @@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3 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); + } + } } } } diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs index 33bab7739..5311a256c 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs @@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3 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; + } } } diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs index 303875eac..14352e5a4 100644 --- a/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs +++ b/src/Ryujinx.Input.SDL3/SDL3JoyConPair.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3 public const string Id = "JoyConPair"; 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 void Dispose() @@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3 right.SetTriggerThreshold(triggerThreshold); } - public static bool IsCombinable(Dictionary gamepadsIds) + public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary joyConIds, out SDL_JoystickID match) { - (int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds); - return leftIndex >= 0 && rightIndex >= 0; - } + bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1); + string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName; + match = 0; - private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary gamepadsIds) - { - Dictionary 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 gamepadsIds) - { - (int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds); - - if (leftIndex <= 0 || rightIndex <= 0) + foreach (var joyConId in joyConIds.Keys) { - return null; + if (SDL_GetGamepadNameForID(joyConId) == matchName) + { + match = joyConId; + + return true; + } } - - SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex); - 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])); + + return false; } } } diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 289dc0e9c..e5f085e0f 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input _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; @@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input IsModified = true; RevertChanges(); FindPairedDeviceInConfigFile(); - + _isChangeTrackingActive = true; // Enable configuration change tracking } @@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input 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 { @@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input DeviceList.Clear(); Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled])); - int controllerNumber = 0; + foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) { using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); @@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (gamepad != null) { + int controllerNumber = 0; string name = GetUniqueGamepadName(gamepad, ref controllerNumber); 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 Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId); - LoadDevice(); + _isLoaded = false; LoadConfiguration(); + LoadDevice(); + _isLoaded = true; OnPropertyChanged(); IsModified = false;