From 1d9c13fa2bb16b84945ec3e1331767f0d4afd9b6 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 20 May 2026 08:44:55 -0400 Subject: [PATCH] added HD Rumble toggle to UI, simplified rumble loop --- assets/Locales/Root.json | 50 +++++++++++++ .../Hid/Controller/RumbleConfigController.cs | 5 ++ src/Ryujinx.Input.SDL3/NpadHdRumble.cs | 74 +++++-------------- src/Ryujinx.Input/HLE/NpadController.cs | 27 +++---- src/Ryujinx/Headless/HeadlessRyujinx.Init.cs | 1 + .../Configuration/ConfigurationFileFormat.cs | 2 +- .../ConfigurationState.Migration.cs | 1 + .../UI/Models/Input/GamepadInputConfig.cs | 3 + .../UI/ViewModels/Input/InputViewModel.cs | 1 + .../ViewModels/Input/RumbleInputViewModel.cs | 3 + .../UI/Views/Input/RumbleInputView.axaml | 9 +++ .../UI/Views/Input/RumbleInputView.axaml.cs | 2 + 12 files changed, 108 insertions(+), 70 deletions(-) diff --git a/assets/Locales/Root.json b/assets/Locales/Root.json index 757ddeec8..d9139904c 100644 --- a/assets/Locales/Root.json +++ b/assets/Locales/Root.json @@ -12250,6 +12250,56 @@ "zh_TW": "弱震動調節:" } }, + { + "ID": "ControllerSettingsRumbleUseHDRumble", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable HD Rumble", + "es_ES": "Activa vibración HD", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "HDRumbleTooltip", + "Translations": { + "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.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "sv_SE": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "DialogMessageSaveNotAvailableMessage", "Translations": { diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs index ee8ab457d..f190996c1 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/RumbleConfigController.cs @@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller /// Enable Rumble /// public bool EnableRumble { get; set; } + + /// + /// Enable HD Rumble support + /// = 0) { - if (!String.IsNullOrEmpty(SDL_GetError())) - { - Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); - SDL_ClearError(); - } - return false; + return true; } + + if (!String.IsNullOrEmpty(SDL_GetError())) + { + Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError()); + SDL_ClearError(); + } + return false; } - - return true; } private static int EncodeLowFreq(float lowFreq) @@ -155,67 +153,31 @@ namespace Ryujinx.Input.SDL3 EncodeHighAmp(right.AmplitudeHigh)); } - public 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. + // 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 { - result = 1; return result; } - bool match = true; - int totalFreq = 0; - int totalAmp = 0; - - byte* head = data; - for (int i = 0; i < (int) length; i++) - { - if (*data != _lastRumbleData[i]) - { - match = false; - } - - if (i < 2) - { - data++; - continue; - } - - // Mario Kart 8 Deluxe sends rumble packets where the amplitude is zero, but the frequency isn't. - // It's likely that the hardware accounts for this, but on the off-chance it doesn't, we did. - if (i == 2 || i == 4 || i == 6 || i == 8) // frequency - { - totalFreq += *data; - } - else if (i == 3 || i == 5 || i == 7 || i == 9) // amplitude - { - totalAmp += *data; - } - - data++; - } - data = head; - - if (!match || (totalFreq == 0 || totalAmp == 0)) + 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) { _lastWriteTicks = currentTicks; - for (int i = 0; i < (int)length; i++) - { - _lastRumbleData[i] = *data; - data++; - } } } - + SDL_UnlockJoysticks(); + return result; } diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs index fe225111b..85ca5ffcb 100644 --- a/src/Ryujinx.Input/HLE/NpadController.cs +++ b/src/Ryujinx.Input/HLE/NpadController.cs @@ -562,28 +562,29 @@ namespace Ryujinx.Input.HLE VibrationValue leftVibrationValue = dualVibrationValue.Item1; VibrationValue rightVibrationValue = dualVibrationValue.Item2; - + leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble; rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble; - if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) + if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false) { - 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)); + float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15))); + float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85))); _gamepad?.Rumble(low, high, 0xFFFFFFFF); } - 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} " + - $"R.low.freq={rightVibrationValue.FrequencyLow}, " + - $"R.high.freq={rightVibrationValue.FrequencyHigh}"); + 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.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}), " + + $"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.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})"); } } } diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index af61b7b63..3574c3061 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -221,6 +221,7 @@ namespace Ryujinx.Headless StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index 0b451eacb..1fe98ee69 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration /// /// The current version of the file format /// - public const int CurrentVersion = 72; + public const int CurrentVersion = 73; /// /// Version of the configuration file format diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index 90a045a67..728321985 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -333,6 +333,7 @@ namespace Ryujinx.Ava.Systems.Configuration EnableRumble = false, StrongRumble = 1f, WeakRumble = 1f, + UseHDRumble = true }; } } diff --git a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs index 0eeef45f5..2a076f525 100644 --- a/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs +++ b/src/Ryujinx/UI/Models/Input/GamepadInputConfig.cs @@ -20,6 +20,7 @@ namespace Ryujinx.Ava.UI.Models.Input public float WeakRumble { get; set; } public float StrongRumble { get; set; } + public bool UseHDRumble { get; set; } public string Id { get; set; } @@ -236,6 +237,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = controllerInput.Rumble.EnableRumble; WeakRumble = controllerInput.Rumble.WeakRumble; StrongRumble = controllerInput.Rumble.StrongRumble; + UseHDRumble = controllerInput.Rumble.UseHDRumble; } if (controllerInput.Led != null) @@ -307,6 +309,7 @@ namespace Ryujinx.Ava.UI.Models.Input EnableRumble = EnableRumble, WeakRumble = WeakRumble, StrongRumble = StrongRumble, + UseHDRumble = UseHDRumble, }, Led = new LedConfigController { diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index 51229af72..68559d6c1 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -789,6 +789,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input StrongRumble = 1f, WeakRumble = 1f, EnableRumble = false, + UseHDRumble = true }, }; } diff --git a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs index e2323f567..74e0cd289 100644 --- a/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/RumbleInputViewModel.cs @@ -9,5 +9,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input [ObservableProperty] public partial float WeakRumble { get; set; } + + [ObservableProperty] + public partial bool EnableHDRumble { get; set; } } } diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml index 98489aab0..49eb1a717 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml @@ -53,6 +53,15 @@ Margin="5,0" Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> + + + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 347d011d5..655bdf591 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -22,6 +22,7 @@ namespace Ryujinx.Ava.UI.Views.Input { StrongRumble = config.StrongRumble, WeakRumble = config.WeakRumble, + EnableHDRumble = config.UseHDRumble }; InitializeComponent(); @@ -45,6 +46,7 @@ namespace Ryujinx.Ava.UI.Views.Input GamepadInputConfig config = viewModel.Config; config.StrongRumble = content.ViewModel.StrongRumble; config.WeakRumble = content.ViewModel.WeakRumble; + config.UseHDRumble = content.ViewModel.EnableHDRumble; }; await contentDialog.ShowAsync();