From 70f79e689bc947313aab11c41e59928ce43be517 Mon Sep 17 00:00:00 2001
From: mpnico <mpnico@gmail.com>
Date: Thu, 5 Aug 2021 00:39:40 +0200
Subject: [PATCH] Implement vibrations (#2468)

* First working vibration implementation

* Fix Infinite Rumble in SDL2Mouse

* Stop ignoring one vibValues every 2

* Remove RumbleInfinity as suggested

* Reworked all the vibration handle / calculation

* Revert HidVibrationDevicePosition changes

* Add UI to enable and tune rumble

* Remove some stub logs

* Add PlayerIndex in rumble debug log

* Fix all requested changes

* Implements hid::GetVibrationDeviceInfo

* Better implements HidVibrationValue.Equals/GetHashCode

* Added requested changes from code review

* Last fixes from review

* Update configuration file version for rebase
---
 .../GenericControllerInputConfig.cs           |   5 +
 .../Hid/Controller/RumbleConfigController.cs  |  20 +++
 .../Services/Hid/HidDevices/NpadDevices.cs    |  56 +++++++
 .../Vibration/HidVibrationDeviceHandle.cs     |  10 ++
 .../Types/Vibration/HidVibrationDeviceType.cs |   3 +-
 .../Types/Vibration/HidVibrationValue.cs      |  17 +-
 Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs    | 145 +++++++++++++-----
 Ryujinx.HLE/HOS/Services/Hid/ResultCode.cs    |   5 +-
 .../Services/Hid/Types/Npad/NpadStyleIndex.cs |  13 ++
 Ryujinx.Headless.SDL2/Program.cs              |   6 +
 Ryujinx.Input.SDL2/SDL2Gamepad.cs             |  14 +-
 Ryujinx.Input/HLE/NpadController.cs           |  27 ++++
 Ryujinx.Input/HLE/NpadManager.cs              |   9 +-
 Ryujinx.Input/IGamepad.cs                     |   2 +-
 .../Configuration/ConfigurationFileFormat.cs  |   2 +-
 Ryujinx/Configuration/ConfigurationState.cs   |  21 +++
 Ryujinx/Ui/Windows/ControllerWindow.cs        |  25 ++-
 Ryujinx/Ui/Windows/ControllerWindow.glade     | 138 +++++++++++++++++
 18 files changed, 468 insertions(+), 50 deletions(-)
 create mode 100644 Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceHandle.cs
 create mode 100644 Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs

diff --git a/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs b/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs
index e3423bb5ec..6c4562cfb2 100644
--- a/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs
+++ b/Ryujinx.Common/Configuration/Hid/Controller/GenericControllerInputConfig.cs
@@ -33,5 +33,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
         /// Controller Motion Settings
         /// </summary>
         public MotionConfigController Motion { get; set; }
+
+        /// <summary>
+        /// Controller Rumble Settings
+        /// </summary>
+        public RumbleConfigController Rumble { get; set; }
     }
 }
diff --git a/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs
new file mode 100644
index 0000000000..48be4f13e2
--- /dev/null
+++ b/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs
@@ -0,0 +1,20 @@
+namespace Ryujinx.Common.Configuration.Hid.Controller
+{
+    public class RumbleConfigController
+    {
+        /// <summary>
+        /// Controller Strong Rumble Multiplier
+        /// </summary>
+        public float StrongRumble { get; set; }
+
+        /// <summary>
+        /// Controller Weak Rumble Multiplier
+        /// </summary>
+        public float WeakRumble { get; set; }
+
+        /// <summary>
+        /// Enable Rumble
+        /// </summary>
+        public bool EnableRumble { get; set; }
+    }
+}
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
index bbc30172c0..55f8070a77 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
@@ -1,7 +1,9 @@
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 using Ryujinx.Common;
+using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.HOS.Kernel.Threading;
 using Ryujinx.HLE.HOS.Services.Hid.Types;
@@ -20,11 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
         private ControllerType[] _configuredTypes;
         private KEvent[] _styleSetUpdateEvents;
         private bool[] _supportedPlayers;
