Implement vibrations (#2468)

* First working vibration implementation

* Fix Infinite Rumble in SDL2Mouse

* Stop ignoring one vibValues every 2

* Remove RumbleInfinity as suggested

* Reworked all the vibration handle / calculation

* Revert HidVibrationDevicePosition changes

* Add UI to enable and tune rumble

* Remove some stub logs

* Add PlayerIndex in rumble debug log

* Fix all requested changes

* Implements hid::GetVibrationDeviceInfo

* Better implements HidVibrationValue.Equals/GetHashCode

* Added requested changes from code review

* Last fixes from review

* Update configuration file version for rebase
This commit is contained in:
mpnico 2021-08-05 00:39:40 +02:00 committed by GitHub
parent 46ffc81d90
commit 70f79e689b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 468 additions and 50 deletions

View file

@ -33,5 +33,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Controller Motion Settings /// Controller Motion Settings
/// </summary> /// </summary>
public MotionConfigController Motion { get; set; } public MotionConfigController Motion { get; set; }
/// <summary>
/// Controller Rumble Settings
/// </summary>
public RumbleConfigController Rumble { get; set; }
} }
} }

View file

@ -0,0 +1,20 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class RumbleConfigController
{
/// <summary>
/// Controller Strong Rumble Multiplier
/// </summary>
public float StrongRumble { get; set; }
/// <summary>
/// Controller Weak Rumble Multiplier
/// </summary>
public float WeakRumble { get; set; }
/// <summary>
/// Enable Rumble
/// </summary>
public bool EnableRumble { get; set; }
}
}

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Hid.Types; using Ryujinx.HLE.HOS.Services.Hid.Types;
@ -20,11 +22,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private ControllerType[] _configuredTypes; private ControllerType[] _configuredTypes;
private KEvent[] _styleSetUpdateEvents; private KEvent[] _styleSetUpdateEvents;
private bool[] _supportedPlayers; private bool[] _supportedPlayers;
private static HidVibrationValue _neutralVibrationValue = new HidVibrationValue
{
AmplitudeLow = 0f,
FrequencyLow = 160f,
AmplitudeHigh = 0f,
FrequencyHigh = 320f
};
internal NpadJoyHoldType JoyHold { get; set; } internal NpadJoyHoldType JoyHold { get; set; }
internal bool SixAxisActive = false; // TODO: link to hidserver when implemented internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
internal ControllerType SupportedStyleSets { get; set; } internal ControllerType SupportedStyleSets { get; set; }
public Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>> RumbleQueues = new Dictionary<PlayerIndex, ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>>();
public Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)> LastVibrationValues = new Dictionary<PlayerIndex, (HidVibrationValue, HidVibrationValue)>();
public NpadDevices(Switch device, bool active = true) : base(device, active) public NpadDevices(Switch device, bool active = true) : base(device, active)
{ {
_configuredTypes = new ControllerType[MaxControllers]; _configuredTypes = new ControllerType[MaxControllers];
@ -596,5 +608,49 @@ namespace Ryujinx.HLE.HOS.Services.Hid
WriteNewSixInputEntry(ref currentNpad.JoyLeftSixAxisSensor, ref newState); WriteNewSixInputEntry(ref currentNpad.JoyLeftSixAxisSensor, ref newState);
WriteNewSixInputEntry(ref currentNpad.JoyRightSixAxisSensor, ref newState); WriteNewSixInputEntry(ref currentNpad.JoyRightSixAxisSensor, ref newState);
} }
public void UpdateRumbleQueue(PlayerIndex index, Dictionary<byte, HidVibrationValue> dualVibrationValues)
{
if (RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> currentQueue))
{
if (!dualVibrationValues.TryGetValue(0, out HidVibrationValue leftVibrationValue))
{
leftVibrationValue = _neutralVibrationValue;
}
if (!dualVibrationValues.TryGetValue(1, out HidVibrationValue rightVibrationValue))
{
rightVibrationValue = _neutralVibrationValue;
}
if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue) || !leftVibrationValue.Equals(dualVibrationValue.Item1) || !rightVibrationValue.Equals(dualVibrationValue.Item2))
{
currentQueue.Enqueue((leftVibrationValue, rightVibrationValue));
LastVibrationValues[index] = (leftVibrationValue, rightVibrationValue);
}
}
}
public HidVibrationValue GetLastVibrationValue(PlayerIndex index, byte position)
{
if (!LastVibrationValues.TryGetValue(index, out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
{
return _neutralVibrationValue;
}
return (position == 0) ? dualVibrationValue.Item1 : dualVibrationValue.Item2;
}
public ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> GetRumbleQueue(PlayerIndex index)
{
if (!RumbleQueues.TryGetValue(index, out ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> rumbleQueue))
{
rumbleQueue = new ConcurrentQueue<(HidVibrationValue, HidVibrationValue)>();
_device.Hid.Npads.RumbleQueues[index] = rumbleQueue;
}
return rumbleQueue;
}
} }
} }

