diff --git a/src/Ryujinx.Input.SDL3/NpadHdRumble.cs b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
new file mode 100644
index 000000000..e367f6a9c
--- /dev/null
+++ b/src/Ryujinx.Input.SDL3/NpadHdRumble.cs
@@ -0,0 +1,151 @@
+using Ryujinx.HLE.HOS.Services.Hid;
+using SDL;
+using static SDL.SDL3;
+using System;
+
+namespace Ryujinx.Input.SDL3
+{
+ ///
+ /// Manages a HID handle of a gamepad to encode and write HD rumble commands for Nin controllers.
+ ///
+ public unsafe class NpadHdRumble : IDisposable
+ {
+ private readonly SDL_hid_device* _hidHandle;
+
+ private int _globalCount;
+
+ private NpadHdRumble(SDL_hid_device* hidHandle)
+ {
+ _hidHandle = hidHandle;
+ }
+
+ public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
+ {
+ ushort vendor = SDL_GetGamepadVendor(gamepadHandle);
+ if (vendor != 0x057e)
+ {
+ return null;
+ }
+
+ ushort product = SDL_GetGamepadProduct(gamepadHandle);
+ if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e)
+ {
+ return null;
+ }
+
+ return new NpadHdRumble(SDL_hid_open(vendor, product, 0));
+ }
+
+ // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
+ private void WriteHdRumble(
+ int encLeftLowFreq, int encLeftLowAmp,
+ int encLeftHighFreq, int encLeftHighAmp,
+ int encRightLowFreq, int encRightLowAmp,
+ int encRightHighFreq, int encRightHighAmp)
+ {
+ 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);
+
+ if (_globalCount > 0xF)
+ {
+ _globalCount = 0x0;
+ }
+
+ fixed (byte* ptr = buf)
+ {
+ SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length);
+ }
+ }
+
+
+ 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;
+ }
+
+ 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;
+ }
+
+ private static int EncodeLowAmp(float rawAmp)
+ {
+ int encodedAmp = 0;
+
+ if (rawAmp is > 0 and < 0.012f)
+ {
+ encodedAmp = 1;
+ }
+ else if (rawAmp is >= 0.012f and < 0.112f)
+ {
+ encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
+ }
+ else if (rawAmp is >= 0.112f and < 0.225f)
+ {
+ encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
+ }
+ else if (rawAmp is >= 0.225f and <= 1f)
+ {
+ encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
+ }
+
+ return (int)Math.Floor(encodedAmp / 2.0) + 64;
+ }
+
+ private static int EncodeHighAmp(float rawAmp)
+ {
+ int encodedAmp = 0;
+
+ if (rawAmp is > 0 and < 0.012f)
+ {
+ encodedAmp = 1;
+ }
+ else if (rawAmp is >= 0.012f and < 0.112f)
+ {
+ encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
+ }
+ else if (rawAmp is >= 0.112f and < 0.225f)
+ {
+ encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
+ }
+ else if (rawAmp is >= 0.225f and <= 1f)
+ {
+ encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
+ }
+
+ return encodedAmp * 2;
+ }
+
+ public bool HdRumble(VibrationValue left, VibrationValue right)
+ {
+ 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));
+ return true;
+ }
+
+ public void Dispose()
+ {
+ SDL_hid_close(_hidHandle);
+ }
+ }
+}
diff --git a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
index 4985d8eea..c23b64304 100644
--- a/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
+++ b/src/Ryujinx.Input.SDL3/SDL3Gamepad.cs
@@ -2,6 +2,7 @@ using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -76,11 +77,14 @@ namespace Ryujinx.Input.SDL3
private SDL_Gamepad* _gamepadHandle;
+ private NpadHdRumble _hdRumble;
+
private float _triggerThreshold;
public SDL3Gamepad(SDL_Gamepad* gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
+ _hdRumble = NpadHdRumble.Create(gamepadHandle);
_buttonsUserMapping = new List(20);
Name = SDL_GetGamepadName(_gamepadHandle);
@@ -165,6 +169,10 @@ namespace Ryujinx.Input.SDL3
protected virtual void Dispose(bool disposing)
{
+ if (disposing && _hdRumble != null)
+ {
+ _hdRumble.Dispose();
+ }
if (disposing && _gamepadHandle != null)
{
SDL_CloseGamepad(_gamepadHandle);
@@ -184,6 +192,11 @@ namespace Ryujinx.Input.SDL3
_triggerThreshold = triggerThreshold;
}
+ public bool HDRumble(VibrationValue left, VibrationValue right)
+ {
+ return _hdRumble?.HdRumble(left, right) ?? false;
+ }
+
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if ((Features & GamepadFeaturesFlag.Rumble) == 0)
diff --git a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
index 5311a256c..5d779518d 100644
--- a/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
+++ b/src/Ryujinx.Input.SDL3/SDL3JoyCon.cs
@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -61,6 +62,8 @@ namespace Ryujinx.Input.SDL3
public GamepadFeaturesFlag Features { get; }
private SDL_Gamepad* _gamepadHandle;
+
+ private NpadHdRumble _hdRumble;
private enum JoyConType
{
@@ -76,6 +79,7 @@ namespace Ryujinx.Input.SDL3
public SDL3JoyCon(SDL_Gamepad* gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
+ _hdRumble = NpadHdRumble.Create(gamepadHandle);
_buttonsUserMapping = new List(10);
Name = SDL_GetGamepadName(_gamepadHandle);
@@ -139,6 +143,10 @@ namespace Ryujinx.Input.SDL3
protected virtual void Dispose(bool disposing)
{
+ if (disposing && _hdRumble != null)
+ {
+ _hdRumble.Dispose();
+ }
if (disposing && _gamepadHandle != null)
{
SDL_CloseGamepad(_gamepadHandle);
@@ -156,6 +164,11 @@ namespace Ryujinx.Input.SDL3
{
}
+
+ public bool HDRumble(VibrationValue left, VibrationValue right)
+ {
+ return _hdRumble?.HdRumble(left, right) ?? false;
+ }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs
index 29bc973f6..a46ff8daf 100644
--- a/src/Ryujinx.Input/HLE/NpadController.cs
+++ b/src/Ryujinx.Input/HLE/NpadController.cs
@@ -559,18 +559,29 @@ namespace Ryujinx.Input.HLE
{
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
-
+
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));
+
+ leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
+ leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
+ rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
+ rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
- _gamepad?.Rumble(low, high, uint.MaxValue);
+ if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
+ {
+ _gamepad?.Rumble(low, high, uint.MaxValue);
+ }
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
+ $"L.low.freq={leftVibrationValue.FrequencyLow}, " +
+ $"L.high.freq={leftVibrationValue.FrequencyHigh}, " +
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
- $"--> ({low}, {high})");
+ $"R.low.freq={rightVibrationValue.FrequencyLow}, " +
+ $"R.high.freq={rightVibrationValue.FrequencyHigh}");
}
}
}
diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs
index 945ccfa8b..d45ac0444 100644
--- a/src/Ryujinx.Input/IGamepad.cs
+++ b/src/Ryujinx.Input/IGamepad.cs
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Memory;
+using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -74,6 +75,16 @@ namespace Ryujinx.Input
public void ClearLed() => SetLed(0);
+ ///
+ /// Starts an HD vibration effect on the gamepad if available.
+ ///
+ /// The vibration data for the left side
+ /// The vibration data for the right side
+ bool HDRumble(VibrationValue left, VibrationValue right)
+ {
+ return false;
+ }
+
///
/// Starts a rumble effect on the gamepad.
///