+        private static HidVibrationValue _neutralVibrationValue = new HidVibrationValue
+        {
+            AmplitudeLow = 0f,
+            FrequencyLow = 160f,
+            AmplitudeHigh = 0f,
+            FrequencyHigh = 320f
+        };
 
         internal NpadJoyHoldType JoyHold { get; set; }
         internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
         internal ControllerType SupportedStyleSets { get; set; }
 
+        public Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>> RumbleQueues = new Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>>();
+        public Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)> LastVibrationValues = new Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)>();
+
         public NpadDevices(Switch device, bool active = true) : base(device, active)
         {
             _configuredTypes = new ControllerType[MaxControllers];
@@ -596,5 +608,49 @@ namespace Ryujinx.HLE.HOS.Services.Hid
             WriteNewSixInputEntry(ref currentNpad.JoyLeftSixAxisSensor, ref newState);
             WriteNewSixInputEntry(ref currentNpad.JoyRightSixAxisSensor, ref newState);
         }
+
+        public void UpdateRumbleQueue(PlayerIndex index, Dictionary<byte, HidVibrationValue> dualVibrationValues)
+        {
+            if (RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> currentQueue))
+            {
+                if (!dualVibrationValues.TryGetValue(0, out HidVibrationValue leftVibrationValue))
+                {
+                    leftVibrationValue = _neutralVibrationValue;
+                }
+
+                if (!dualVibrationValues.TryGetValue(1, out HidVibrationValue rightVibrationValue))
+                {
+                    rightVibrationValue = _neutralVibrationValue;
+                }
+
+                if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue) || !leftVibrationValue.Equals(dualVibrationValue.Item1) || !rightVibrationValue.Equals(dualVibrationValue.Item2))
+                {
+                    currentQueue.Enqueue((leftVibrationValue, rightVibrationValue));
+
+                    LastVibrationValues[index] = (leftVibrationValue, rightVibrationValue);
+                }
+            }
+        }
+
+        public HidVibrationValue GetLastVibrationValue(PlayerIndex index, byte position)
+        {
+            if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
+            {
+                return _neutralVibrationValue;
+            }
+
+            return (position == 0) ? dualVibrationValue.Item1 : dualVibrationValue.Item2;
+        }
+
+        public ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> GetRumbleQueue(PlayerIndex index)
+        {
+            if (!RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> rumbleQueue))
+            {
+                rumbleQueue = new ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>();
+                _device.Hid.Npads.RumbleQueues[index] = rumbleQueue;
+            }
+
+            return rumbleQueue;
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceHandle.cs b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceHandle.cs
new file mode 100644
index 0000000000..4501c721a4
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceHandle.cs
@@ -0,0 +1,10 @@
+namespace Ryujinx.HLE.HOS.Services.Hid
+{
+    public struct HidVibrationDeviceHandle
+    {
+        public byte DeviceType;
+        public byte PlayerId;
+        public byte Position;
+        public byte Reserved;
+    }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceType.cs b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceType.cs
index cf9e64985c..898384be73 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceType.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationDeviceType.cs
@@ -3,6 +3,7 @@
     public enum HidVibrationDeviceType
     {
         None,
-        LinearResonantActuator
+        LinearResonantActuator,
+        GcErm
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationValue.cs b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationValue.cs
index 7211396e42..3f45d26991 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationValue.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidServer/Types/Vibration/HidVibrationValue.cs
@@ -1,4 +1,7 @@
-namespace Ryujinx.HLE.HOS.Services.Hid
+using Ryujinx.HLE.HOS.Tamper;
+using System;
+
+namespace Ryujinx.HLE.HOS.Services.Hid
 {
     public struct HidVibrationValue
     {
@@ -6,5 +9,17 @@
         public float FrequencyLow;
         public float AmplitudeHigh;
         public float FrequencyHigh;
+
+        public override bool Equals(object obj)
+        {
+            return obj is HidVibrationValue value &&
+                   AmplitudeLow == value.AmplitudeLow &&
+                   AmplitudeHigh == value.AmplitudeHigh;
+        }
+
+        public override int GetHashCode()
+        {
+            return HashCode.Combine(AmplitudeLow, AmplitudeHigh);
+        }
     }
 }
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
index 3f2aae356f..140545ad92 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
@@ -1,10 +1,13 @@
+using Ryujinx.Common;
 using Ryujinx.Common.Logging;
 using Ryujinx.HLE.HOS.Ipc;
 using Ryujinx.HLE.HOS.Kernel.Common;
 using Ryujinx.HLE.HOS.Kernel.Threading;
 using Ryujinx.HLE.HOS.Services.Hid.HidServer;
 using Ryujinx.HLE.HOS.Services.Hid.Types;
+using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
 
@@ -37,7 +40,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
 
         private HidSensorFusionParameters  _sensorFusionParams;
         private HidAccelerometerParameters _accelerometerParams;
-        private HidVibrationValue          _vibrationValue;
 
         public IHidServer(ServiceCtx context) : base(context.Device.System.HidServer)
         {
@@ -52,7 +54,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
 
             _sensorFusionParams  = new HidSensorFusionParameters();
             _accelerometerParams = new HidAccelerometerParameters();
-            _vibrationValue      = new HidVibrationValue();
 
             // TODO: signal event at right place
             _xpadIdEvent.ReadableEvent.Signal();
@@ -1025,29 +1026,78 @@ namespace Ryujinx.HLE.HOS.Services.Hid
         // GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo
         public ResultCode GetVibrationDeviceInfo(ServiceCtx context)
         {
-            int vibrationDeviceHandle = context.RequestData.ReadInt32();
+            HidVibrationDeviceHandle deviceHandle = context.RequestData.ReadStruct<HidVibrationDeviceHandle>();
+            NpadStyleIndex deviceType = (NpadStyleIndex)deviceHandle.DeviceType;
+            NpadIdType npadIdType = (NpadIdType)deviceHandle.PlayerId;
 
-            HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue
+            if (deviceType < NpadStyleIndex.System || deviceType >= NpadStyleIndex.FullKey)
             {
-                DeviceType = HidVibrationDeviceType.None,
-                Position   = HidVibrationDevicePosition.None
-            };
+                if (npadIdType >= (NpadIdType.Player8 + 1) && npadIdType != NpadIdType.Handheld && npadIdType != NpadIdType.Unknown)
+                {
+                    return ResultCode.InvalidNpadIdType;
+                }
 
-            context.ResponseData.Write((int)deviceInfo.DeviceType);
-            context.ResponseData.Write((int)deviceInfo.Position);
+                if (deviceHandle.Position > 1)
+                {
+                    return ResultCode.InvalidDeviceIndex;
+                }
 
-            Logger.Stub?.PrintStub(LogClass.ServiceHid, new { vibrationDeviceHandle, deviceInfo.DeviceType, deviceInfo.Position });
+                HidVibrationDeviceType vibrationDeviceType = HidVibrationDeviceType.None;
 
-            return ResultCode.Success;
+                if (Enum.IsDefined(typeof(NpadStyleIndex), deviceType))
+                {
+                    vibrationDeviceType = HidVibrationDeviceType.LinearResonantActuator;
+                }
+                else if ((uint)deviceType == 8)
+                {
+                    vibrationDeviceType = HidVibrationDeviceType.GcErm;
+                }
+
+                HidVibrationDevicePosition vibrationDevicePosition = HidVibrationDevicePosition.None;
+
+                if (vibrationDeviceType == HidVibrationDeviceType.LinearResonantActuator)
+                {
+                    if (deviceHandle.Position == 0)
+                    {
+                        vibrationDevicePosition = HidVibrationDevicePosition.Left;
+                    }
+                    else if (deviceHandle.Position == 1)
+                    {
+                        vibrationDevicePosition = HidVibrationDevicePosition.Right;
+                    }
+                    else
+                    {
+                        throw new ArgumentOutOfRangeException(nameof(deviceHandle.Position));
+                    }
+                }
+
+                HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue
+                {
+                    DeviceType = vibrationDeviceType,
+                    Position = vibrationDevicePosition
+                };
+
+                context.ResponseData.WriteStruct(deviceInfo);
+
+                return ResultCode.Success;
+            }
+
+            return ResultCode.InvalidNpadDeviceType;
         }
 
         [CommandHipc(201)]
         // SendVibrationValue(nn::hid::VibrationDeviceHandle, nn::hid::VibrationValue, nn::applet::AppletResourceUserId)
         public ResultCode SendVibrationValue(ServiceCtx context)
         {
-            int vibrationDeviceHandle = context.RequestData.ReadInt32();
+            HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
+            {
+                DeviceType = context.RequestData.ReadByte(),
+                PlayerId = context.RequestData.ReadByte(),
+                Position = context.RequestData.ReadByte(),
+                Reserved = context.RequestData.ReadByte()
+            };
 
-            _vibrationValue = new HidVibrationValue
+            HidVibrationValue vibrationValue = new HidVibrationValue
             {
                 AmplitudeLow  = context.RequestData.ReadSingle(),
                 FrequencyLow  = context.RequestData.ReadSingle(),
@@ -1057,14 +1107,11 @@ namespace Ryujinx.HLE.HOS.Services.Hid
 
             long appletResourceUserId = context.RequestData.ReadInt64();
 
-            Logger.Debug?.PrintStub(LogClass.ServiceHid, new {
-                appletResourceUserId,
-                vibrationDeviceHandle,
-                _vibrationValue.AmplitudeLow,
-                _vibrationValue.FrequencyLow,
-                _vibrationValue.AmplitudeHigh,
-                _vibrationValue.FrequencyHigh
-            });
+            Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
+
+            dualVibrationValues[deviceHandle.Position] = vibrationValue;
+
+            context.Device.Hid.Npads.UpdateRumbleQueue((PlayerIndex)deviceHandle.PlayerId, dualVibrationValues);
 
             return ResultCode.Success;
         }
@@ -1073,22 +1120,22 @@ namespace Ryujinx.HLE.HOS.Services.Hid
         // GetActualVibrationValue(nn::hid::VibrationDeviceHandle, nn::applet::AppletResourceUserId) -> nn::hid::VibrationValue
         public ResultCode GetActualVibrationValue(ServiceCtx context)
         {
-            int vibrationDeviceHandle = context.RequestData.ReadInt32();
+            HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
+            {
+                DeviceType = context.RequestData.ReadByte(),
+                PlayerId = context.RequestData.ReadByte(),
+                Position = context.RequestData.ReadByte(),
+                Reserved = context.RequestData.ReadByte()
+            };
+
             long appletResourceUserId = context.RequestData.ReadInt64();
 
-            context.ResponseData.Write(_vibrationValue.AmplitudeLow);
-            context.ResponseData.Write(_vibrationValue.FrequencyLow);
-            context.ResponseData.Write(_vibrationValue.AmplitudeHigh);
-            context.ResponseData.Write(_vibrationValue.FrequencyHigh);
+            HidVibrationValue vibrationValue = context.Device.Hid.Npads.GetLastVibrationValue((PlayerIndex)deviceHandle.PlayerId, deviceHandle.Position);
 
-            Logger.Stub?.PrintStub(LogClass.ServiceHid, new {
-                appletResourceUserId,
-                vibrationDeviceHandle,
-                _vibrationValue.AmplitudeLow,
-                _vibrationValue.FrequencyLow,
-                _vibrationValue.AmplitudeHigh,
-                _vibrationValue.FrequencyHigh
-            });
+            context.ResponseData.Write(vibrationValue.AmplitudeLow);
+            context.ResponseData.Write(vibrationValue.FrequencyLow);
+            context.ResponseData.Write(vibrationValue.AmplitudeHigh);
+            context.ResponseData.Write(vibrationValue.FrequencyHigh);
 
             return ResultCode.Success;
         }
@@ -1138,13 +1185,31 @@ namespace Ryujinx.HLE.HOS.Services.Hid
 
             context.Memory.Read(context.Request.PtrBuff[1].Position, vibrationValueBuffer);
 
-            // TODO: Read all handles and values from buffer.
+            Span<HidVibrationDeviceHandle> deviceHandles = MemoryMarshal.Cast<byte, HidVibrationDeviceHandle>(vibrationDeviceHandleBuffer);
+            Span<HidVibrationValue> vibrationValues = MemoryMarshal.Cast<byte, HidVibrationValue>(vibrationValueBuffer);
 
-            Logger.Debug?.PrintStub(LogClass.ServiceHid, new {
-                appletResourceUserId,
-                VibrationDeviceHandleBufferLength = vibrationDeviceHandleBuffer.Length,
-                VibrationValueBufferLength = vibrationValueBuffer.Length
-            });
+            if (!deviceHandles.IsEmpty && vibrationValues.Length == deviceHandles.Length)
+            {
+                Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
+                PlayerIndex currentIndex = (PlayerIndex)deviceHandles[0].PlayerId;
+
+                for (int deviceCounter = 0; deviceCounter < deviceHandles.Length; deviceCounter++)
+                {
+                    PlayerIndex index = (PlayerIndex)deviceHandles[deviceCounter].PlayerId;
+                    byte position = deviceHandles[deviceCounter].Position;
+
+                    if (index != currentIndex || dualVibrationValues.Count == 2)
+                    {
+                        context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
+                        dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
+                    }
+
+                    dualVibrationValues[position] = vibrationValues[deviceCounter];
+                    currentIndex = index;
+                }
+
+                context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
+            }
 
             return ResultCode.Success;
         }
diff --git a/Ryujinx.HLE/HOS/Services/Hid/ResultCode.cs b/Ryujinx.HLE/HOS/Services/Hid/ResultCode.cs
index 9b829cc503..9c87ac1dc9 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/ResultCode.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/ResultCode.cs
@@ -7,6 +7,9 @@
 
         Success = 0,
 
-        InvalidNpadIdType = (710 << ErrorCodeShift) | ModuleId
+        InvalidNpadDeviceType = (122 << ErrorCodeShift) | ModuleId,
+        InvalidNpadIdType     = (123 << ErrorCodeShift) | ModuleId,
+        InvalidDeviceIndex    = (124 << ErrorCodeShift) | ModuleId,
+        InvalidBufferSize     = (131 << ErrorCodeShift) | ModuleId
     }
 } 
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs
new file mode 100644
index 0000000000..b85681d271
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Hid/Types/Npad/NpadStyleIndex.cs
@@ -0,0 +1,13 @@
+namespace Ryujinx.HLE.HOS.Services.Hid
+{
+    public enum NpadStyleIndex : byte
+    {
+        FullKey = 3,
+        Handheld = 4,
+        JoyDual = 5,
+        JoyLeft = 6,
+        JoyRight = 7,
+        SystemExt = 32,
+        System = 33
+    }
+}
diff --git a/Ryujinx.Headless.SDL2/Program.cs b/Ryujinx.Headless.SDL2/Program.cs
index 2884f38a3e..c3a10929db 100644
--- a/Ryujinx.Headless.SDL2/Program.cs
+++ b/Ryujinx.Headless.SDL2/Program.cs
@@ -236,6 +236,12 @@ namespace Ryujinx.Headless.SDL2
                             EnableMotion = true,
                             Sensitivity  = 100,
                             GyroDeadzone = 1,