View file

@ -0,0 +1,10 @@
namespace Ryujinx.HLE.HOS.Services.Hid
{
public struct HidVibrationDeviceHandle
{
public byte DeviceType;
public byte PlayerId;
public byte Position;
public byte Reserved;
}
}

View file

@ -3,6 +3,7 @@
public enum HidVibrationDeviceType public enum HidVibrationDeviceType
{ {
None, None,
LinearResonantActuator LinearResonantActuator,
GcErm
} }
} }

View file

@ -1,4 +1,7 @@
namespace Ryujinx.HLE.HOS.Services.Hid using Ryujinx.HLE.HOS.Tamper;
using System;
namespace Ryujinx.HLE.HOS.Services.Hid
{ {
public struct HidVibrationValue public struct HidVibrationValue
{ {
@ -6,5 +9,17 @@
public float FrequencyLow; public float FrequencyLow;
public float AmplitudeHigh; public float AmplitudeHigh;
public float FrequencyHigh; public float FrequencyHigh;
public override bool Equals(object obj)
{
return obj is HidVibrationValue value &&
AmplitudeLow == value.AmplitudeLow &&
AmplitudeHigh == value.AmplitudeHigh;
}
public override int GetHashCode()
{
return HashCode.Combine(AmplitudeLow, AmplitudeHigh);
}
} }
} }

View file

