Author SHA1 Message Date
Coxxs 280c427c55 Implement CreateLibraryAppletEx in ILibraryAppletCreator 2026-09-18 01:11:26 -05:00
KeatonTheBot 25b4892981 Raise application pool sizes for DRAM selections (again)
- Fixes Ultracam 20+ (beta) support
2026-09-18 01:11:26 -05:00
Neo d16d67f7c4 HLE: Update ProcessResult in ProcessLoader.cs
Some games/applications may crash with a specific error:

00:00:00.690 |E| Application : Unhandled exception caught: System.Collections.Generic.KeyNotFoundException: The given key '0' was not present in the dictionary.

(or a different key)

However, this message is misleading and not indicative of the actual error (it masks it).

As an example, the error above was seen when attempting to load a mod onto Yo-kai Watch 1 (base game + update worked). However, after patching the ProcessResult, the resulting error was more revealing:

00:00:00.716 |E| Application : Unhandled exception caught: LibHac.Common.HorizonResultException: ResultFsInvalidCharacter (2002-6004): Error creating LocalFileSystem. at LibHac.FsSystem.LocalFileSystem..ctor(String rootPath)

(and so forth)

The actual error was due to an incorrect folder name in the Mods folder.
2026-09-18 01:11:26 -05:00
KeatonTheBot e7cb0632c5 Fix build errors in NceCpuContext, LibKenjinx.Input 2026-09-14 14:45:05 -05:00
KeatonTheBot 4ea8c0b280 ContentDialogHelper: Fix explicit type 2026-09-10 23:50:22 -05:00
Max aef309800c Fixed rumble not being sent to the controller
(cherry picked from commit 892fcd9444)
2026-09-10 23:45:47 -05:00
Max a008cd20f1 [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.

(cherry picked from commit e3150d20bb)
2026-09-10 23:45:46 -05:00
Max ec6ce46357 [HID] Fixed HD Rumble latency 2026-09-10 23:45:46 -05:00
stossy11 a7a3a17b48 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.

(cherry picked from commit 5d3e392082)
2026-09-10 23:35:14 -05:00
Neo 94cafdd1f9 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.

(cherry picked from commit c57e91cc59)
2026-09-10 23:35:11 -05:00
Neo 1b57022694 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.

(cherry picked from commit 0381c7157b)
2026-09-10 23:35:08 -05:00
avan 2bb07a4364 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.

(cherry picked from commit 93b4c53c8a)
2026-09-10 23:35:05 -05:00
avan 5e67569a6e 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.

(cherry picked from commit ef14467d1f)
2026-09-10 23:34:59 -05:00
KeatonTheBot 97f0336aa2 Raise application pool sizes for DRAM selections
- Fixes crash with BotW and possibly other games

(cherry picked from commit e07f333a31)
2026-09-05 14:33:15 -05:00
31 changed files with 472 additions and 159 deletions
+16 -3
View File
@@ -2,6 +2,7 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using System;
@@ -11,6 +12,8 @@ using System.Linq;
using System.Numerics;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using ControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
using PlayerIndex = Ryujinx.Common.Configuration.Hid.PlayerIndex;
using StickInputId = Ryujinx.Input.StickInputId;
namespace LibKenjinx
@@ -268,7 +271,12 @@ namespace LibKenjinx
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();
}
@@ -487,9 +495,14 @@ namespace LibKenjinx
//throw new System.NotImplementedException();
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
public bool HDRumble(VibrationValue left, VibrationValue right)
{
//throw new System.NotImplementedException();
throw new NotImplementedException();
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
throw new NotImplementedException();
}
public GamepadStateSnapshot GetMappedStateSnapshot()
@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Enable Rumble
/// </summary>
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 = "")
{
return ResolveManifestPath(path).Item1.GetManifestResourceNames()
.Where(r => r.EndsWith(ext))
(Assembly assembly, string resourcePath) = ResolveManifestPath(path);
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();
}
+2 -1
View File
@@ -1,4 +1,5 @@
using ARMeilleure.Memory;
using ARMeilleure.Translation.PTC;
using Ryujinx.Cpu.Signal;
using Ryujinx.Common;
using Ryujinx.Memory;
@@ -127,7 +128,7 @@ namespace Ryujinx.Cpu.Nce
}
/// <inheritdoc/>
public IDiskCacheLoadState LoadDiskCache(string titleIdText, string displayVersion, bool enabled, string cacheSelector)
public IDiskCacheLoadState LoadDiskCache(PtcCacheInfo cacheInfo, bool enabled)
{
return new DummyDiskCacheLoadState();
}
@@ -1773,7 +1773,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
(_, varType) = IoMap.GetSpirvBuiltIn(ioVariable);
if (IoMap.IsPerVertexBuiltIn(ioVariable))
if (context.Definitions.Stage.IsVtg() && IoMap.IsPerVertexBuiltIn(ioVariable))
{
perVertexBuiltIn = ioVariable;
ioVariable = IoVariable.Position;
@@ -26,12 +26,12 @@ namespace Ryujinx.HLE.HOS.Kernel.Common
{
MemoryArrange.MemoryArrange4GiB or
MemoryArrange.MemoryArrange4GiBSystemDev or
MemoryArrange.MemoryArrange6GiBAppletDev => 3152 * MiB,
MemoryArrange.MemoryArrange6GiBAppletDev => 3236 * MiB,
MemoryArrange.MemoryArrange4GiBAppletDev => 2048 * MiB,
MemoryArrange.MemoryArrange6GiB => 4783 * MiB,
MemoryArrange.MemoryArrange8GiB => 6831 * MiB,
MemoryArrange.MemoryArrange10GiB => 8879 * MiB,
MemoryArrange.MemoryArrange12GiB => 10927 * MiB,
MemoryArrange.MemoryArrange6GiB => 4867 * MiB,
MemoryArrange.MemoryArrange8GiB => 6915 * MiB,
MemoryArrange.MemoryArrange10GiB => 8963 * MiB,
MemoryArrange.MemoryArrange12GiB => 11011 * MiB,
_ => 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>> _arbiterThreads;
private readonly ByDynamicPriority _byDynamicPriority;
public KAddressArbiter(KernelContext context)
{
@@ -23,7 +22,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
_condVarThreads = [];
_arbiterThreads = [];
_byDynamicPriority = new ByDynamicPriority();
}
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))
{
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);
}
else
@@ -332,14 +325,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
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);
}
else
@@ -424,14 +412,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
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);
}
else
@@ -627,12 +610,28 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
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
@@ -749,8 +749,28 @@ namespace Ryujinx.HLE.HOS
}
List<Cheat> cheats = mods.Cheats;
Dictionary<string, ulong> processExes = tamperInfo.BuildIds.Zip(tamperInfo.CodeAddresses, (k, v) => new { k, v })
.ToDictionary(x => x.k[..Math.Min(Cheat.CheatIdSize, x.k.Length)], x => x.v);
Dictionary<string, ulong> processExes = new();
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)
{
@@ -71,7 +71,14 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
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
public ResultCode TrySelectUserWithoutInteraction(ServiceCtx context)
{
@@ -21,6 +21,21 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Sys
return ResultCode.Success;
}
[CommandCmif(3)] // 20.0.0+
// CreateLibraryAppletEx(u32, u32, u64) -> object<nn::am::service::ILibraryAppletAccessor>
public ResultCode CreateLibraryAppletEx(ServiceCtx context)
{
AppletId appletId = (AppletId)context.RequestData.ReadInt32();
_ = context.RequestData.ReadInt32(); // libraryAppletMode
_ = context.RequestData.ReadUInt64(); // threadId
MakeObject(context, new ILibraryAppletAccessor(appletId, context.Device.System));
return ResultCode.Success;
}
[CommandCmif(10)]
// CreateStorage(u64) -> object<nn::am::service::IStorage>
public ResultCode CreateStorage(ServiceCtx context)
@@ -23,7 +23,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private readonly bool[] _supportedPlayers;
private VibrationValue _neutralVibrationValue = new()
{
AmplitudeLow = 0f,
AmplitudeLow = 0.01f,
FrequencyLow = 160f,
AmplitudeHigh = 0f,
FrequencyHigh = 320f,
@@ -24,7 +24,16 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
public ProcessResult ActiveApplication => _processesByPid[_latestPid];
public ProcessResult ActiveApplication
{
get
{
if (_latestPid != 0 && _processesByPid.TryGetValue(_latestPid, out ProcessResult process))
return process;
return null;
}
}
public ProcessLoader(Switch device)
{
+207 -70
View File
@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL;
using static SDL.SDL3;
@@ -12,140 +13,276 @@ namespace Ryujinx.Input.SDL3
{
private readonly SDL_hid_device* _hidHandle;
private byte[] _buffer;
private static ushort _vendor;
private static ushort _product;
private int _globalCount;
private ulong _lastWriteTicks;
private NpadHdRumble(SDL_hid_device* hidHandle)
{
_hidHandle = hidHandle;
InitializeDevice();
}
public static NpadHdRumble Create(SDL_Gamepad* gamepadHandle)
{
ushort vendor = SDL_GetGamepadVendor(gamepadHandle);
if (vendor != 0x057e)
_vendor = SDL_GetGamepadVendor(gamepadHandle);
if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor))
{
return null;
}
ushort product = SDL_GetGamepadProduct(gamepadHandle);
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e)
_product = SDL_GetGamepadProduct(gamepadHandle);
if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product))
{
return null;
}
return new NpadHdRumble(SDL_hid_open(vendor, product, 0));
int serialNumber = 0;
string? serial = SDL_GetGamepadSerial(gamepadHandle);
if (serial is not null)
{
int.TryParse(serial, out serialNumber);
}
return new NpadHdRumble(SDL_hid_open(_vendor, _product, serialNumber));
}
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
private void WriteHdRumble(
int encLeftLowFreq, int encLeftLowAmp,
int encLeftHighFreq, int encLeftHighAmp,
int encRightLowFreq, int encRightLowAmp,
int encRightHighFreq, int encRightHighAmp)
private bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right)
{
byte[] buf = new byte[10];
buf[0] = 0x10;
buf[1] = (byte)((++_globalCount) & 0xF);
buf[2] = (byte)(encLeftHighFreq & 0xFF);
buf[3] = (byte)(encLeftHighAmp + ((encLeftHighFreq >> 8) & 0xFF));
buf[4] = (byte)(encLeftLowFreq + ((encLeftLowAmp >> 8) & 0xFF));
buf[5] = (byte)(encLeftLowAmp & 0xFF);
buf[6] = (byte)(encRightHighFreq & 0xFF);
buf[7] = (byte)(encRightHighAmp + ((encRightHighFreq >> 8) & 0xFF));
buf[8] = (byte)(encRightLowFreq + ((encRightLowAmp >> 8) & 0xFF));
buf[9] = (byte)(encRightLowAmp & 0xFF);
int leftLowAmp = EncodeLowAmp(left.AmplitudeLow);
int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8);
int leftHighFreq = EncodeHighFreq(left.FrequencyHigh);
int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8);
int rightLowAmp = EncodeLowAmp(right.AmplitudeLow);
int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8);
int rightHighFreq = EncodeHighFreq(right.FrequencyHigh);
int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8);
_buffer[0] = 0x10;
_buffer[1] = (byte)((_globalCount++) & 0xF);
// 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)
{
_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)
{
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f);
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40;
return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f);
}
private static int EncodeHighFreq(float highFreq)
{
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f);
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4;
return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f);
}
private static int EncodeLowAmp(float rawAmp)
{
int encodedAmp = 0;
double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return (int)Math.Floor(encodedAmp / 2.0) + 64;
encodedAmp = 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.Round(encodedAmp);
}
private static int EncodeHighAmp(float rawAmp)
{
int encodedAmp = 0;
double encodedAmp = 0;
if (rawAmp is > 0 and < 0.012f)
{
encodedAmp = 1;
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
}
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
}
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return encodedAmp * 2;
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = Math.Round(encodedAmp / 2.0);
encodedAmp = Math.Clamp(encodedAmp, 0.0, 100.2867);
return (int)encodedAmp;
}
public bool HdRumble(VibrationValue left, VibrationValue right)
{
WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
EncodeLowAmp(left.AmplitudeLow),
EncodeHighFreq(left.FrequencyHigh),
EncodeHighAmp(left.AmplitudeHigh),
EncodeLowFreq(right.FrequencyLow),
EncodeLowAmp(right.AmplitudeLow),
EncodeHighFreq(right.FrequencyHigh),
EncodeHighAmp(right.AmplitudeHigh));
return true;
if(_product is (ushort) HDRumbleSupportedProduct.ProController
or (ushort) HDRumbleSupportedProduct.JoyconLeft
or (ushort) HDRumbleSupportedProduct.JoyconRight
or (ushort) HDRumbleSupportedProduct.JoyconPair
or (ushort) HDRumbleSupportedProduct.JoyconGrip)
{
return WriteNintendoHdRumble(left, right);
}
return false;
}
private int SendHdRumble(byte* data, nuint length)
{
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()
{
GC.SuppressFinalize(this);
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;
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if ((Features & GamepadFeaturesFlag.Rumble) == 0)
return;
{
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -199,6 +201,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
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)
+14 -3
View File
@@ -163,7 +163,7 @@ namespace Ryujinx.Input.SDL3
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public bool HDRumble(VibrationValue left, VibrationValue right)
@@ -171,10 +171,12 @@ namespace Ryujinx.Input.SDL3
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)
return;
{
return false;
}
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
@@ -193,6 +195,15 @@ namespace Ryujinx.Input.SDL3
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
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)
+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.Linq;
using System.Numerics;
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
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)
{
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
left.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)
+8 -2
View File
@@ -1,6 +1,7 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using SDL;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -385,9 +386,14 @@ namespace Ryujinx.Input.SDL3
// 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)
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Drawing;
using System.Numerics;
@@ -66,7 +67,12 @@ namespace Ryujinx.Input.SDL3
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();
}
+30 -26
View File
@@ -554,34 +554,38 @@ namespace Ryujinx.Input.HLE
{
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;
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}");
return;
}
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
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)));
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))
{
_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>
/// <param name="left">The vibration data for the left side</param>
/// <param name="right">The vibration data for the right side</param>
bool HDRumble(VibrationValue left, VibrationValue right)
{
return false;
}
bool HDRumble(VibrationValue left, VibrationValue right);
/// <summary>
/// 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="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>
void Rumble(float lowFrequency, float highFrequency, uint durationMs);
bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
/// <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>
/// <returns>A remapped snaphost of the state of the gamepad.</returns>
GamepadStateSnapshot GetMappedStateSnapshot();
@@ -1323,6 +1323,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableRumble = false,
StrongRumble = 1f,
WeakRumble = 1f,
UseHDRumble = false
};
}
}
+2
View File
@@ -455,6 +455,8 @@
"ControllerSettingsRumble": "Rumble",
"ControllerSettingsRumbleStrongMultiplier": "Strong 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}]",
"DialogMessageSaveNotAvailableCreateSaveMessage": "Would you like to create savedata for this game?",
"DialogConfirmationTitle": "Ryujinx - Confirmation",
@@ -222,6 +222,7 @@ namespace Ryujinx.Headless
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = true
},
};
}
+14 -2
View File
@@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input;
using System;
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;
+7 -1
View File
@@ -1,4 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input;
using System;
using System.Drawing;
@@ -63,8 +64,13 @@ namespace Ryujinx.Ava.Input
{
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();
}
@@ -121,7 +121,7 @@ namespace Ryujinx.Ava.UI.Helpers
startedDeferring = true;
ContentDialogButtonClickDeferral deferral = args.GetDeferral();
Deferral deferral = args.GetDeferral();
sender.PrimaryButtonClick -= DeferClose;
@@ -19,6 +19,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; }
public ControllerType ControllerType { get; set; }
@@ -482,6 +483,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = controllerInput.Rumble.EnableRumble;
WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble;
UseHDRumble = controllerInput.Rumble.UseHDRumble;
}
}
}
@@ -539,6 +541,7 @@ namespace Ryujinx.Ava.UI.Models.Input
EnableRumble = EnableRumble,
WeakRumble = WeakRumble,
StrongRumble = StrongRumble,
UseHDRumble = UseHDRumble,
},
Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft,
@@ -685,6 +685,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = false
},
};
}
@@ -23,5 +23,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
OnPropertyChanged();
}
}
private bool _enableHDRumble;
public bool EnableHDRumble
{
get => _enableHDRumble;
set
{
_enableHDRumble = value;
OnPropertyChanged();
}
}
}
}
@@ -55,6 +55,15 @@
Margin="5,0"
Text="{Binding WeakRumble, StringFormat=\{0:0.00\}}" />
</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>
</Grid>
</UserControl>
@@ -24,6 +24,7 @@ namespace Ryujinx.Ava.UI.Views.Input
{
StrongRumble = config.StrongRumble,
WeakRumble = config.WeakRumble,
EnableHDRumble = config.UseHDRumble
};
InitializeComponent();
@@ -49,6 +50,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();