+                        },
+                        Rumble = new RumbleConfigController
+                        {
+                            StrongRumble = 1f,
+                            WeakRumble = 1f,
+                            EnableRumble = false
                         }
                     };
                 }
diff --git a/Ryujinx.Input.SDL2/SDL2Gamepad.cs b/Ryujinx.Input.SDL2/SDL2Gamepad.cs
index 26a808e493..0ccd8bb34e 100644
--- a/Ryujinx.Input.SDL2/SDL2Gamepad.cs
+++ b/Ryujinx.Input.SDL2/SDL2Gamepad.cs
@@ -1,5 +1,6 @@
 using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Logging;
 using System;
 using System.Collections.Generic;
 using System.Numerics;
@@ -151,7 +152,18 @@ namespace Ryujinx.Input.SDL2
                 ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
                 ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
 
-                SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
+                if (durationMs == uint.MaxValue)
+                {
+                    SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY);
+                }
+                else if (durationMs > SDL_HAPTIC_INFINITY)
+                {
+                    Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
+                }
+                else
+                {
+                    SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
+                }
             }
         }
 
diff --git a/Ryujinx.Input/HLE/NpadController.cs b/Ryujinx.Input/HLE/NpadController.cs
index 3559b015c8..79c18ecf7f 100644
--- a/Ryujinx.Input/HLE/NpadController.cs
+++ b/Ryujinx.Input/HLE/NpadController.cs
@@ -2,8 +2,11 @@
 using Ryujinx.Common.Configuration.Hid;
 using Ryujinx.Common.Configuration.Hid.Controller;
 using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Logging;
 using Ryujinx.HLE.HOS.Services.Hid;
 using System;