@ -1,10 +1,13 @@
using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc; using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Common;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.HLE.HOS.Services.Hid.HidServer; using Ryujinx.HLE.HOS.Services.Hid.HidServer;
using Ryujinx.HLE.HOS.Services.Hid.Types; using Ryujinx.HLE.HOS.Services.Hid.Types;
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -37,7 +40,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private HidSensorFusionParameters _sensorFusionParams; private HidSensorFusionParameters _sensorFusionParams;
private HidAccelerometerParameters _accelerometerParams; private HidAccelerometerParameters _accelerometerParams;
private HidVibrationValue _vibrationValue;
public IHidServer(ServiceCtx context) : base(context.Device.System.HidServer) public IHidServer(ServiceCtx context) : base(context.Device.System.HidServer)
{ {
@ -52,7 +54,6 @@ namespace Ryujinx.HLE.HOS.Services.Hid
_sensorFusionParams = new HidSensorFusionParameters(); _sensorFusionParams = new HidSensorFusionParameters();
_accelerometerParams = new HidAccelerometerParameters(); _accelerometerParams = new HidAccelerometerParameters();
_vibrationValue = new HidVibrationValue();
// TODO: signal event at right place // TODO: signal event at right place
_xpadIdEvent.ReadableEvent.Signal(); _xpadIdEvent.ReadableEvent.Signal();
@ -1025,29 +1026,78 @@ namespace Ryujinx.HLE.HOS.Services.Hid
// GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo // GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo
public ResultCode GetVibrationDeviceInfo(ServiceCtx context) public ResultCode GetVibrationDeviceInfo(ServiceCtx context)
{ {
int vibrationDeviceHandle = context.RequestData.ReadInt32(); HidVibrationDeviceHandle deviceHandle = context.RequestData.ReadStruct<HidVibrationDeviceHandle>();
NpadStyleIndex deviceType = (NpadStyleIndex)deviceHandle.DeviceType;
NpadIdType npadIdType = (NpadIdType)deviceHandle.PlayerId;
HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue if (deviceType < NpadStyleIndex.System || deviceType >= NpadStyleIndex.FullKey)
{ {
DeviceType = HidVibrationDeviceType.None, if (npadIdType >= (NpadIdType.Player8 + 1) && npadIdType != NpadIdType.Handheld && npadIdType != NpadIdType.Unknown)
Position = HidVibrationDevicePosition.None {
}; return ResultCode.InvalidNpadIdType;
}
context.ResponseData.Write((int)deviceInfo.DeviceType); if (deviceHandle.Position > 1)
context.ResponseData.Write((int)deviceInfo.Position); {
return ResultCode.InvalidDeviceIndex;
}
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { vibrationDeviceHandle, deviceInfo.DeviceType, deviceInfo.Position }); HidVibrationDeviceType vibrationDeviceType = HidVibrationDeviceType.None;
return ResultCode.Success; if (Enum.IsDefined(typeof(NpadStyleIndex), deviceType))
{
vibrationDeviceType = HidVibrationDeviceType.LinearResonantActuator;
}
else if ((uint)deviceType == 8)
{
vibrationDeviceType = HidVibrationDeviceType.GcErm;
}
HidVibrationDevicePosition vibrationDevicePosition = HidVibrationDevicePosition.None;
if (vibrationDeviceType == HidVibrationDeviceType.LinearResonantActuator)
{
if (deviceHandle.Position == 0)
{
vibrationDevicePosition = HidVibrationDevicePosition.Left;
}
else if (deviceHandle.Position == 1)
{
vibrationDevicePosition = HidVibrationDevicePosition.Right;
}
else
{
throw new ArgumentOutOfRangeException(nameof(deviceHandle.Position));
}
}
HidVibrationDeviceValue deviceInfo = new HidVibrationDeviceValue
{
DeviceType = vibrationDeviceType,
Position = vibrationDevicePosition
};
context.ResponseData.WriteStruct(deviceInfo);
return ResultCode.Success;
}
return ResultCode.InvalidNpadDeviceType;
} }
[CommandHipc(201)] [CommandHipc(201)]
// SendVibrationValue(nn::hid::VibrationDeviceHandle, nn::hid::VibrationValue, nn::applet::AppletResourceUserId) // SendVibrationValue(nn::hid::VibrationDeviceHandle, nn::hid::VibrationValue, nn::applet::AppletResourceUserId)
public ResultCode SendVibrationValue(ServiceCtx context) public ResultCode SendVibrationValue(ServiceCtx context)
{ {
int vibrationDeviceHandle = context.RequestData.ReadInt32(); HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
{
DeviceType = context.RequestData.ReadByte(),
PlayerId = context.RequestData.ReadByte(),
Position = context.RequestData.ReadByte(),
Reserved = context.RequestData.ReadByte()
};
_vibrationValue = new HidVibrationValue HidVibrationValue vibrationValue = new HidVibrationValue
{ {
AmplitudeLow = context.RequestData.ReadSingle(), AmplitudeLow = context.RequestData.ReadSingle(),
FrequencyLow = context.RequestData.ReadSingle(), FrequencyLow = context.RequestData.ReadSingle(),
@ -1057,14 +1107,11 @@ namespace Ryujinx.HLE.HOS.Services.Hid
long appletResourceUserId = context.RequestData.ReadInt64(); long appletResourceUserId = context.RequestData.ReadInt64();
Logger.Debug?.PrintStub(LogClass.ServiceHid, new { Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
appletResourceUserId,
vibrationDeviceHandle, dualVibrationValues[deviceHandle.Position] = vibrationValue;
_vibrationValue.AmplitudeLow,
_vibrationValue.FrequencyLow, context.Device.Hid.Npads.UpdateRumbleQueue((PlayerIndex)deviceHandle.PlayerId, dualVibrationValues);
_vibrationValue.AmplitudeHigh,
_vibrationValue.FrequencyHigh
});
return ResultCode.Success; return ResultCode.Success;
} }
@ -1073,22 +1120,22 @@ namespace Ryujinx.HLE.HOS.Services.Hid
// GetActualVibrationValue(nn::hid::VibrationDeviceHandle, nn::applet::AppletResourceUserId) -> nn::hid::VibrationValue // GetActualVibrationValue(nn::hid::VibrationDeviceHandle, nn::applet::AppletResourceUserId) -> nn::hid::VibrationValue
public ResultCode GetActualVibrationValue(ServiceCtx context) public ResultCode GetActualVibrationValue(ServiceCtx context)
{ {
int vibrationDeviceHandle = context.RequestData.ReadInt32(); HidVibrationDeviceHandle deviceHandle = new HidVibrationDeviceHandle
{
DeviceType = context.RequestData.ReadByte(),
PlayerId = context.RequestData.ReadByte(),
Position = context.RequestData.ReadByte(),
Reserved = context.RequestData.ReadByte()
};
long appletResourceUserId = context.RequestData.ReadInt64(); long appletResourceUserId = context.RequestData.ReadInt64();
context.ResponseData.Write(_vibrationValue.AmplitudeLow); HidVibrationValue vibrationValue = context.Device.Hid.Npads.GetLastVibrationValue((PlayerIndex)deviceHandle.PlayerId, deviceHandle.Position);
context.ResponseData.Write(_vibrationValue.FrequencyLow);
context.ResponseData.Write(_vibrationValue.AmplitudeHigh);
context.ResponseData.Write(_vibrationValue.FrequencyHigh);
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { context.ResponseData.Write(vibrationValue.AmplitudeLow);
appletResourceUserId, context.ResponseData.Write(vibrationValue.FrequencyLow);
vibrationDeviceHandle, context.ResponseData.Write(vibrationValue.AmplitudeHigh);
_vibrationValue.AmplitudeLow, context.ResponseData.Write(vibrationValue.FrequencyHigh);
_vibrationValue.FrequencyLow,
_vibrationValue.AmplitudeHigh,
_vibrationValue.FrequencyHigh
});
return ResultCode.Success; return ResultCode.Success;
} }
@ -1138,13 +1185,31 @@ namespace Ryujinx.HLE.HOS.Services.Hid
context.Memory.Read(context.Request.PtrBuff[1].Position, vibrationValueBuffer); context.Memory.Read(context.Request.PtrBuff[1].Position, vibrationValueBuffer);
// TODO: Read all handles and values from buffer. Span<HidVibrationDeviceHandle> deviceHandles = MemoryMarshal.Cast<byte, HidVibrationDeviceHandle>(vibrationDeviceHandleBuffer);
Span<HidVibrationValue> vibrationValues = MemoryMarshal.Cast<byte, HidVibrationValue>(vibrationValueBuffer);
Logger.Debug?.PrintStub(LogClass.ServiceHid, new { if (!deviceHandles.IsEmpty && vibrationValues.Length == deviceHandles.Length)
appletResourceUserId, {
VibrationDeviceHandleBufferLength = vibrationDeviceHandleBuffer.Length, Dictionary<byte, HidVibrationValue> dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
VibrationValueBufferLength = vibrationValueBuffer.Length PlayerIndex currentIndex = (PlayerIndex)deviceHandles[0].PlayerId;
});
for (int deviceCounter = 0; deviceCounter < deviceHandles.Length; deviceCounter++)
{
PlayerIndex index = (PlayerIndex)deviceHandles[deviceCounter].PlayerId;
byte position = deviceHandles[deviceCounter].Position;
if (index != currentIndex || dualVibrationValues.Count == 2)
{
context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
dualVibrationValues = new Dictionary<byte, HidVibrationValue>();
}
dualVibrationValues[position] = vibrationValues[deviceCounter];
currentIndex = index;
}
context.Device.Hid.Npads.UpdateRumbleQueue(currentIndex, dualVibrationValues);
}
return ResultCode.Success; return ResultCode.Success;
} }

