mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-28 07:09:05 +00:00
- 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. Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/109
289 lines
9.6 KiB
C#
289 lines
9.6 KiB
C#
using Ryujinx.Common.Logging;
|
|
using Ryujinx.HLE.HOS.Services.Hid;
|
|
using SDL;
|
|
using static SDL.SDL3;
|
|
using System;
|
|
|
|
namespace Ryujinx.Input.SDL3
|
|
{
|
|
/// <summary>
|
|
/// Manages a HID handle of a gamepad to encode and write HD rumble commands for Nin controllers.
|
|
/// </summary>
|
|
public unsafe class NpadHdRumble : IDisposable
|
|
{
|
|
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)
|
|
{
|
|
_vendor = SDL_GetGamepadVendor(gamepadHandle);
|
|
if (!Enum.IsDefined(typeof(HDRumbleSupportedVendor), _vendor))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
_product = SDL_GetGamepadProduct(gamepadHandle);
|
|
if (!Enum.IsDefined(typeof(HDRumbleSupportedProduct), _product))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
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 bool WriteNintendoHdRumble(VibrationValue left, VibrationValue right)
|
|
{
|
|
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 = _buffer)
|
|
{
|
|
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)
|
|
{
|
|
return (int)Math.Clamp(32 * Math.Log2(lowFreq * 0.1f) - 0x40, 81.75177f, 1252.572266f);
|
|
}
|
|
|
|
private static int EncodeHighFreq(float highFreq)
|
|
{
|
|
return (int)Math.Clamp(32 * Math.Log2(highFreq * 0.1f) - 0x60, 81.75177f, 1252.572266f);
|
|
}
|
|
|
|
private static int EncodeLowAmp(float rawAmp)
|
|
{
|
|
double encodedAmp = 0;
|
|
|
|
if (rawAmp is > 0 and < 0.012f)
|
|
encodedAmp = 1;
|
|
|
|
else if (rawAmp is >= 0.012f and < 0.112f)
|
|
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
|
|
|
|
else if (rawAmp is >= 0.112f and < 0.225f)
|
|
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
|
|
|
|
else if (rawAmp is >= 0.225f and <= 1f)
|
|
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)
|
|
{
|
|
double encodedAmp = 0;
|
|
|
|
if (rawAmp is > 0 and < 0.012f)
|
|
encodedAmp = 1;
|
|
|
|
else if (rawAmp is >= 0.012f and < 0.112f)
|
|
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
|
|
|
|
else if (rawAmp is >= 0.112f and < 0.225f)
|
|
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
|
|
|
|
else if (rawAmp is >= 0.225f and <= 1f)
|
|
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)
|
|
{
|
|
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
|
|
}
|
|
}
|