+using System.Collections.Generic;
+using System.Collections.Concurrent;
 using System.Numerics;
 using System.Runtime.CompilerServices;
 
@@ -534,5 +537,29 @@ namespace Ryujinx.Input.HLE
         {
             Dispose(true);
         }
+
+        public void UpdateRumble(ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> queue)
+        {
+            if (queue.TryDequeue(out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
+            {
+                if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
+                {
+                    HidVibrationValue leftVibrationValue = dualVibrationValue.Item1;
+                    HidVibrationValue 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));
+
+                    _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}, " +
+                        $"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
+                        $"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
+                        $"--> ({low}, {high})");
+                }
+            }
+        }
     }
 }
diff --git a/Ryujinx.Input/HLE/NpadManager.cs b/Ryujinx.Input/HLE/NpadManager.cs
index 03bde64b7e..a0d2e513f0 100644
--- a/Ryujinx.Input/HLE/NpadManager.cs
+++ b/Ryujinx.Input/HLE/NpadManager.cs
@@ -4,6 +4,7 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion;
 using Ryujinx.Common.Configuration.Hid.Keyboard;
 using Ryujinx.HLE.HOS.Services.Hid;
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
@@ -167,6 +168,7 @@ namespace Ryujinx.Input.HLE
                     (SixAxisInput, SixAxisInput) motionState = default;
 
                     NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