View file

@ -7,6 +7,9 @@
Success = 0, Success = 0,
InvalidNpadIdType = (710 << ErrorCodeShift) | ModuleId InvalidNpadDeviceType = (122 << ErrorCodeShift) | ModuleId,
InvalidNpadIdType = (123 << ErrorCodeShift) | ModuleId,
InvalidDeviceIndex = (124 << ErrorCodeShift) | ModuleId,
InvalidBufferSize = (131 << ErrorCodeShift) | ModuleId
} }
} }

View file

@ -0,0 +1,13 @@
namespace Ryujinx.HLE.HOS.Services.Hid
{
public enum NpadStyleIndex : byte
{
FullKey = 3,
Handheld = 4,
JoyDual = 5,
JoyLeft = 6,
JoyRight = 7,
SystemExt = 32,
System = 33
}
}

View file

@ -236,6 +236,12 @@ namespace Ryujinx.Headless.SDL2
EnableMotion = true, EnableMotion = true,
Sensitivity = 100, Sensitivity = 100,
GyroDeadzone = 1, GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false
} }
}; };
} }

View file

@ -1,5 +1,6 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
@ -151,7 +152,18 @@ namespace Ryujinx.Input.SDL2
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue); ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue); ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs); if (durationMs == uint.MaxValue)
{
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY);
}
else if (durationMs > SDL_HAPTIC_INFINITY)
{
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
}
else
{
SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs);
}
} }
} }

