Author SHA1 Message Date
KeatonTheBot 9bf15801fd misc: Fix config file version 2026-09-08 16:13:26 -05:00
Max e3150d20bb [HID] Restructure HD Rumble class for future controller support
- 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.
2026-09-08 15:54:23 -05:00
Max 50538d601f [HID] Fixed HD Rumble latency 2026-09-08 15:54:23 -05:00
stossy11 5d3e392082 Fix fragment ClipDistance SPIR-V lookup
- Fixed ClipDistance being treated as VTG gl_PerVertex block, Causing the lookup of Input.Position when it's undeclared.
2026-09-08 15:54:23 -05:00
Neo c57e91cc59 Implement: TrySelectUserWithoutInteraction Command 52
Adds support for IAccountServiceForApplication command 52, TrySelectUserWithoutInteraction, introduced in Firmware 19.0.0.

Command 51 (TrySelectUserWithoutInteractionDeprecated) and command 52 use the existing ApplicationServiceServer.TrySelectUserWithoutInteraction implementation.

This prevents newer titles from failing when requesting automatic user selection through the new command.

Tomb Raider: Definitive Edition is now fully playable.
2026-09-08 15:54:22 -05:00
Neo 0381c7157b Fix: GetAllAvailableResources Filtering by Resource Path
Fix EmbeddedResources.GetAllAvailableResources to respect the specified resource path when enumerating embedded resources.

Previously, resources were filtered only by file extension. Adding locale-named JSON files under anywhere else caused them to be included when querying Assets/Locales, resulting in duplicate entries in the language menu.
2026-09-08 15:54:22 -05:00
avan 93b4c53c8a Fix crash on duplicate Build IDs in ModLoader.LoadCheats
Multiple executables may report the same Build ID, causing ToDictionary to throw an ArgumentException while creating the executable lookup.

Build the lookup incrementally and keep the first code address when duplicate Build IDs are encountered. Log a warning if a duplicate Build ID is associated with a different code address.
2026-09-08 15:54:22 -05:00
avan ef14467d1f Fix a hang during the loading stage
In KAddressArbiter, threads were originally inserted into the wait queue according to their DynamicPriority.