+                    Ryujinx.HLE.HOS.Services.Hid.PlayerIndex playerIndex = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
 
                     bool isJoyconPair = false;
 
@@ -177,6 +179,7 @@ namespace Ryujinx.Input.HLE
 
                         controller.UpdateUserConfiguration(inputConfig);
                         controller.Update();
+                        controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
 
                         inputState = controller.GetHLEInputState();
 
@@ -199,15 +202,15 @@ namespace Ryujinx.Input.HLE
                         motionState.Item1.Orientation = new float[9];
                     }
 
-                    inputState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
-                    motionState.Item1.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
+                    inputState.PlayerId = playerIndex;
+                    motionState.Item1.PlayerId = playerIndex;
 
                     hleInputStates.Add(inputState);
                     hleMotionStates.Add(motionState.Item1);
 
                     if (isJoyconPair && !motionState.Item2.Equals(default))
                     {
-                        motionState.Item2.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
+                        motionState.Item2.PlayerId = playerIndex;
 
                         hleMotionStates.Add(motionState.Item2);
                     }
diff --git a/Ryujinx.Input/IGamepad.cs b/Ryujinx.Input/IGamepad.cs
index cc788333b0..c83ad5f822 100644
--- a/Ryujinx.Input/IGamepad.cs
+++ b/Ryujinx.Input/IGamepad.cs
@@ -66,7 +66,7 @@ namespace Ryujinx.Input
         void SetConfiguration(InputConfig configuration);
 
         /// <summary>
