From 7f0e82fe481ce9c2cb6eb57e63171421254d2c6b Mon Sep 17 00:00:00 2001 From: Schuay Date: Sat, 18 Apr 2026 13:54:25 +0000 Subject: [PATCH] fix/sdl3-gamepad-bugs (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four SDL3 gamepad fixes, all affecting real users with Switch controllers. **Crash on reconnect / broken motion** — `GetFeaturesFlag()` was calling `SDL_DestroyProperties()` on properties owned by SDL internally, causing a use-after-free. This breaks gyro after the first read and crashes on reconnect. Fix: just don't destroy them. **Thread safety in `GetGamepads()`** — was locking the wrong objects and holding locks across `yield` boundaries, which is undefined behavior with `System.Threading.Lock`. Fix: snapshot IDs under `_lock`, iterate outside it. **Rumble NPE on disconnect** — `UpdateRumble()` called `_gamepad.Rumble()` without a null check; throws if the controller disconnects mid-update. Fix: `_gamepad?.Rumble(...)`. **Linux: HIDAPI conflicts with `hid_nintendo`** — when the `hid_nintendo` kernel module is loaded, it provides evdev nodes for both the gamepad and IMU that SDL3's evdev backend correctly combines. HIDAPI conflicts with it, breaking gyro and wireless hotplug. Fix: detect `/sys/module/hid_nintendo` at startup and disable HIDAPI for Switch controllers automatically. Overridable via `SDL_JOYSTICK_HIDAPI_SWITCH=1`. Tested with an 8BitDo Ultimate Bluetooth Controller on Linux. Co-authored-by: schuay <36006+schuay@users.noreply.github.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/19 --- src/Ryujinx.Input.SDL3/SDL3Gamepad.cs | 4 +++- src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs | 26 +++++++-------------- src/Ryujinx.Input/HLE/NpadController.cs | 2 +- src/Ryujinx.SDL3.Common/SDL3Driver.cs | 8 +++++++ 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs index 2b006147d..4985d8eea 100644 --- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs +++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs @@ -151,7 +151,9 @@ namespace Ryujinx.Input.SDL3 result |= GamepadFeaturesFlag.Led; } SDL_UnlockProperties(propID); - SDL_DestroyProperties(propID); + + // NOTE: Do not call SDL_DestroyProperties here. These properties are owned + // internally by SDL and are freed when SDL_CloseGamepad is called (in Dispose). return result; } diff --git a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs index 897966689..0e2a63579 100644 --- a/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs +++ b/src/Ryujinx.Input.SDL3/SDL3GamepadDriver.cs @@ -331,28 +331,18 @@ namespace Ryujinx.Input.SDL3 public IEnumerable GetGamepads() { - lock (_gamepadsIds) + string[] ids; + lock (_lock) { - foreach (var gamepad in _gamepadsIds) - { - yield return GetGamepad(gamepad.Value); - } + ids = _gamepadsIds.Values + .Concat(_joyConsIds.Values) + .Concat(_linkedJoyConsIds.Values) + .ToArray(); } - lock (_joyConsIds) + foreach (string id in ids) { - foreach (var gamepad in _joyConsIds) - { - yield return GetGamepad(gamepad.Value); - } - } - - lock (_linkedJoyConsIds) - { - foreach (var gamepad in _linkedJoyConsIds) - { - yield return GetGamepad(gamepad.Value); - } + yield return GetGamepad(id); } } } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 84f9e89ab..29bc973f6 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -563,7 +563,7 @@ namespace Ryujinx.Input.HLE float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble)); float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble)); - _gamepad.Rumble(low, high, uint.MaxValue); + _gamepad?.Rumble(low, high, uint.MaxValue); Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + $"L.low.amp={leftVibrationValue.AmplitudeLow}, " + diff --git a/src/Ryujinx.SDL3.Common/SDL3Driver.cs b/src/Ryujinx.SDL3.Common/SDL3Driver.cs index 529a846e0..557c461c5 100644 --- a/src/Ryujinx.SDL3.Common/SDL3Driver.cs +++ b/src/Ryujinx.SDL3.Common/SDL3Driver.cs @@ -61,6 +61,14 @@ namespace Ryujinx.SDL3.Common SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1"); + // When hid_nintendo is loaded, it creates separate evdev devices for the gamepad + // and IMU which SDL3's evdev backend combines via UNIQ matching. Using HIDAPI + // instead conflicts with the kernel driver and breaks motion and hotplug. + if (OperatingSystem.IsLinux() && Directory.Exists("/sys/module/hid_nintendo")) + { + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0"); + } + // NOTE: As of SDL3 2.24.0, joycons are combined by default but the motion source only come from one of them. // We disable this behavior for now. SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");