View file

@ -2,8 +2,11 @@
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -534,5 +537,29 @@ namespace Ryujinx.Input.HLE
{ {
Dispose(true); Dispose(true);
} }
public void UpdateRumble(ConcurrentQueue<(HidVibrationValue, HidVibrationValue)> queue)
{
if (queue.TryDequeue(out (HidVibrationValue, HidVibrationValue) dualVibrationValue))
{
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
{
HidVibrationValue leftVibrationValue = dualVibrationValue.Item1;
HidVibrationValue 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));
_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}, " +
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
$"--> ({low}, {high})");
}
}
}
} }
} }

View file

@ -4,6 +4,7 @@ using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -167,6 +168,7 @@ namespace Ryujinx.Input.HLE
(SixAxisInput, SixAxisInput) motionState = default; (SixAxisInput, SixAxisInput) motionState = default;
NpadController controller = _controllers[(int)inputConfig.PlayerIndex]; NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
Ryujinx.HLE.HOS.Services.Hid.PlayerIndex playerIndex = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
bool isJoyconPair = false; bool isJoyconPair = false;
@ -177,6 +179,7 @@ namespace Ryujinx.Input.HLE
controller.UpdateUserConfiguration(inputConfig); controller.UpdateUserConfiguration(inputConfig);
controller.Update(); controller.Update();
controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
inputState = controller.GetHLEInputState(); inputState = controller.GetHLEInputState();
@ -199,15 +202,15 @@ namespace Ryujinx.Input.HLE
motionState.Item1.Orientation = new float[9]; motionState.Item1.Orientation = new float[9];
} }
inputState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex; inputState.PlayerId = playerIndex;
motionState.Item1.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex; motionState.Item1.PlayerId = playerIndex;
hleInputStates.Add(inputState); hleInputStates.Add(inputState);
hleMotionStates.Add(motionState.Item1); hleMotionStates.Add(motionState.Item1);
if (isJoyconPair && !motionState.Item2.Equals(default)) if (isJoyconPair && !motionState.Item2.Equals(default))
{ {
motionState.Item2.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex; motionState.Item2.PlayerId = playerIndex;
hleMotionStates.Add(motionState.Item2); hleMotionStates.Add(motionState.Item2);
} }

View file

@ -66,7 +66,7 @@ namespace Ryujinx.Input
void SetConfiguration(InputConfig configuration); void SetConfiguration(InputConfig configuration);
/// <summary> /// <summary>
/// Starts a rumble effect on the gampead. /// Starts a rumble effect on the gamepad.
/// </summary> /// </summary>
/// <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>

View file

@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
/// <summary> /// <summary>
/// The current version of the file format /// The current version of the file format
/// </summary> /// </summary>
public const int CurrentVersion = 29; public const int CurrentVersion = 30;
public int Version { get; set; } public int Version { get; set; }

View file

@ -1,6 +1,7 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Configuration.System; using Ryujinx.Configuration.System;
@ -874,6 +875,26 @@ namespace Ryujinx.Configuration
configurationFileUpdated = true; configurationFileUpdated = true;
} }
if (configurationFileFormat.Version < 30)
{
Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 30.");
foreach(InputConfig config in configurationFileFormat.InputConfig)
{
if (config is StandardControllerInputConfig controllerConfig)
{
controllerConfig.Rumble = new RumbleConfigController
{
EnableRumble = false,
StrongRumble = 1f,
WeakRumble = 1f
};
}
}
configurationFileUpdated = true;
}
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog; Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale; Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom; Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;

View file