-        /// Starts a rumble effect on the gampead.
+        /// Starts a rumble effect on the gamepad.
         /// </summary>
         /// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
         /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
diff --git a/Ryujinx/Configuration/ConfigurationFileFormat.cs b/Ryujinx/Configuration/ConfigurationFileFormat.cs
index 65165d8db3..ae43d587db 100644
--- a/Ryujinx/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx/Configuration/ConfigurationFileFormat.cs
@@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        public const int CurrentVersion = 29;
+        public const int CurrentVersion = 30;
 
         public int Version { get; set; }
 
diff --git a/Ryujinx/Configuration/ConfigurationState.cs b/Ryujinx/Configuration/ConfigurationState.cs
index 41bd64a7b0..fe4ff77466 100644
--- a/Ryujinx/Configuration/ConfigurationState.cs
+++ b/Ryujinx/Configuration/ConfigurationState.cs
@@ -1,6 +1,7 @@
 using Ryujinx.Common;
 using Ryujinx.Common.Configuration;
 using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
 using Ryujinx.Common.Configuration.Hid.Keyboard;
 using Ryujinx.Common.Logging;
 using Ryujinx.Configuration.System;
@@ -874,6 +875,26 @@ namespace Ryujinx.Configuration
                 configurationFileUpdated = true;
             }
 
