From f8a2e4ca8e848cf2ba2ac5da9a06d3239907c55d Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 22 May 2026 16:07:10 -0400 Subject: [PATCH] basically rewrote the class for clarity and structure --- src/Ryujinx.Input.SDL3/NpadHdRumble.cs | 271 +++++++++++++----------- src/Ryujinx.Input/HLE/NpadController.cs | 8 +- 2 files changed, 146 insertions(+), 133 deletions(-) diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs index 6684658e8..d8a03cd52 100644 --- a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs +++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs @@ -1,9 +1,8 @@ using Ryujinx.Common.Logging; using Ryujinx.HLE.HOS.Services.Hid; -using Ryujinx.Input.HLE; using SDL; using static SDL.SDL3; -using System; +using System; namespace Ryujinx.Input.SDL3 { @@ -13,7 +12,9 @@ namespace Ryujinx.Input.SDL3 public unsafe class NpadHdRumble : IDisposable { private readonly SDL_hid_device* _hidHandle; - private readonly ulong _pollRate; + + private byte[] _buffer; + private static ushort _vendor; private static ushort _product; private int _globalCount; @@ -23,79 +24,88 @@ namespace Ryujinx.Input.SDL3 { _hidHandle = hidHandle; InitializeDevice(); - _pollRate = GetPollRate(); } 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; } _product = SDL_GetGamepadProduct(gamepadHandle); - if (!Enum.IsDefined(typeof(HDRumbleSupported), _product)) + 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); + // Clamping should be done for LRA safety. + int leftLowAmp = EncodeLowAmp(left.AmplitudeLow) + 0x80; + int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8); + int leftHighFreq = EncodeHighFreq(left.FrequencyHigh); + int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8); + + int rightLowAmp = EncodeLowAmp(right.AmplitudeLow) + 0x80; + int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8); + int rightHighFreq = EncodeHighFreq(left.FrequencyHigh); + int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8); + + _buffer[0] = 0x10; + _buffer[1] = (byte)((++_globalCount) & 0xF); + _buffer[2] = (byte)(leftLowFreq & 0xFF); + _buffer[3] = (byte)(leftHighAmp & 0xFF); + _buffer[4] = (byte)(leftHighFreq & 0xFF); + _buffer[5] = (byte)(leftLowAmp & 0xFF); + + _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.Round((32 * Math.Log2(lf / 10)) - 0x40); } 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.Round(((32 * Math.Log2(hf / 10)) - 0x60) * 4); } private static int EncodeLowAmp(float rawAmp) @@ -108,18 +118,18 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = 4 * Math.Log2(rawAmp * 110f); + encodedAmp = Math.Round(4 * Math.Log2(rawAmp * 110f)); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = 16 * Math.Log2(rawAmp * 17f); + encodedAmp = Math.Round(16 * Math.Log2(rawAmp * 17f)); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); + encodedAmp = Math.Round(32 * Math.Log2(rawAmp * 8.7f)); } - return (int)Math.Floor(encodedAmp / 2.0) + 64; + return (int) (encodedAmp / 2) + 64; } private static int EncodeHighAmp(float rawAmp) @@ -132,30 +142,32 @@ namespace Ryujinx.Input.SDL3 } else if (rawAmp is >= 0.012f and < 0.112f) { - encodedAmp = 4 * Math.Log2(rawAmp * 110f); + encodedAmp = Math.Round(4 * Math.Log2(rawAmp * 110f)); } else if (rawAmp is >= 0.112f and < 0.225f) { - encodedAmp = 16 * Math.Log2(rawAmp * 17f); + encodedAmp = Math.Round(16 * Math.Log2(rawAmp * 17f)); } else if (rawAmp is >= 0.225f and <= 1f) { - encodedAmp = 32 * Math.Log2(rawAmp * 8.7f); + encodedAmp = Math.Round(32 * Math.Log2(rawAmp * 8.7f)); } - return (int) Math.Round(encodedAmp * 2); + return (int) encodedAmp * 2; } 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) @@ -164,123 +176,124 @@ namespace Ryujinx.Input.SDL3 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. // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX - if ((currentTicks - _lastWriteTicks) <= _pollRate) + if ((currentTicks - _lastWriteTicks) <= GetPollRate()) { return result; } - SDL_LockJoysticks(); + // 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) { - // 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() { - byte[] init = new byte[64]; - - // Pro Controller and Charge Grip - if (_product - is (ushort)HDRumbleSupported.ProController - or (ushort)HDRumbleSupported.JoyconGrip) + if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo) { - init[0] = 0x80; - init[1] = 0x02; - - fixed (byte* ptr = init) - { - SDL_hid_write(_hidHandle, ptr, 64); - } + _buffer = new byte[10]; + byte[] init = new byte[64]; - return; - } - - // Joycons - if (_product - is (ushort)HDRumbleSupported.JoyconLeft - or (ushort)HDRumbleSupported.JoyconRight - or (ushort)HDRumbleSupported.JoyconPair) - { - init[0] = 0x01; - init[1] = 0x01; - init[2] = 0x00; - init[3] = 0x01; - init[4] = 0x40; - init[5] = 0x40; - init[6] = 0x00; - init[7] = 0x01; - init[8] = 0x40; - init[9] = 0x40; - - // TODO: Resend with updated packet for player number LEDs. - // And probably move this to NpadDevices. - init[10] = 0x01; // 0x30 - init[11] = 0x01; // 0x01 0x03 0x07 0x0F - - fixed (byte* ptr = init) + // Pro Controller and Charge Grip + if (_product + is (ushort)HDRumbleSupportedProduct.ProController + or (ushort)HDRumbleSupportedProduct.JoyconGrip) { - SDL_hid_write(_hidHandle, ptr, 64); + 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; } - - 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() { - byte[] dataArray = new byte[10]; - int read; - - ulong startTime = SDL_GetTicks(); - fixed (byte* ptr = dataArray) + ulong pollRate = 0; + if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo) { - read = SDL_hid_read(_hidHandle, ptr, 10); + 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; + } } - ulong endTime = SDL_GetTicks(); - ulong readTime = endTime - startTime; - Logger.Debug?.PrintMsg(LogClass.Hid, $"POLL RATE: {readTime}ms."); - - if (read == 0) - { - return 0; - } - - return readTime; + Logger.Debug?.PrintMsg(LogClass.Hid, $"POLL RATE: {pollRate}ms."); + return pollRate; } public void Dispose() { + GC.SuppressFinalize(this); SDL_hid_close(_hidHandle); } } - public enum HDRumbleSupported : ushort + public enum HDRumbleSupportedVendor : ushort { - // Currently, HD Rumble only supports the Pro Controller and Joycons. - // We need to initialize each device differently. + 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. + // When this happens, we'll refactor this class to reflect it. + + // 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 85ca5ffcb..287853a4e 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -578,12 +578,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})"); } }