@ -34,6 +34,8 @@ namespace Ryujinx.Ui.Windows
private bool _isWaitingForInput; private bool _isWaitingForInput;
#pragma warning disable CS0649, IDE0044 #pragma warning disable CS0649, IDE0044
[GUI] Adjustment _controllerStrongRumble;
[GUI] Adjustment _controllerWeakRumble;
[GUI] Adjustment _controllerDeadzoneLeft; [GUI] Adjustment _controllerDeadzoneLeft;
[GUI] Adjustment _controllerDeadzoneRight; [GUI] Adjustment _controllerDeadzoneRight;
[GUI] Adjustment _controllerTriggerThreshold; [GUI] Adjustment _controllerTriggerThreshold;
@ -99,6 +101,8 @@ namespace Ryujinx.Ui.Windows
[GUI] ToggleButton _rSl; [GUI] ToggleButton _rSl;
[GUI] ToggleButton _rSr; [GUI] ToggleButton _rSr;
[GUI] Image _controllerImage; [GUI] Image _controllerImage;
[GUI] CheckButton _enableRumble;
[GUI] Box _rumbleBox;
#pragma warning restore CS0649, IDE0044 #pragma warning restore CS0649, IDE0044
private MainWindow _mainWindow; private MainWindow _mainWindow;
@ -314,6 +318,7 @@ namespace Ryujinx.Ui.Windows
_deadZoneRightBox.Hide(); _deadZoneRightBox.Hide();
_triggerThresholdBox.Hide(); _triggerThresholdBox.Hide();
_motionBox.Hide(); _motionBox.Hide();
_rumbleBox.Hide();
} }
else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller"))
{ {
@ -407,6 +412,8 @@ namespace Ryujinx.Ui.Windows
_zR.Label = "Unbound"; _zR.Label = "Unbound";
_rSl.Label = "Unbound"; _rSl.Label = "Unbound";
_rSr.Label = "Unbound"; _rSr.Label = "Unbound";
_controllerStrongRumble.Value = 1;
_controllerWeakRumble.Value = 1;
_controllerDeadzoneLeft.Value = 0; _controllerDeadzoneLeft.Value = 0;
_controllerDeadzoneRight.Value = 0; _controllerDeadzoneRight.Value = 0;
_controllerTriggerThreshold.Value = 0; _controllerTriggerThreshold.Value = 0;
@ -419,6 +426,7 @@ namespace Ryujinx.Ui.Windows
_gyroDeadzone.Value = 1; _gyroDeadzone.Value = 1;
_dsuServerHost.Buffer.Text = ""; _dsuServerHost.Buffer.Text = "";
_dsuServerPort.Buffer.Text = ""; _dsuServerPort.Buffer.Text = "";
_enableRumble.Active = false;
} }
private void SetValues(InputConfig config) private void SetValues(InputConfig config)
@ -497,6 +505,9 @@ namespace Ryujinx.Ui.Windows
_zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString();
_rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString();
_rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString();
_controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble;
_controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble;
_enableRumble.Active = controllerConfig.Rumble.EnableRumble;
_controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
_controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
_controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
@ -706,7 +717,13 @@ namespace Ryujinx.Ui.Windows
InvertStickY = _invertRStickY.Active, InvertStickY = _invertRStickY.Active,
StickButton = rStickButton, StickButton = rStickButton,
}, },
Motion = motionConfig Motion = motionConfig,
Rumble = new RumbleConfigController
{
StrongRumble = (float)_controllerStrongRumble.Value,
WeakRumble = (float)_controllerWeakRumble.Value,
EnableRumble = _enableRumble.Active
}
}; };
} }
@ -1045,6 +1062,12 @@ namespace Ryujinx.Ui.Windows
EnableMotion = true, EnableMotion = true,
Sensitivity = 100, Sensitivity = 100,
GyroDeadzone = 1, GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false
} }
}; };
} }

View file

@ -7,6 +7,20 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">4</property> <property name="page_increment">4</property>
</object> </object>
<object class="GtkAdjustment" id="_controllerStrongRumble">
<property name="lower">0.1</property>
<property name="upper">10</property>
<property name="value">1.0</property>
<property name="step_increment">0.1</property>
<property name="page_increment">1.0</property>
</object>
<object class="GtkAdjustment" id="_controllerWeakRumble">
<property name="lower">0.1</property>
<property name="upper">10</property>
<property name="value">1.0</property>
<property name="step_increment">0.1</property>
<property name="page_increment">1.0</property>
</object>
<object class="GtkAdjustment" id="_controllerDeadzoneLeft"> <object class="GtkAdjustment" id="_controllerDeadzoneLeft">
<property name="upper">1</property> <property name="upper">1</property>
<property name="value">0.050000000000000003</property> <property name="value">0.050000000000000003</property>
@ -1249,6 +1263,130 @@
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkBox" id="_rumbleBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Rumble</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="_enableRumble">
<property name="label" translatable="yes">Enable</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="_StrongMultiBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Strong rumble multiplier</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">_controllerStrongRumble</property>
<property name="round_digits">1</property>
<property name="digits">1</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="_WeakMultiBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Weak rumble multiplier</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">_controllerWeakRumble</property>
<property name="round_digits">1</property>
<property name="digits">1</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>