+            if (configurationFileFormat.Version < 30)
+            {
+                Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30.");
+
+                foreach(InputConfig config in configurationFileFormat.InputConfig)
+                {
+                    if (config is StandardControllerInputConfig controllerConfig)
+                    {
+                        controllerConfig.Rumble = new RumbleConfigController
+                        {
+                            EnableRumble = false,
+                            StrongRumble = 1f,
+                            WeakRumble = 1f
+                        };
+                    }
+                }
+
+                configurationFileUpdated = true;
+            }
+
             Logger.EnableFileLog.Value             = configurationFileFormat.EnableFileLog;
             Graphics.ResScale.Value                = configurationFileFormat.ResScale;
             Graphics.ResScaleCustom.Value          = configurationFileFormat.ResScaleCustom;
diff --git a/Ryujinx/Ui/Windows/ControllerWindow.cs b/Ryujinx/Ui/Windows/ControllerWindow.cs
index c57a62c70b..36c2a7aafb 100644
--- a/Ryujinx/Ui/Windows/ControllerWindow.cs
+++ b/Ryujinx/Ui/Windows/ControllerWindow.cs
@@ -34,6 +34,8 @@ namespace Ryujinx.Ui.Windows
         private bool _isWaitingForInput;
 
 #pragma warning disable CS0649, IDE0044
+        [GUI] Adjustment   _controllerStrongRumble;
+        [GUI] Adjustment   _controllerWeakRumble;
         [GUI] Adjustment   _controllerDeadzoneLeft;
         [GUI] Adjustment   _controllerDeadzoneRight;
         [GUI] Adjustment   _controllerTriggerThreshold;
@@ -99,6 +101,8 @@ namespace Ryujinx.Ui.Windows
         [GUI] ToggleButton _rSl;
         [GUI] ToggleButton _rSr;
         [GUI] Image        _controllerImage;
+        [GUI] CheckButton  _enableRumble;
+        [GUI] Box          _rumbleBox;
 #pragma warning restore CS0649, IDE0044
 
         private MainWindow _mainWindow;
@@ -314,6 +318,7 @@ namespace Ryujinx.Ui.Windows
                 _deadZoneRightBox.Hide();
                 _triggerThresholdBox.Hide();
                 _motionBox.Hide();
+                _rumbleBox.Hide();
             }
             else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller"))
             {
@@ -407,6 +412,8 @@ namespace Ryujinx.Ui.Windows
             _zR.Label                         = "Unbound";
             _rSl.Label                        = "Unbound";
             _rSr.Label                        = "Unbound";
+            _controllerStrongRumble.Value     = 1;
+            _controllerWeakRumble.Value       = 1;
             _controllerDeadzoneLeft.Value     = 0;
             _controllerDeadzoneRight.Value    = 0;
             _controllerTriggerThreshold.Value = 0;
@@ -419,6 +426,7 @@ namespace Ryujinx.Ui.Windows
             _gyroDeadzone.Value               = 1;
             _dsuServerHost.Buffer.Text        = "";
             _dsuServerPort.Buffer.Text        = "";
+            _enableRumble.Active              = false;
         }
 
         private void SetValues(InputConfig config)
@@ -497,6 +505,9 @@ namespace Ryujinx.Ui.Windows
                     _zR.Label                         = controllerConfig.RightJoycon.ButtonZr.ToString();
                     _rSl.Label                        = controllerConfig.RightJoycon.ButtonSl.ToString();
                     _rSr.Label                        = controllerConfig.RightJoycon.ButtonSr.ToString();
+                    _controllerStrongRumble.Value     = controllerConfig.Rumble.StrongRumble;
+                    _controllerWeakRumble.Value       = controllerConfig.Rumble.WeakRumble;
+                    _enableRumble.Active              = controllerConfig.Rumble.EnableRumble;
                     _controllerDeadzoneLeft.Value     = controllerConfig.DeadzoneLeft;
                     _controllerDeadzoneRight.Value    = controllerConfig.DeadzoneRight;
                     _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
@@ -706,7 +717,13 @@ namespace Ryujinx.Ui.Windows
                         InvertStickY = _invertRStickY.Active,
                         StickButton  = rStickButton,
                     },
-                    Motion           = motionConfig
+                    Motion           = motionConfig,
+                    Rumble           = new RumbleConfigController
+                    {
+                        StrongRumble = (float)_controllerStrongRumble.Value,
+                        WeakRumble   = (float)_controllerWeakRumble.Value,
+                        EnableRumble = _enableRumble.Active
+                    }
                 };
             }
 
