basically rewrote the class for clarity and structure

This commit is contained in:
Max 2026-05-22 16:07:10 -04:00
parent e921043d23
commit f8a2e4ca8e
2 changed files with 146 additions and 133 deletions

View file

@ -1,9 +1,8 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input.HLE;
using SDL;
using static SDL.SDL3;
using System;
using System;
namespace Ryujinx.Input.SDL3
{
@ -13,7 +12,9 @@ namespace Ryujinx.Input.SDL3
public unsafe class NpadHdRumble : IDisposable
{
private readonly SDL_hid_device* _hidHandle;
private readonly ulong _pollRate;
private byte[] _buffer;
private static ushort _vendor;
private static ushort _product;
private int _globalCount;
@ -23,79 +24,88 @@ namespace Ryujinx.Input.SDL3
{
_hidHandle = hidHandle;
InitializeDevice();
_pollRate = GetPollRate();
}
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;
}
_product = SDL_GetGamepadProduct(gamepadHandle);
if (!Enum.IsDefined(typeof(HDRumbleSupported), _product))
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 bool 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);
// Clamping should be done for LRA safety.
int leftLowAmp = EncodeLowAmp(left.AmplitudeLow) + 0x80;
int leftLowFreq = EncodeLowFreq(left.FrequencyLow) + (leftLowAmp >> 8);
int leftHighFreq = EncodeHighFreq(left.FrequencyHigh);
int leftHighAmp = EncodeHighAmp(left.AmplitudeHigh) + (leftHighFreq >> 8);
int rightLowAmp = EncodeLowAmp(right.AmplitudeLow) + 0x80;
int rightLowFreq = EncodeLowFreq(right.FrequencyLow) + (rightLowAmp >> 8);
int rightHighFreq = EncodeHighFreq(left.FrequencyHigh);
int rightHighAmp = EncodeHighAmp(right.AmplitudeHigh) + (rightHighFreq >> 8);
_buffer[0] = 0x10;
_buffer[1] = (byte)((++_globalCount) & 0xF);
_buffer[2] = (byte)(leftLowFreq & 0xFF);
_buffer[3] = (byte)(leftHighAmp & 0xFF);
_buffer[4] = (byte)(leftHighFreq & 0xFF);
_buffer[5] = (byte)(leftLowAmp & 0xFF);
_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)
{
if (SendHdRumble(ptr, (nuint)buf.Length) >= 0)
if (SendHdRumble(ptr, (nuint)_buffer.Length) >= 0)
{
return true;
}
if (!String.IsNullOrEmpty(SDL_GetError()))
{
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
SDL_ClearError();
}
return false;
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.Round((32 * Math.Log2(lf / 10)) - 0x40);
}
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.Round(((32 * Math.Log2(hf / 10)) - 0x60) * 4);
}
private static int EncodeLowAmp(float rawAmp)
@ -108,18 +118,18 @@ namespace Ryujinx.Input.SDL3
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return (int)Math.Floor(encodedAmp / 2.0) + 64;
return (int) (encodedAmp / 2) + 64;
}
private static int EncodeHighAmp(float rawAmp)
@ -132,30 +142,32 @@ namespace Ryujinx.Input.SDL3
}
else if (rawAmp is >= 0.012f and < 0.112f)
{
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
encodedAmp = Math.Round(4 * Math.Log2(rawAmp * 110f));
}
else if (rawAmp is >= 0.112f and < 0.225f)
{
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
encodedAmp = Math.Round(16 * Math.Log2(rawAmp * 17f));
}
else if (rawAmp is >= 0.225f and <= 1f)
{
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
encodedAmp = Math.Round(32 * Math.Log2(rawAmp * 8.7f));
}
return (int) Math.Round(encodedAmp * 2);
return (int) encodedAmp * 2;
}
public bool HdRumble(VibrationValue left, VibrationValue right)
{
return 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));
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)
@ -164,123 +176,124 @@ namespace Ryujinx.Input.SDL3
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.
// https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX
if ((currentTicks - _lastWriteTicks) <= _pollRate)
if ((currentTicks - _lastWriteTicks) <= GetPollRate())
{
return result;
}
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)
{
// 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;
}
_lastWriteTicks = currentTicks;
}
SDL_UnlockJoysticks();
return result;
}
private void InitializeDevice()
{
byte[] init = new byte[64];
// Pro Controller and Charge Grip
if (_product
is (ushort)HDRumbleSupported.ProController
or (ushort)HDRumbleSupported.JoyconGrip)
if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
{
init[0] = 0x80;
init[1] = 0x02;
fixed (byte* ptr = init)
{
SDL_hid_write(_hidHandle, ptr, 64);
}
_buffer = new byte[10];
byte[] init = new byte[64];
return;
}
// Joycons
if (_product
is (ushort)HDRumbleSupported.JoyconLeft
or (ushort)HDRumbleSupported.JoyconRight
or (ushort)HDRumbleSupported.JoyconPair)
{
init[0] = 0x01;
init[1] = 0x01;
init[2] = 0x00;
init[3] = 0x01;
init[4] = 0x40;
init[5] = 0x40;
init[6] = 0x00;
init[7] = 0x01;
init[8] = 0x40;
init[9] = 0x40;
// TODO: Resend with updated packet for player number LEDs.
// And probably move this to NpadDevices.
init[10] = 0x01; // 0x30
init[11] = 0x01; // 0x01 0x03 0x07 0x0F
fixed (byte* ptr = init)
// Pro Controller and Charge Grip
if (_product
is (ushort)HDRumbleSupportedProduct.ProController
or (ushort)HDRumbleSupportedProduct.JoyconGrip)
{
SDL_hid_write(_hidHandle, ptr, 64);
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;
}
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()
{
byte[] dataArray = new byte[10];
int read;
ulong startTime = SDL_GetTicks();
fixed (byte* ptr = dataArray)
ulong pollRate = 0;
if (_vendor is (ushort)HDRumbleSupportedVendor.Nintendo)
{
read = SDL_hid_read(_hidHandle, ptr, 10);
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;
}
}
ulong endTime = SDL_GetTicks();
ulong readTime = endTime - startTime;
Logger.Debug?.PrintMsg(LogClass.Hid, $"POLL RATE: {readTime}ms.");
if (read == 0)
{
return 0;
}
return readTime;
Logger.Debug?.PrintMsg(LogClass.Hid, $"POLL RATE: {pollRate}ms.");
return pollRate;
}
public void Dispose()
{
GC.SuppressFinalize(this);
SDL_hid_close(_hidHandle);
}
}
public enum HDRumbleSupported : ushort
public enum HDRumbleSupportedVendor : ushort
{
// Currently, HD Rumble only supports the Pro Controller and Joycons.
// We need to initialize each device differently.
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.
// When this happens, we'll refactor this class to reflect it.
// 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
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
}
}

View file

@ -578,12 +578,12 @@ namespace Ryujinx.Input.HLE
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.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.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " +
$"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " +
$"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.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " +
$"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})");
}
}