When multiple threads had the same DynamicPriority, the original implementation could not guarantee their existing order. Threads with the same dynamic priority are now inserted in FIFO order, preventing unstable wake-up ordering from blocking the guest synchronization flow.
2026-09-08 15:54:22 -05:00
KeatonTheBot e07f333a31 Raise application pool sizes for DRAM selections
- Fixes crash with BotW and possibly other games
2026-09-05 14:09:46 -05:00
26 changed files with 421 additions and 155 deletions
@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Enable Rumble /// Enable Rumble
/// </summary> /// </summary>
public bool EnableRumble { get; set; } public bool EnableRumble { get; set; }
/// <summary>
/// Enable HD Rumble support
/// </summary
public bool UseHDRumble { get; set; }
} }
} }
@@ -126,8 +126,13 @@ namespace Ryujinx.Common
public static string[] GetAllAvailableResources(string path, string ext = "") public static string[] GetAllAvailableResources(string path, string ext = "")
{ {
return ResolveManifestPath(path).Item1.GetManifestResourceNames() (Assembly assembly, string resourcePath) = ResolveManifestPath(path);
.Where(r => r.EndsWith(ext))
string manifestPath = assembly.GetName().Name + "." + resourcePath.Replace('/', '.');
return assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(manifestPath + ".", StringComparison.Ordinal))
.Where(r => r.EndsWith(ext, StringComparison.Ordinal))
.ToArray(); .ToArray();
} }
@@ -1790,7 +1790,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{ {
(_, varType) = IoMap.GetSpirvBuiltIn(ioVariable); (_, varType) = IoMap.GetSpirvBuiltIn(ioVariable);
if (IoMap.IsPerVertexBuiltIn(ioVariable)) if (context.Definitions.Stage.IsVtg() && IoMap.IsPerVertexBuiltIn(ioVariable))
{ {
perVertexBuiltIn = ioVariable; perVertexBuiltIn = ioVariable;
ioVariable = IoVariable.Position; ioVariable = IoVariable.Position;
@@ -26,12 +26,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
{ {
MemoryArrange.MemoryArrange4GiB or MemoryArrange.MemoryArrange4GiB or
MemoryArrange.MemoryArrange4GiBSystemDev or MemoryArrange.MemoryArrange4GiBSystemDev or
MemoryArrange.MemoryArrange6GiBAppletDev => 3152 * MiB, MemoryArrange.MemoryArrange6GiBAppletDev => 3173 * MiB,
MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB, MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB,
MemoryArrange.MemoryArrange6GiB => 4783 * MiB, MemoryArrange.MemoryArrange6GiB => 4804 * MiB,
MemoryArrange.MemoryArrange8GiB => 6831 * MiB, MemoryArrange.MemoryArrange8GiB => 6852 * MiB,
MemoryArrange.MemoryArrange10GiB => 8879 * MiB, MemoryArrange.MemoryArrange10GiB => 8900 * MiB,
MemoryArrange.MemoryArrange12GiB => 10927 * MiB, MemoryArrange.MemoryArrange12GiB => 10948 * MiB,
_ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."), _ => throw new ArgumentException($"Invalid memory arrange \"{arrange}\"."),
}; };
} }
@@ -15,7 +15,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private readonly Dictionary<ulong, List<KThread>> _condVarThreads; private readonly Dictionary<ulong, List<KThread>> _condVarThreads;
private readonly Dictionary<ulong, List<KThread>> _arbiterThreads; private readonly Dictionary<ulong, List<KThread>> _arbiterThreads;
private readonly ByDynamicPriority _byDynamicPriority;
public KAddressArbiter(KernelContext context) public KAddressArbiter(KernelContext context)
{ {
@@ -23,7 +22,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_condVarThreads = []; _condVarThreads = [];
_arbiterThreads = []; _arbiterThreads = [];
_byDynamicPriority = new ByDynamicPriority();
} }
public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle) public Result ArbitrateLock(int ownerHandle, ulong mutexAddress, int requesterHandle)
@@ -142,14 +140,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads)) if (_condVarThreads.TryGetValue(condVarAddress, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -332,14 +325,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads)) if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -424,14 +412,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
if (_arbiterThreads.TryGetValue(address, out List<KThread> threads)) if (_arbiterThreads.TryGetValue(address, out List<KThread> threads))
{ {
int i = 0; int i = FindDynamicPriorityFifoInsertionIndex(threads, currentThread);
if (threads.Count > 0)
{
i = threads.BinarySearch(currentThread, _byDynamicPriority);
if (i < 0) i = ~i;
}
threads.Insert(i, currentThread); threads.Insert(i, currentThread);
} }
else else
@@ -627,12 +610,28 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
return validCount; return validCount;
} }
private class ByDynamicPriority : IComparer<KThread> private static int FindDynamicPriorityFifoInsertionIndex(List<KThread> threads, KThread currentThread)
{ {
public int Compare(KThread x, KThread y) int low = 0;
int high = threads.Count;
// Lower numeric values represent higher priorities. Use upper-bound insertion
// to preserve FIFO order among waiters with the same dynamic priority.
while (low < high)
{ {
return x!.DynamicPriority.CompareTo(y!.DynamicPriority); int middle = low + ((high - low) >> 1);
if (threads[middle].DynamicPriority <= currentThread.DynamicPriority)
{
low = middle + 1;
}
else
{
high = middle;
}
} }
return low;
} }
} }
} }
+22 -2
View File
@@ -783,8 +783,28 @@ namespace Ryujinx.HLE.HOS
} }
List<Cheat> cheats = mods.Cheats; List<Cheat> cheats = mods.Cheats;
Dictionary<string, ulong> processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v }) Dictionary<string, ulong> processExes = new();
.ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
foreach ((string buildId, ulong codeAddress) in tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses))
{
string normalizedBuildId = buildId[..Math.Min(Cheat.CheatIdSize, buildId.Length)];
if (processExes.TryGetValue(normalizedBuildId, out ulong existingAddress))
{
if (existingAddress != codeAddress)
{
Logger.Warning?.Print(
LogClass.ModLoader,
$"Duplicate BuildId prefix '{normalizedBuildId}' has different code addresses. " +
$"Existing: 0x{existingAddress:X}, duplicate: 0x{codeAddress:X}. " +
$"Keeping the first one.");
}
continue;
}
processExes.Add(normalizedBuildId, codeAddress);
}
foreach (Cheat cheat in cheats) foreach (Cheat cheat in cheats)
{ {
@@ -71,7 +71,14 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
return _applicationServiceServer.IsUserRegistrationRequestPermitted(context); return _applicationServiceServer.IsUserRegistrationRequestPermitted(context);
} }
[CommandCmif(51)] [CommandCmif(51)] // 1.0.0 – 18.1.0
// TrySelectUserWithoutInteraction(bool) -> nn::account::Uid
public ResultCode TrySelectUserWithoutInteractionDeprecated(ServiceCtx context)
{
return _applicationServiceServer.TrySelectUserWithoutInteraction(context);
}
[CommandCmif(52)] // 19.0.0+
// TrySelectUserWithoutInteraction(bool) -> nn::account::Uid // TrySelectUserWithoutInteraction(bool) -> nn::account::Uid
public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context) public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context)
{ {
@@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private readonly bool[] _supportedPlayers; private readonly bool[] _supportedPlayers;
private VibrationValue _neutralVibrationValue = new() private VibrationValue _neutralVibrationValue = new()
{ {
AmplitudeLow = 0f, AmplitudeLow = 0.01f,
FrequencyLow = 160f, FrequencyLow = 160f,
AmplitudeHigh = 0f, AmplitudeHigh = 0f,
FrequencyHigh = 320f, FrequencyHigh = 320f,
+207 -70
View File
@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
@@ -12,140 +13,276 @@ namespace Ryujinx.Input.SDL3
{ {
private readonly SDL_hid_device* _hidHandle; private readonly SDL_hid_device* _hidHandle;
private byte[] _buffer;
private static ushort _vendor;
private static ushort _product;
private int _globalCount; private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle) private NpadHdRumble(SDL_hid_device* hidHandle)
{ {
_hidHandle = hidHandle; _hidHandle = hidHandle;
InitializeDevice();
} }
public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle) public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
{ {
ushort vendor = SDL_GetGamepadVendor(gamepadHandle); _vendor = SDL_GetGamepadVendor(gamepadHandle);
if (vendor != 0x057e) if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor))
{ {
return null; return null;
} }
ushort product = SDL_GetGamepadProduct(gamepadHandle); _product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e) if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product))
{ {
return null; 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 // Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
private void WriteHdRumble( private bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right)
int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp,
int encRightHighFreq, int encRightHighAmp)
{ {
byte[] buf = new byte[10]; int leftLowAmp = EncodeLowAmp(left.AmplitudeLow);
int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8);
buf[0] = 0x10; int leftHighFreq = EncodeHighFreq(left.FrequencyHigh);
buf[1] = (byte)((++_globalCount) & 0xF); int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8);
buf[2] = (byte)(encLeftHighFreq & 0xFF); int rightLowAmp = EncodeLowAmp(right.AmplitudeLow);
buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF)); int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8);
buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF)); int rightHighFreq = EncodeHighFreq(right.FrequencyHigh);
buf[5] = (byte)(encLeftLowAmp & 0xFF); int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8);
buf[6] = (byte)(encRightHighFreq & 0xFF); _buffer[0] = 0x10;
buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF)); _buffer[1] = (byte)((_globalCount++) & 0xF);
buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF));
buf[9] = (byte)(encRightLowAmp & 0xFF); // 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) if (_globalCount > 0xF)
{ {
_globalCount = 0x0; _globalCount = 0x0;
} }
fixed (byte* ptr = buf) fixed (byte* ptr = _buffer)
{ {
SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length); if (SendHdRumble(ptr, (nuint)_buffer.Length) >= 0)
{
return true;
}
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
} }
return false;
} }
private static int EncodeLowFreq(float lowFreq) private static int EncodeLowFreq(float lowFreq)
{ {
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f); return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f);
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40;
} }
private static int EncodeHighFreq(float highFreq) private static int EncodeHighFreq(float highFreq)
{ {
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f); return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f);
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4;
} }
private static int EncodeLowAmp(float rawAmp) private static int EncodeLowAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1; encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
} encodedAmp = Math.Round((encodedAmp / 2.0) + 64.0);
encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
return (int)Math.Floor(encodedAmp / 2.0) + 64; return (int)Math.Round(encodedAmp);
} }
private static int EncodeHighAmp(float rawAmp) private static int EncodeHighAmp(float rawAmp)
{ {
int encodedAmp = 0; double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f) if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1; encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f) else if (rawAmp is >= 0.012f and < 0.112f)
{ encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f) else if (rawAmp is >= 0.112f and < 0.225f)
{ encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f) else if (rawAmp is >= 0.225f and <= 1f)
{ encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
} encodedAmp = Math.Round(encodedAmp / 2.0);
encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
return encodedAmp * 2; return (int)encodedAmp;
} }
public bool HdRumble(VibrationValue left, VibrationValue right) public bool HdRumble(VibrationValue left, VibrationValue right)
{ {
WriteHdRumble(EncodeLowFreq(left.FrequencyLow), if(_product is (ushort) HDRumbleSupportedProduct.ProController
EncodeLowAmp(left.AmplitudeLow), or (ushort) HDRumbleSupportedProduct.JoyconLeft
EncodeHighFreq(left.FrequencyHigh), or (ushort) HDRumbleSupportedProduct.JoyconRight
EncodeHighAmp(left.AmplitudeHigh), or (ushort) HDRumbleSupportedProduct.JoyconPair
EncodeLowFreq(right.FrequencyLow), or (ushort) HDRumbleSupportedProduct.JoyconGrip)
EncodeLowAmp(right.AmplitudeLow), {
EncodeHighFreq(right.FrequencyHigh), return WriteNintendoHdRumble(left, right);
EncodeHighAmp(right.AmplitudeHigh)); }
return true;
return false;
}
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.
if ((currentTicks - _lastWriteTicks) <= GetPollRate())
{
return result;
}
result = SDL_hid_write(_hidHandle, data, length);
if (result >= 0)
{
_lastWriteTicks = currentTicks;
}
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() public void Dispose()
{ {
GC.SuppressFinalize(this);
SDL_hid_close(_hidHandle); SDL_hid_close(_hidHandle);
} }
} }
public enum HDRumbleSupportedVendor : ushort
{
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,
// Valve Steam Family: 0x28de
// https://github.com/libsdl-org/SDL/issues/9148
SteamDeck = 0x11ff,
SteamDeckVirtualDevice = 0x1205,
SteamController = 0x1106,
// PlayStation Dualsense: 0x054c
Dualsense = 0x0ce6
}
} }
+13 -2
View File
@@ -177,10 +177,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; return _hdRumble?.HdRumble(left, right) ?? false;
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if ((Features & GamepadFeaturesFlag.Rumble) == 0) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -199,6 +201,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
} }
if (!String.IsNullOrEmpty(SDL_GetError()))
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+14 -3
View File
@@ -163,7 +163,7 @@ namespace Ryujinx.Input.SDL3
public void SetTriggerThreshold(float triggerThreshold) public void SetTriggerThreshold(float triggerThreshold)
{ {
// No operations
} }
public bool HDRumble(VibrationValue left, VibrationValue right) public bool HDRumble(VibrationValue left, VibrationValue right)
@@ -171,10 +171,12 @@ namespace Ryujinx.Input.SDL3
return _hdRumble?.HdRumble(left, right) ?? false; return _hdRumble?.HdRumble(left, right) ?? false;
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if ((Features & GamepadFeaturesFlag.Rumble) == 0) if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return; {
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -193,6 +195,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs)) if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller."); Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
} }
if (!String.IsNullOrEmpty(SDL_GetError()))
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+21 -2
View File
@@ -1,4 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Gommon;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
return left.IsPressed(inputId) || right.IsPressed(inputId); return left.IsPressed(inputId) || right.IsPressed(inputId);
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{
// return _hdRumble?.HdRumble(left, right) ?? false;
// TODO: Track rumble and motion on both controllers
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
if (lowFrequency != 0) if (lowFrequency != 0)
{ {
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
left.Rumble(0, 0, durationMs); left.Rumble(0, 0, durationMs);
right.Rumble(0, 0, durationMs); right.Rumble(0, 0, durationMs);
} }
if (!SDL_GetError().IsNullOrEmpty())
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
return false;
}
return true;
} }
public void SetConfiguration(InputConfig configuration) public void SetConfiguration(InputConfig configuration)
+8 -2
View File
@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using SDL; using SDL;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
@@ -385,9 +386,14 @@ namespace Ryujinx.Input.SDL3
// No operations // No operations
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{ {
// No operations return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
return false;
} }
public Vector3 GetMotionData(MotionInputId inputId) public Vector3 GetMotionData(MotionInputId inputId)
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Numerics; using System.Numerics;
@@ -66,7 +67,12 @@ namespace Ryujinx.Input.SDL3
throw new NotImplementedException(); throw new NotImplementedException();
} }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool HDRumble(VibrationValue left, VibrationValue right)
{
throw new NotImplementedException();
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
+29 -26
View File
@@ -554,34 +554,37 @@ namespace Ryujinx.Input.HLE
{ {
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue)) if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
{ {
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble) if (_config is not StandardControllerInputConfig controllerConfig ||
!controllerConfig.Rumble.EnableRumble)
{ {
VibrationValue leftVibrationValue = dualVibrationValue.Item1; return;
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;
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} " +
$"R.low.freq={rightVibrationValue.FrequencyLow}, " +
$"R.high.freq={rightVibrationValue.FrequencyHigh}");
} }
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 (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
{
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} " +
// 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.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.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.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({rightVibrationValue.FrequencyLow}), " +
$"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})");
} }
} }
} }
+3 -6
View File
@@ -71,10 +71,7 @@ namespace Ryujinx.Input
/// </summary> /// </summary>
/// <param name="left">The vibration data for the left side</param> /// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param> /// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right) bool HDRumble(VibrationValue left, VibrationValue right);
{
return false;
}
/// <summary> /// <summary>
/// Starts a rumble effect on the gamepad. /// Starts a rumble effect on the gamepad.
@@ -82,10 +79,10 @@ namespace Ryujinx.Input
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param> /// <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> /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param> /// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
void Rumble(float lowFrequency, float highFrequency, uint durationMs); bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <summary> /// <summary>
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>. /// Get a snaphost of the state of the gamepad that is remapped with the information from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
/// </summary> /// </summary>
/// <returns>A remapped snaphost of the state of the gamepad.</returns> /// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot(); GamepadStateSnapshot GetMappedStateSnapshot();
@@ -1422,6 +1422,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableRumble = false, EnableRumble = false,
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
UseHDRumble = false
}; };
} }
} }
+2
View File
@@ -462,6 +462,8 @@
"ControllerSettingsRumble": "Rumble", "ControllerSettingsRumble": "Rumble",
"ControllerSettingsRumbleStrongMultiplier": "Strong Rumble Multiplier", "ControllerSettingsRumbleStrongMultiplier": "Strong Rumble Multiplier",
"ControllerSettingsRumbleWeakMultiplier": "Weak Rumble Multiplier", "ControllerSettingsRumbleWeakMultiplier": "Weak Rumble Multiplier",
"ControllerSettingsRumbleUseHDRumble": "Enable HD Rumble",
"HDRumbleTooltip": "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.",
"DialogMessageSaveNotAvailableMessage": "There is no savedata for {0} [{1:x16}]", "DialogMessageSaveNotAvailableMessage": "There is no savedata for {0} [{1:x16}]",
"DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?", "DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?",
"DialogConfirmationTitle": "Ryujinx - Confirmation", "DialogConfirmationTitle": "Ryujinx - Confirmation",
@@ -223,6 +223,7 @@ namespace Ryujinx.Headless
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = true
}, },
}; };
} }
+14 -2
View File
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -149,9 +150,20 @@ namespace Ryujinx.Ava.Input
} }
} }
public void SetTriggerThreshold(float triggerThreshold) { } public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { } public bool HDRumble(VibrationValue left, VibrationValue right)
{
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
return false;
}
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Drawing; using System.Drawing;
@@ -63,8 +64,13 @@ namespace Ryujinx.Ava.Input
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public bool HDRumble(VibrationValue left, VibrationValue right)
{
throw new NotImplementedException();
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -19,6 +19,7 @@ namespace Ryujinx.Ava.UI.Models.Input
public float WeakRumble { get; set; } public float WeakRumble { get; set; }
public float StrongRumble { get; set; } public float StrongRumble { get; set; }
public bool UseHDRumble { get; set; }
public string Id { get; set; } public string Id { get; set; }
public ControllerType ControllerType { get; set; } public ControllerType ControllerType { get; set; }
@@ -202,6 +203,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = controllerInput.Rumble.EnableRumble; EnableRumble = controllerInput.Rumble.EnableRumble;
WeakRumble = controllerInput.Rumble.WeakRumble; WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble; StrongRumble = controllerInput.Rumble.StrongRumble;
UseHDRumble = controllerInput.Rumble.UseHDRumble;
} }
} }
} }
@@ -259,6 +261,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = EnableRumble, EnableRumble = EnableRumble,
WeakRumble = WeakRumble, WeakRumble = WeakRumble,
StrongRumble = StrongRumble, StrongRumble = StrongRumble,
UseHDRumble = UseHDRumble,
}, },
Version = InputConfig.CurrentVersion, Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft, DeadzoneLeft = DeadzoneLeft,
@@ -678,6 +678,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
StrongRumble = 1f, StrongRumble = 1f,
WeakRumble = 1f, WeakRumble = 1f,
EnableRumble = false, EnableRumble = false,
UseHDRumble = false
}, },
}; };
} }
@@ -5,9 +5,12 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public partial class RumbleInputViewModel : BaseModel public partial class RumbleInputViewModel : BaseModel
{ {
[ObservableProperty] [ObservableProperty]
private float _strongRumble; public partial float StrongRumble { get; set; }
[ObservableProperty] [ObservableProperty]
private float _weakRumble; public partial float WeakRumble { get; set; }
[ObservableProperty]
public partial bool EnableHDRumble { get; set; }
} }
} }
@@ -55,6 +55,15 @@
Margin="5,0" Margin="5,0"
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" /> Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
</StackPanel> </StackPanel>
<CheckBox
Margin="5"
IsChecked="{Binding EnableHDRumble}">
<TextBlock
Margin="0,3,0,0"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsRumbleUseHDRumble}"
ToolTip.Tip="{locale:Locale HDRumbleTooltip}" />
</CheckBox>
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -24,6 +24,7 @@ namespace Ryujinx.Ava.UI.Views.Input
{ {
StrongRumble = config.StrongRumble, StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble, WeakRumble = config.WeakRumble,
EnableHDRumble = config.UseHDRumble
}; };
InitializeComponent(); InitializeComponent();
@@ -49,6 +50,7 @@ namespace Ryujinx.Ava.UI.Views.Input
GamepadInputConfig config = viewModel.Config; GamepadInputConfig config = viewModel.Config;
config.StrongRumble = content._viewModel.StrongRumble; config.StrongRumble = content._viewModel.StrongRumble;
config.WeakRumble = content._viewModel.WeakRumble; config.WeakRumble = content._viewModel.WeakRumble;
config.UseHDRumble = content._viewModel.EnableHDRumble;
}; };
await contentDialog.ShowAsync(); await contentDialog.ShowAsync();