@@ -1045,6 +1062,12 @@ namespace Ryujinx.Ui.Windows
                             EnableMotion = true,
                             Sensitivity  = 100,
                             GyroDeadzone = 1,
+                        },
+                        Rumble = new RumbleConfigController
+                        {
+                            StrongRumble = 1f,
+                            WeakRumble   = 1f,
+                            EnableRumble = false
                         }
                     };
                 }
diff --git a/Ryujinx/Ui/Windows/ControllerWindow.glade b/Ryujinx/Ui/Windows/ControllerWindow.glade
index 8e897795a6..a396df03cb 100644
--- a/Ryujinx/Ui/Windows/ControllerWindow.glade
+++ b/Ryujinx/Ui/Windows/ControllerWindow.glade
@@ -7,6 +7,20 @@
     <property name="step_increment">1</property>
     <property name="page_increment">4</property>
   </object>
+  <object class="GtkAdjustment" id="_controllerStrongRumble">
+    <property name="lower">0.1</property>
+    <property name="upper">10</property>
+    <property name="value">1.0</property>
+    <property name="step_increment">0.1</property>
+    <property name="page_increment">1.0</property>
+  </object>
+  <object class="GtkAdjustment" id="_controllerWeakRumble">
+    <property name="lower">0.1</property>
+    <property name="upper">10</property>
+    <property name="value">1.0</property>
+    <property name="step_increment">0.1</property>
+    <property name="page_increment">1.0</property>
+  </object>
   <object class="GtkAdjustment" id="_controllerDeadzoneLeft">
     <property name="upper">1</property>
     <property name="value">0.050000000000000003</property>
@@ -1249,6 +1263,130 @@
                                         <property name="position">1</property>
                                       </packing>
                                     </child>
+                                    <child>
+                                      <object class="GtkBox" id="_rumbleBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="margin_top">10</property>
+                                        <property name="orientation">vertical</property>
+                                        <child>
+                                          <object class="GtkLabel">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="margin_top">10</property>
+                                            <property name="margin_bottom">5</property>
+                                            <property name="label" translatable="yes">Rumble</property>
+                                            <attributes>
+                                              <attribute name="weight" value="bold"/>
+                                            </attributes>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkCheckButton" id="_enableRumble">
+                                            <property name="label" translatable="yes">Enable</property>
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                            <property name="receives_default">False</property>
+                                            <property name="draw_indicator">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkBox" id="_StrongMultiBox">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="margin_top">10</property>
+                                            <property name="orientation">vertical</property>
+                                            <child>
+                                              <object class="GtkLabel">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">False</property>
+                                                <property name="halign">start</property>
+                                                <property name="label" translatable="yes">Strong rumble multiplier</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">False</property>
+                                                <property name="fill">True</property>
+                                                <property name="position">0</property>
+                                              </packing>
+                                            </child>
+                                            <child>
+                                              <object class="GtkScale">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">True</property>
+                                                <property name="adjustment">_controllerStrongRumble</property>
+                                                <property name="round_digits">1</property>
+                                                <property name="digits">1</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">True</property>
+                                                <property name="fill">True</property>
+                                                <property name="position">1</property>
+                                              </packing>
+                                            </child>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">2</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkBox" id="_WeakMultiBox">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="margin_top">10</property>
+                                            <property name="orientation">vertical</property>
+                                            <child>
+                                              <object class="GtkLabel">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">False</property>
+                                                <property name="halign">start</property>
+                                                <property name="label" translatable="yes">Weak rumble multiplier</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">False</property>
+                                                <property name="fill">True</property>
+                                                <property name="position">0</property>
+                                              </packing>
+                                            </child>
+                                            <child>
+                                              <object class="GtkScale">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">True</property>
+                                                <property name="adjustment">_controllerWeakRumble</property>
+                                                <property name="round_digits">1</property>
+                                                <property name="digits">1</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">True</property>
+                                                <property name="fill">True</property>
+                                                <property name="position">1</property>
+                                              </packing>
+                                            </child>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">3</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">2</property>
+                                      </packing>
+                                    </child>
                                   </object>
                                   <packing>
                                     <property name="expand">False</property>