From 5566e752a4bde2520e8a187bfe035ca8ef6a765a Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 26 Jun 2026 05:11:19 +0000 Subject: [PATCH] [HID] Restructure HD Rumble class for future controller support (#109) - Attempted fixing the strength: so far it hasn't been successful. - Rumble should skip vibrations if they're not in-line with poll-rate: would like to come back to this. Queuing just does exactly what the hid buffer does, but our timer (poll rate) is not in sync with the rate the controller is reading at, which causes excess drops. - Refactored the class so that implementing support for HD rumble for other controllers (DS5, Steam Controller) is much easier in the future. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/109 --- assets/Locales/Root.json | 2 +- .../Services/Hid/HidDevices/NpadDevices.cs | 2 +- src/Ryujinx.Input.SDL3/NpadHdRumble.cs | 251 ++++++++++++------ src/Ryujinx.Input/HLE/NpadController.cs | 8 +- src/Ryujinx.Input/InputConfigDefaults.cs | 2 +- .../ConfigurationState.Migration.cs | 2 +- 6 files changed, 176 insertions(+), 91 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index d93a2b5b7..9ca437a83 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -9606,7 +9606,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Sends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave ON if you're using JoyCons or a Pro Controller.", + "en_US": "EXPERIMENTAL.\n\nSends more data to the controller for better rumble.\n\nCurrently only supports first-party Nintendo Switch controllers.\n\nLeave OFF if unsure.", "es_ES": "", "fr_FR": "", "he_IL": "", diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs index 5acee3bb7..21a4222a2 100644 --- a/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs +++ b/src/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs @@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid private readonly bool[] _supportedPlayers; private VibrationValue _neutralVibrationValue = new() { - AmplitudeLow = 0f, + AmplitudeLow = 0.01f, FrequencyLow = 160f, AmplitudeHigh = 0f, FrequencyHigh = 320f, diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs index 408b5213b..55d83e69c 100644 --- a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs +++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs @@ -13,84 +13,98 @@ namespace Ryujinx.Input.SDL3 { private readonly SDL_hid_device* _hidHandle; + private byte[] _buffer; + private static ushort _vendor; + private static ushort _product; + private int _globalCount; private ulong _lastWriteTicks; private NpadHdRumble(SDL_hid_device* hidHandle) { _hidHandle = hidHandle; + InitializeDevice(); } public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle) { - ushort vendor = SDL_GetGamepadVendor(gamepadHandle); - if (vendor != 0x057e) + _vendor = SDL_GetGamepadVendor(gamepadHandle); + if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor)) { return null; } - ushort product = SDL_GetGamepadProduct(gamepadHandle); - if (!Enum.IsDefined(typeof(HDRumbleSupported), product)) + _product = SDL_GetGamepadProduct(gamepadHandle); + if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product)) { return null; } - return new NpadHdRumble(SDL_hid_open(vendor, product, 0)); + int serialNumber = 0; + string? serial = SDL_GetGamepadSerial(gamepadHandle); + if (serial is not null) + { + int.TryParse(serial, out serialNumber); + } + + return new NpadHdRumble(SDL_hid_open(_vendor, _product, serialNumber)); } // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble - private bool WriteHdRumble( - int encLeftLowFreq, int encLeftLowAmp, - int encLeftHighFreq, int encLeftHighAmp, - int encRightLowFreq, int encRightLowAmp, - int encRightHighFreq, int encRightHighAmp) + private bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right) { - byte[] buf = new byte[10]; - - buf[0] = 0x10; - buf[1] = (byte)((++_globalCount) & 0xF); - - buf[2] = (byte)(encLeftHighFreq & 0xFF); - buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF)); - buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF)); - buf[5] = (byte)(encLeftLowAmp & 0xFF); - - buf[6] = (byte)(encRightHighFreq & 0xFF); - buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF)); - buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF)); - buf[9] = (byte)(encRightLowAmp & 0xFF); - + int leftLowAmp = EncodeLowAmp(left.AmplitudeLow); + int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8); + int leftHighFreq = EncodeHighFreq(left.FrequencyHigh); + int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8); + + int rightLowAmp = EncodeLowAmp(right.AmplitudeLow); + int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8); + int rightHighFreq = EncodeHighFreq(right.FrequencyHigh); + int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8); + + _buffer[0] = 0x10; + _buffer[1] = (byte)((_globalCount++) & 0xF); + + // Left LRA + _buffer[2] = (byte)(leftLowFreq & 0xFF); + _buffer[3] = (byte)(leftHighAmp & 0xFF); + _buffer[4] = (byte)(leftHighFreq & 0xFF); + _buffer[5] = (byte)(leftLowAmp & 0xFF); + + // Right LRA + _buffer[6] = (byte)(rightLowFreq & 0xFF); + _buffer[7] = (byte)(rightHighAmp & 0xFF); + _buffer[8] = (byte)(rightHighFreq & 0xFF); + _buffer[9] = (byte)(rightLowAmp & 0xFF); + if (_globalCount > 0xF) { _globalCount = 0x0; } - - fixed (byte* ptr = buf) + + fixed (byte* ptr = _buffer) { - if (SendHDRumble(ptr, (nuint)buf.Length) >= 0) + if (SendHdRumble(ptr, (nuint)_buffer.Length) >= 0) { return true; } - if (!String.IsNullOrEmpty(SDL_GetError())) - { - Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); - SDL_ClearError(); - } - return false; + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); } + + return false; } private static int EncodeLowFreq(float lowFreq) { - float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); - return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40); + return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f); } private static int EncodeHighFreq(float highFreq) { - float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); - return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4); + return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f); } private static int EncodeLowAmp(float rawAmp) @@ -98,23 +112,20 @@ namespace Ryujinx.Input.SDL3 double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) - { encodedAmp = 1; - } + else if (rawAmp is >= 0.012f and < 0.112f) - { encodedAmp = 4 * Math.Log2(rawAmp * 110f); - } + else if (rawAmp is >= 0.112f and < 0.225f) - { encodedAmp = 16 * Math.Log2(rawAmp * 17f); - } + else if (rawAmp is >= 0.225f and <= 1f) - { encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); - } - - return (int)Math.Floor(encodedAmp / 2.0) + 64; + + encodedAmp = Math.Round((encodedAmp / 2.0) + 64.0); + encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867); + return (int)Math.Round(encodedAmp); } private static int EncodeHighAmp(float rawAmp) @@ -122,82 +133,156 @@ namespace Ryujinx.Input.SDL3 double encodedAmp = 0; if (rawAmp is > 0 and < 0.012f) - { encodedAmp = 1; - } + else if (rawAmp is >= 0.012f and < 0.112f) - { encodedAmp = 4 * Math.Log2(rawAmp * 110f); - } + else if (rawAmp is >= 0.112f and < 0.225f) - { encodedAmp = 16 * Math.Log2(rawAmp * 17f); - } + else if (rawAmp is >= 0.225f and <= 1f) - { encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); - } - - return (int) Math.Round(encodedAmp * 2); + + encodedAmp = Math.Round(encodedAmp / 2.0); + encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867); + return (int)encodedAmp; } public bool HdRumble(VibrationValue left, VibrationValue right) { - return WriteHdRumble(EncodeLowFreq(left.FrequencyLow), - EncodeLowAmp(left.AmplitudeLow), - EncodeHighFreq(left.FrequencyHigh), - EncodeHighAmp(left.AmplitudeHigh), - EncodeLowFreq(right.FrequencyLow), - EncodeLowAmp(right.AmplitudeLow), - EncodeHighFreq(right.FrequencyHigh), - EncodeHighAmp(right.AmplitudeHigh)); + if(_product is (ushort) HDRumbleSupportedProduct.ProController + or (ushort) HDRumbleSupportedProduct.JoyconLeft + or (ushort) HDRumbleSupportedProduct.JoyconRight + or (ushort) HDRumbleSupportedProduct.JoyconPair + or (ushort) HDRumbleSupportedProduct.JoyconGrip) + { + return WriteNintendoHdRumble(left, right); + } + + return false; } - private int SendHDRumble(byte* data, nuint length) + private int SendHdRumble(byte* data, nuint length) { int result = 0; ulong currentTicks = SDL_GetTicks(); // Ditch rumble if we haven't hit the poll-rate yet. - // TODO: figure out a better way to do this - // While the polling check makes the rumble accurate, it also causes it to miss signals. - if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX + if ((currentTicks - _lastWriteTicks) <= GetPollRate()) { return result; } - SDL_LockJoysticks(); + result = SDL_hid_write(_hidHandle, data, length); + if (result >= 0) { - // Fun fact: Mario Kart 8 Deluxe sends rumble packets - // where the amplitude is zero, but the frequency isn't. - result = SDL_hid_write(_hidHandle, data, length); - if (result >= 0) - { - _lastWriteTicks = currentTicks; - } + _lastWriteTicks = currentTicks; } - SDL_UnlockJoysticks(); return result; } + private void InitializeDevice() + { + if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo) + { + _buffer = new byte[10]; + byte[] init = new byte[64]; + + // Pro Controller and Charge Grip + if (_product + is (ushort)HDRumbleSupportedProduct.ProController + or (ushort)HDRumbleSupportedProduct.JoyconGrip) + { + SDL_LockJoysticks(); + fixed (byte* ptr = init) + { + init[0] = 0x80; + init[1] = 0x05; // Allow bluetooth timeout TODO: use 0x04 to force USB only (toggle?) + SDL_hid_write(_hidHandle, ptr, 64); + } + SDL_UnlockJoysticks(); + + return; + } + + // Joycons + if (_product + is (ushort)HDRumbleSupportedProduct.JoyconLeft + or (ushort)HDRumbleSupportedProduct.JoyconRight + or (ushort)HDRumbleSupportedProduct.JoyconPair) + { + + SDL_LockJoysticks(); + fixed (byte* ptr = init) + { + // we could write data to the controller here (see above) + } + SDL_UnlockJoysticks(); + + return; + } + } + } + + private ulong GetPollRate() + { + ulong pollRate = 0; + if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo) + { + // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX + pollRate = (ulong) 16.67; + if (_product is (ushort)HDRumbleSupportedProduct.ProController + && SDL_hid_get_device_info(_hidHandle)->bus_type == SDL_hid_bus_type.SDL_HID_API_BUS_USB) + { + pollRate = (ulong) 8.33; + } + } + + return pollRate; + } + public void Dispose() { + GC.SuppressFinalize(this); SDL_hid_close(_hidHandle); } } - public enum HDRumbleSupported : ushort + public enum HDRumbleSupportedVendor : ushort { - JoyConLeft = 0x2006, - JoyConRight = 0x2007, + Nintendo = 0x057e, + Valve = 0x28de, + Sony = 0x054c + } + + public enum HDRumbleSupportedProduct : ushort + { + // TODO: Currently, HD Rumble only supports the Pro Controller and JoyCons. + // We need to initialize and report to each device differently. + + // Nintendo Switch: 0x057e + JoyconLeft = 0x2006, + JoyconRight = 0x2007, JoyconPair = 0x2008, ProController = 0x2009, JoyconGrip = 0x200e, + + // Nintendo Switch 2: 0x057e Joycon2Right = 0x2066, Joycon2Left = 0x2067, Joycon2Pair = 0x2068, Switch2ProController = 0x2069, - GamecubeController = 0x2073 + GamecubeController = 0x2073, + + // Valve Steam Family: 0x28de + // https://github.com/libsdl-org/SDL/issues/9148 + SteamDeck = 0x11ff, + SteamDeckVirtualDevice = 0x1205, + SteamController = 0x1106, + + // PlayStation Dualsense: 0x054c + Dualsense = 0x0ce6 } } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index 75e04ae40..d33d802da 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -582,12 +582,12 @@ namespace Ryujinx.Input.HLE Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " + // Value=value/multiplier * multiplier (result) $"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " + - $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " + + $"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.AmplitudeHigh}), " + $"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " + - $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " + - $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " + + $"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({leftVibrationValue.FrequencyHigh}), " + + $"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.AmplitudeLow}), " + $"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " + - $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " + + $"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.FrequencyLow}), " + $"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})"); } } diff --git a/src/Ryujinx.Input/InputConfigDefaults.cs b/src/Ryujinx.Input/InputConfigDefaults.cs index 5608923f7..7d0332d60 100644 --- a/src/Ryujinx.Input/InputConfigDefaults.cs +++ b/src/Ryujinx.Input/InputConfigDefaults.cs @@ -140,7 +140,7 @@ namespace Ryujinx.Input StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, - UseHDRumble = true, + UseHDRumble = false }, }; } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 3fd132cff..0930b11cc 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -336,7 +336,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f, - UseHDRumble = true + UseHDRumble = false }; } }