0
0
Fork 0

Add Motion controls (#1363)

* Add motion controls

Apply suggestions from code review

Co-authored-by: Ac_K <Acoustik666@gmail.com>

* cleanup

* add reference orientation and derive relative orientation from it

* cleanup

* remove unused variable and strange file

* Review_2.

* change GetInput to TryGetInput

* Review_3.

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: LDj3SNuD <dvitiello@gmail.com>
This commit is contained in:
emmauss 2020-09-29 21:32:42 +00:00 committed by GitHub
parent a6f8a0b01e
commit 26319d5ab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1780 additions and 63 deletions

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 = 14; public const int CurrentVersion = 15;
public int Version { get; set; } public int Version { get; set; }

View file

@ -483,12 +483,10 @@ namespace Ryujinx.Configuration
Ui.EnableCustomTheme.Value = false; Ui.EnableCustomTheme.Value = false;
Ui.CustomThemePath.Value = ""; Ui.CustomThemePath.Value = "";
Hid.EnableKeyboard.Value = false; Hid.EnableKeyboard.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys Hid.Hotkeys.Value = new KeyboardHotkeys
{ {
ToggleVsync = Key.Tab ToggleVsync = Key.Tab
}; };
Hid.InputConfig.Value = new List<InputConfig> Hid.InputConfig.Value = new List<InputConfig>
{ {
new KeyboardConfig new KeyboardConfig
@ -529,7 +527,15 @@ namespace Ryujinx.Configuration
ButtonZr = Key.O, ButtonZr = Key.O,
ButtonSl = Key.PageUp, ButtonSl = Key.PageUp,
ButtonSr = Key.PageDown ButtonSr = Key.PageDown
} },
EnableMotion = false,
MirrorInput = false,
Slot = 0,
AltSlot = 0,
Sensitivity = 100,
GyroDeadzone = 1,
DsuServerHost = "127.0.0.1",
DsuServerPort = 26760
} }
}; };
} }
@ -628,7 +634,15 @@ namespace Ryujinx.Configuration
ButtonZr = Key.O, ButtonZr = Key.O,
ButtonSl = Key.Unbound, ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound ButtonSr = Key.Unbound
} },
EnableMotion = false,
MirrorInput = false,
Slot = 0,
AltSlot = 0,
Sensitivity = 100,
GyroDeadzone = 1,
DsuServerHost = "127.0.0.1",
DsuServerPort = 26760
} }
}; };

View file

@ -16,5 +16,45 @@ namespace Ryujinx.Common.Configuration.Hid
/// Player's Index for the controller /// Player's Index for the controller
/// </summary> /// </summary>
public PlayerIndex PlayerIndex { get; set; } public PlayerIndex PlayerIndex { get; set; }
/// <summary>
/// Motion Controller Slot
/// </summary>
public int Slot { get; set; }
/// <summary>
/// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
/// </summary>
public int AltSlot { get; set; }
/// <summary>
/// Mirror motion input in Pair mode
/// </summary>
public bool MirrorInput { get; set; }
/// <summary>
/// Host address of the DSU Server
/// </summary>
public string DsuServerHost { get; set; }
/// <summary>
/// Port of the DSU Server
/// </summary>
public int DsuServerPort { get; set; }
/// <summary>
/// Gyro Sensitivity
/// </summary>
public int Sensitivity { get; set; }
/// <summary>
/// Gyro Deadzone
/// </summary>
public double GyroDeadzone { get; set; }
/// <summary>
/// Enable Motion Controls
/// </summary>
public bool EnableMotion { get; set; }
} }
} }

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory; using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using System;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
@ -317,6 +317,89 @@ namespace Ryujinx.HLE.HOS.Services.Hid
mainLayout.Entries[(int)mainLayout.Header.LatestEntry] = currentEntry; mainLayout.Entries[(int)mainLayout.Header.LatestEntry] = currentEntry;
} }
private static SixAxixLayoutsIndex ControllerTypeToSixAxisLayout(ControllerType controllerType)
=> controllerType switch
{
ControllerType.ProController => SixAxixLayoutsIndex.ProController,
ControllerType.Handheld => SixAxixLayoutsIndex.Handheld,
ControllerType.JoyconPair => SixAxixLayoutsIndex.JoyDualLeft,
ControllerType.JoyconLeft => SixAxixLayoutsIndex.JoyLeft,
ControllerType.JoyconRight => SixAxixLayoutsIndex.JoyRight,
ControllerType.Pokeball => SixAxixLayoutsIndex.Pokeball,
_ => SixAxixLayoutsIndex.SystemExternal
};
public void UpdateSixAxis(IList<SixAxisInput> states)
{
for (int i = 0; i < states.Count; ++i)
{
if (SetSixAxisState(states[i]))
{
i++;
SetSixAxisState(states[i], true);
}
}
}
private bool SetSixAxisState(SixAxisInput state, bool isRightPair = false)
{
if (state.PlayerId == PlayerIndex.Unknown)
{
return false;
}
ref ShMemNpad currentNpad = ref _device.Hid.SharedMemory.Npads[(int)state.PlayerId];
if (currentNpad.Header.Type == ControllerType.None)
{
return false;
}
HidVector accel = new HidVector()
{
X = state.Accelerometer.X,
Y = state.Accelerometer.Y,
Z = state.Accelerometer.Z
};
HidVector gyro = new HidVector()
{
X = state.Gyroscope.X,
Y = state.Gyroscope.Y,
Z = state.Gyroscope.Z
};
HidVector rotation = new HidVector()
{
X = state.Rotation.X,
Y = state.Rotation.Y,
Z = state.Rotation.Z
};
ref NpadSixAxis currentLayout = ref currentNpad.Sixaxis[(int)ControllerTypeToSixAxisLayout(currentNpad.Header.Type) + (isRightPair ? 1 : 0)];
ref SixAxisState currentEntry = ref currentLayout.Entries[(int)currentLayout.Header.LatestEntry];
int previousEntryIndex = (int)(currentLayout.Header.LatestEntry == 0 ?
currentLayout.Header.MaxEntryIndex : currentLayout.Header.LatestEntry - 1);
ref SixAxisState previousEntry = ref currentLayout.Entries[previousEntryIndex];
currentEntry.Accelerometer = accel;
currentEntry.Gyroscope = gyro;
currentEntry.Rotations = rotation;
unsafe
{
for (int i = 0; i < 9; i++)
{
currentEntry.Orientation[i] = state.Orientation[i];
}
}
return currentNpad.Header.Type == ControllerType.JoyconPair && !isRightPair;
}
private void UpdateAllEntries() private void UpdateAllEntries()
{ {
ref Array10<ShMemNpad> controllers = ref _device.Hid.SharedMemory.Npads; ref Array10<ShMemNpad> controllers = ref _device.Hid.SharedMemory.Npads;
@ -359,6 +442,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
break; break;
} }
} }
ref Array6<NpadSixAxis> sixaxis = ref controllers[i].Sixaxis;
for (int l = 0; l < sixaxis.Length; ++l)
{
ref NpadSixAxis currentLayout = ref sixaxis[l];
int currentIndex = UpdateEntriesHeader(ref currentLayout.Header, out int previousIndex);
ref SixAxisState currentEntry = ref currentLayout.Entries[currentIndex];
SixAxisState previousEntry = currentLayout.Entries[previousIndex];
currentEntry.SampleTimestamp = previousEntry.SampleTimestamp + 1;
currentEntry.SampleTimestamp2 = previousEntry.SampleTimestamp2 + 1;
currentEntry._unknown2 = 1;
}
} }
} }
} }

View file

@ -0,0 +1,13 @@
using System.Numerics;
namespace Ryujinx.HLE.HOS.Services.Hid
{
public struct SixAxisInput
{
public PlayerIndex PlayerId;
public Vector3 Accelerometer;
public Vector3 Gyroscope;
public Vector3 Rotation;
public float[] Orientation;
}
}

View file

@ -0,0 +1,14 @@
namespace Ryujinx.HLE.HOS.Services.Hid
{
enum SixAxixLayoutsIndex : int
{
ProController = 0,
Handheld = 1,
JoyDualLeft = 2,
JoyDualRight = 3,
JoyLeft = 4,
JoyRight = 5,
Pokeball = 6,
SystemExternal = 7
}
}

View file

@ -7,8 +7,8 @@ namespace Ryujinx.HLE.HOS.Services.Hid
public ulong SampleTimestamp2; public ulong SampleTimestamp2;
public HidVector Accelerometer; public HidVector Accelerometer;
public HidVector Gyroscope; public HidVector Gyroscope;
HidVector unknownSensor; public HidVector Rotations;
public fixed float Orientation[9]; public fixed float Orientation[9];
ulong _unknown2; public ulong _unknown2;
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"version": 14, "version": 15,
"res_scale": 1, "res_scale": 1,
"res_scale_custom": 1, "res_scale_custom": 1,
"max_anisotropy": -1, "max_anisotropy": -1,
@ -86,7 +86,14 @@
"button_zr": "O", "button_zr": "O",
"button_sl": "Unbound", "button_sl": "Unbound",
"button_sr": "Unbound" "button_sr": "Unbound"
} },
"slot": 0,
"alt_slot": 0,
"mirror_input": false,
"dsu_server_host": "127.0.0.1",
"dsu_server_port": 26760,
"sensitivity": 100,
"enable_motion": false
} }
], ],
"controller_config": [] "controller_config": []

393
Ryujinx/Motion/Client.cs Normal file
View file

@ -0,0 +1,393 @@
using Force.Crc32;
using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Threading.Tasks;
namespace Ryujinx.Motion
{
public class Client : IDisposable
{
public const uint Magic = 0x43555344; // DSUC
public const ushort Version = 1001;
private bool _active;
private readonly Dictionary<int, IPEndPoint> _hosts;
private readonly Dictionary<int, Dictionary<int, MotionInput>> _motionData;
private readonly Dictionary<int, UdpClient> _clients;
private bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length];
public Client()
{
_hosts = new Dictionary<int, IPEndPoint>();
_motionData = new Dictionary<int, Dictionary<int, MotionInput>>();
_clients = new Dictionary<int, UdpClient>();
CloseClients();
}
public void CloseClients()
{
_active = false;
lock (_clients)
{
foreach (var client in _clients)
{
try
{
client.Value?.Dispose();
}
#pragma warning disable CS0168
catch (SocketException ex)
#pragma warning restore CS0168
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error code {ex.ErrorCode}");
}
}
_hosts.Clear();
_clients.Clear();
_motionData.Clear();
}
}
public void RegisterClient(int player, string host, int port)
{
if (_clients.ContainsKey(player))
{
return;
}
try
{
lock (_clients)
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port);
UdpClient client = new UdpClient(host, port);
_clients.Add(player, client);
_hosts.Add(player, endPoint);
_active = true;
Task.Run(() =>
{
ReceiveLoop(player);
});
}
}
catch (FormatException fex)
{
if (!_clientErrorStatus[player])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error {fex.Message}");
_clientErrorStatus[player] = true;
}
}
catch (SocketException ex)
{
if (!_clientErrorStatus[player])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error code {ex.ErrorCode}");
_clientErrorStatus[player] = true;
}
}
}
public bool TryGetData(int player, int slot, out MotionInput input)
{
lock (_motionData)
{
if (_motionData.ContainsKey(player))
{
input = _motionData[player][slot];
return true;
}
}
input = null;
return false;
}
private void Send(byte[] data, int clientId)
{
if (_clients.TryGetValue(clientId, out UdpClient _client))
{
if (_client != null && _client.Client != null && _client.Client.Connected)
{
try
{
_client?.Send(data, data.Length);
}
catch (SocketException ex)
{
if (!_clientErrorStatus[clientId])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error code {ex.ErrorCode}");
}
_clientErrorStatus[clientId] = true;
_clients.Remove(clientId);
_hosts.Remove(clientId);
_client?.Dispose();
}
}
}
}
private byte[] Receive(int clientId)
{
if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint))
{
if (_clients.TryGetValue(clientId, out UdpClient _client))
{
if (_client != null && _client.Client != null)
{
if (_client.Client.Connected)
{
try
{
var result = _client?.Receive(ref endPoint);
if (result.Length > 0)
{
_clientErrorStatus[clientId] = false;
}
return result;
}
catch (SocketException ex)
{
if (!_clientErrorStatus[clientId])
{
Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error code {ex.ErrorCode}");
}
_clientErrorStatus[clientId] = true;
_clients.Remove(clientId);
_hosts.Remove(clientId);
_client?.Dispose();
}
}
}
}
}
return new byte[0];
}
public void ReceiveLoop(int clientId)
{
while (_active)
{
byte[] data = Receive(clientId);
if (data.Length == 0)
{
continue;
}
#pragma warning disable CS4014
HandleResponse(data, clientId);
#pragma warning restore CS4014
}
}
#pragma warning disable CS1998
public async Task HandleResponse(byte[] data, int clientId)
#pragma warning restore CS1998
{
MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4));
data = data.AsSpan().Slice(16).ToArray();
using (MemoryStream mem = new MemoryStream(data))
{
using (BinaryReader reader = new BinaryReader(mem))
{
switch (type)
{
case MessageType.Protocol:
break;
case MessageType.Info:
ControllerInfoResponse contollerInfo = reader.ReadStruct<ControllerInfoResponse>();
break;
case MessageType.Data:
ControllerDataResponse inputData = reader.ReadStruct<ControllerDataResponse>();
Vector3 accelerometer = new Vector3()
{
X = -inputData.AccelerometerX,
Y = inputData.AccelerometerZ,
Z = -inputData.AccelerometerY
};
Vector3 gyroscrope = new Vector3()
{
X = inputData.GyroscopePitch,
Y = inputData.GyroscopeRoll,
Z = -inputData.GyroscopeYaw
};
ulong timestamp = inputData.MotionTimestamp;
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)clientId);
lock (_motionData)
{
int slot = inputData.Shared.Slot;
if (_motionData.ContainsKey(clientId))
{
if (_motionData[clientId].ContainsKey(slot))
{
var previousData = _motionData[clientId][slot];
previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
}
else
{
MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
_motionData[clientId].Add(slot, input);
}
}
else
{
MotionInput input = new MotionInput();
input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
_motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
}
}
break;
}
}
}
}
public void RequestInfo(int clientId, int slot)
{
if (!_active)
{
return;
}
Header header = GenerateHeader(clientId);
using (MemoryStream mem = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(mem))
{
writer.WriteStruct(header);
ControllerInfoRequest request = new ControllerInfoRequest()
{
Type = MessageType.Info,
PortsCount = 4
};
request.PortIndices[0] = (byte)slot;
writer.WriteStruct(request);
header.Length = (ushort)(mem.Length - 16);
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(mem.ToArray());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
byte[] data = mem.ToArray();
Send(data, clientId);
}
}
}
public unsafe void RequestData(int clientId, int slot)
{
if (!_active)
{
return;
}
Header header = GenerateHeader(clientId);
using (MemoryStream mem = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(mem))
{
writer.WriteStruct(header);
ControllerDataRequest request = new ControllerDataRequest()
{
Type = MessageType.Data,
Slot = (byte)slot,
SubscriberType = SubscriberType.Slot
};
writer.WriteStruct(request);
header.Length = (ushort)(mem.Length - 16);
writer.Seek(6, SeekOrigin.Begin);
writer.Write(header.Length);
header.Crc32 = Crc32Algorithm.Compute(mem.ToArray());
writer.Seek(8, SeekOrigin.Begin);
writer.Write(header.Crc32);
byte[] data = mem.ToArray();
Send(data, clientId);
}
}
}
private Header GenerateHeader(int clientId)
{
Header header = new Header()
{
ID = (uint)clientId,
MagicString = Magic,
Version = Version,
Length = 0,
Crc32 = 0
};
return header;
}
public void Dispose()
{
_active = false;
CloseClients();
}
}
}

View file

@ -0,0 +1,83 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Configuration;
using System;
using System.Numerics;
namespace Ryujinx.Motion
{
public class MotionDevice
{
public Vector3 Gyroscope { get; private set; }
public Vector3 Accelerometer { get; private set; }
public Vector3 Rotation { get; private set; }
public float[] Orientation { get; private set; }
private Client _motionSource;
public MotionDevice(Client motionSource)
{
_motionSource = motionSource;
}
public void RegisterController(PlayerIndex player)
{
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == player);
if (config != null && config.EnableMotion)
{
string host = config.DsuServerHost;
int port = config.DsuServerPort;
_motionSource.RegisterClient((int)player, host, port);
_motionSource.RequestData((int)player, config.Slot);
if (config.ControllerType == ControllerType.JoyconPair && !config.MirrorInput)
{
_motionSource.RequestData((int)player, config.AltSlot);
}
}
}
public void Poll(PlayerIndex player, int slot)
{
InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == player);
Orientation = new float[9];
if (!config.EnableMotion || !_motionSource.TryGetData((int)player, slot, out MotionInput input))
{
Accelerometer = new Vector3();
Gyroscope = new Vector3();
return;
}
Gyroscope = Truncate(input.Gyroscrope * 0.0027f, 3);
Accelerometer = Truncate(input.Accelerometer, 3);
Rotation = Truncate(input.Rotation * 0.0027f, 3);
Matrix4x4 orientation = input.GetOrientation();
Orientation[0] = Math.Clamp(orientation.M11, -1f, 1f);
Orientation[1] = Math.Clamp(orientation.M12, -1f, 1f);
Orientation[2] = Math.Clamp(orientation.M13, -1f, 1f);
Orientation[3] = Math.Clamp(orientation.M21, -1f, 1f);
Orientation[4] = Math.Clamp(orientation.M22, -1f, 1f);
Orientation[5] = Math.Clamp(orientation.M23, -1f, 1f);
Orientation[6] = Math.Clamp(orientation.M31, -1f, 1f);
Orientation[7] = Math.Clamp(orientation.M32, -1f, 1f);
Orientation[8] = Math.Clamp(orientation.M33, -1f, 1f);
}
private static Vector3 Truncate(Vector3 value, int decimals)
{
float power = MathF.Pow(10, decimals);
value.X = float.IsNegative(value.X) ? MathF.Ceiling(value.X * power) / power : MathF.Floor(value.X * power) / power;
value.Y = float.IsNegative(value.Y) ? MathF.Ceiling(value.Y * power) / power : MathF.Floor(value.Y * power) / power;
value.Z = float.IsNegative(value.Z) ? MathF.Ceiling(value.Z * power) / power : MathF.Floor(value.Z * power) / power;
return value;
}
}
}

View file

@ -0,0 +1,85 @@
using System;
using System.Numerics;
namespace Ryujinx.Motion
{
public class MotionInput
{
public ulong TimeStamp { get; set; }
public Vector3 Accelerometer { get; set; }
public Vector3 Gyroscrope { get; set; }
public Vector3 Rotation { get; set; }
private readonly MotionSensorFilter _filter;
private int _calibrationFrame = 0;
public MotionInput()
{
TimeStamp = 0;
Accelerometer = new Vector3();
Gyroscrope = new Vector3();
Rotation = new Vector3();
// TODO: RE the correct filter.
_filter = new MotionSensorFilter(0f);
}
public void Update(Vector3 accel, Vector3 gyro, ulong timestamp, int sensitivity, float deadzone)
{
if (TimeStamp != 0)
{
if (gyro.Length() <= 1f && accel.Length() >= 0.8f && accel.Z >= 0.8f)
{
_calibrationFrame++;
if (_calibrationFrame >= 90)
{
gyro = Vector3.Zero;
Rotation = Vector3.Zero;
_filter.Reset();
_calibrationFrame = 0;
}
}
else
{
_calibrationFrame = 0;
}
Accelerometer = -accel;
if (gyro.Length() < deadzone)
{
gyro = Vector3.Zero;
}
gyro *= (sensitivity / 100f);
Gyroscrope = gyro;
float deltaTime = MathF.Abs((long)(timestamp - TimeStamp) / 1000000f);
Vector3 deltaGyro = gyro * deltaTime;
Rotation += deltaGyro;
_filter.SamplePeriod = deltaTime;
_filter.Update(accel, DegreeToRad(gyro));
}
TimeStamp = timestamp;
}
public Matrix4x4 GetOrientation()
{
return Matrix4x4.CreateFromQuaternion(_filter.Quaternion);
}
private static Vector3 DegreeToRad(Vector3 degree)
{
return degree * (MathF.PI / 180);
}
}
}

View file

@ -0,0 +1,166 @@
using System.Numerics;
namespace Ryujinx.Motion
{
// MahonyAHRS class. Madgwick's implementation of Mayhony's AHRS algorithm.
// See: https://x-io.co.uk/open-source-imu-and-ahrs-algorithms/
// Based on: https://github.com/xioTechnologies/Open-Source-AHRS-With-x-IMU/blob/master/x-IMU%20IMU%20and%20AHRS%20Algorithms/x-IMU%20IMU%20and%20AHRS%20Algorithms/AHRS/MahonyAHRS.cs
public class MotionSensorFilter
{
/// <summary>
/// Sample rate coefficient.
/// </summary>
public const float SampleRateCoefficient = 0.45f;
/// <summary>
/// Gets or sets the sample period.
/// </summary>
public float SamplePeriod { get; set; }
/// <summary>
/// Gets or sets the algorithm proportional gain.
/// </summary>
public float Kp { get; set; }
/// <summary>
/// Gets or sets the algorithm integral gain.
/// </summary>
public float Ki { get; set; }
/// <summary>
/// Gets the Quaternion output.
/// </summary>
public Quaternion Quaternion { get; private set; }
/// <summary>
/// Integral error.
/// </summary>
private Vector3 _intergralError;
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
/// <param name="kp">
/// Algorithm proportional gain.
/// </param>
public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
/// </summary>
/// <param name="samplePeriod">
/// Sample period.
/// </param>
/// <param name="kp">
/// Algorithm proportional gain.
/// </param>
/// <param name="ki">
/// Algorithm integral gain.
/// </param>
public MotionSensorFilter(float samplePeriod, float kp, float ki)
{
SamplePeriod = samplePeriod;
Kp = kp;
Ki = ki;
Reset();
_intergralError = new Vector3();
}
/// <summary>
/// Algorithm IMU update method. Requires only gyroscope and accelerometer data.
/// </summary>
/// <param name="accel">
/// Accelerometer measurement in any calibrated units.
/// </param>
/// <param name="gyro">
/// Gyroscope measurement in radians.
/// </param>
public void Update(Vector3 accel, Vector3 gyro)
{
// Normalise accelerometer measurement.
float norm = 1f / accel.Length();
if (!float.IsFinite(norm))
{
return;
}
accel *= norm;
float q2 = Quaternion.X;
float q3 = Quaternion.Y;
float q4 = Quaternion.Z;
float q1 = Quaternion.W;
// Estimated direction of gravity.
Vector3 gravity = new Vector3()
{
X = 2f * (q2 * q4 - q1 * q3),
Y = 2f * (q1 * q2 + q3 * q4),
Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4
};
// Error is cross product between estimated direction and measured direction of gravity.
Vector3 error = new Vector3()
{
X = accel.Y * gravity.Z - accel.Z * gravity.Y,
Y = accel.Z * gravity.X - accel.X * gravity.Z,
Z = accel.X * gravity.Y - accel.Y * gravity.X
};
if (Ki > 0f)
{
_intergralError += error; // Accumulate integral error.
}
else
{
_intergralError = Vector3.Zero; // Prevent integral wind up.
}
// Apply feedback terms.
gyro += (Kp * error) + (Ki * _intergralError);
// Integrate rate of change of quaternion.
Vector3 delta = new Vector3(q2, q3, q4);
q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod);
q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod);
q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod);
q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod);
// Normalise quaternion.
Quaternion quaternion = new Quaternion(q2, q3, q4, q1);
norm = 1f / quaternion.Length();
if (!float.IsFinite(norm))
{
return;
}
Quaternion = quaternion * norm;
}
public void Reset()
{
Quaternion = Quaternion.Identity;
}
}
}

View file

@ -0,0 +1,50 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct ControllerDataRequest
{
public MessageType Type;
public SubscriberType SubscriberType;
public byte Slot;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] MacAddress;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerDataResponse
{
public SharedResponse Shared;
public byte Connected;
public uint PacketID;
public byte ExtraButtons;
public byte MainButtons;
public ushort PSExtraInput;
public ushort LeftStickXY;
public ushort RightStickXY;
public uint DPadAnalog;
public ulong MainButtonsAnalog;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Touch1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] Touch2;
public ulong MotionTimestamp;
public float AccelerometerX;
public float AccelerometerY;
public float AccelerometerZ;
public float GyroscopePitch;
public float GyroscopeYaw;
public float GyroscopeRoll;
}
enum SubscriberType : byte
{
All = 0,
Slot,
Mac
}
}

View file

@ -0,0 +1,21 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerInfoResponse
{
public SharedResponse Shared;
private byte _zero;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ControllerInfoRequest
{
public MessageType Type;
public int PortsCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] PortIndices;
}
}

View file

@ -0,0 +1,14 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header
{
public uint MagicString;
public ushort Version;
public ushort Length;
public uint Crc32;
public uint ID;
}
}

View file

@ -0,0 +1,9 @@
namespace Ryujinx.Motion
{
public enum MessageType : uint
{
Protocol = 0x100000,
Info,
Data
}
}

View file

@ -0,0 +1,51 @@
using System.Runtime.InteropServices;
namespace Ryujinx.Motion
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SharedResponse
{
public MessageType Type;
public byte Slot;
public SlotState State;
public DeviceModelType ModelType;
public ConnectionType ConnectionType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] MacAddress;
public BatteryStatus BatteryStatus;
}
public enum SlotState : byte
{
Disconnected = 0,
Reserved,
Connected
}
public enum DeviceModelType : byte
{
None = 0,
PartialGyro,
FullGyro
}
public enum ConnectionType : byte
{
None = 0,
USB,
Bluetooth
}
public enum BatteryStatus : byte
{
NA = 0,
Dying,
Low,
Medium,
High,
Full,
Charging,
Charged
}
}

View file

@ -71,6 +71,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.150" /> <PackageReference Include="DiscordRichPresence" Version="1.0.150" />
<PackageReference Include="GLWidget" Version="1.0.2" /> <PackageReference Include="GLWidget" Version="1.0.2" />
<PackageReference Include="GtkSharp" Version="3.22.25.56" /> <PackageReference Include="GtkSharp" Version="3.22.25.56" />

View file

@ -28,10 +28,19 @@ namespace Ryujinx.Ui
[GUI] Adjustment _controllerDeadzoneLeft; [GUI] Adjustment _controllerDeadzoneLeft;
[GUI] Adjustment _controllerDeadzoneRight; [GUI] Adjustment _controllerDeadzoneRight;
[GUI] Adjustment _controllerTriggerThreshold; [GUI] Adjustment _controllerTriggerThreshold;
[GUI] Adjustment _slotNumber;
[GUI] Adjustment _altSlotNumber;
[GUI] Adjustment _sensitivity;
[GUI] Adjustment _gyroDeadzone;
[GUI] CheckButton _enableMotion;
[GUI] CheckButton _mirrorInput;
[GUI] Entry _dsuServerHost;
[GUI] Entry _dsuServerPort;
[GUI] ComboBoxText _inputDevice; [GUI] ComboBoxText _inputDevice;
[GUI] ComboBoxText _profile; [GUI] ComboBoxText _profile;
[GUI] ToggleButton _refreshInputDevicesButton; [GUI] ToggleButton _refreshInputDevicesButton;
[GUI] Box _settingsBox; [GUI] Box _settingsBox;
[GUI] Box _altBox;
[GUI] Grid _leftStickKeyboard; [GUI] Grid _leftStickKeyboard;
[GUI] Grid _leftStickController; [GUI] Grid _leftStickController;
[GUI] Box _deadZoneLeftBox; [GUI] Box _deadZoneLeftBox;
@ -225,6 +234,7 @@ namespace Ryujinx.Ui
{ {
_leftSideTriggerBox.Hide(); _leftSideTriggerBox.Hide();
_rightSideTriggerBox.Hide(); _rightSideTriggerBox.Hide();
_altBox.Hide();
switch (_controllerType.ActiveId) switch (_controllerType.ActiveId)
{ {
@ -234,6 +244,9 @@ namespace Ryujinx.Ui
case "JoyconRight": case "JoyconRight":
_rightSideTriggerBox.Show(); _rightSideTriggerBox.Show();
break; break;
case "JoyconPair":
_altBox.Show();
break;
} }
switch (_controllerType.ActiveId) switch (_controllerType.ActiveId)
@ -290,6 +303,14 @@ namespace Ryujinx.Ui
_controllerDeadzoneLeft.Value = 0; _controllerDeadzoneLeft.Value = 0;
_controllerDeadzoneRight.Value = 0; _controllerDeadzoneRight.Value = 0;
_controllerTriggerThreshold.Value = 0; _controllerTriggerThreshold.Value = 0;
_mirrorInput.Active = false;
_enableMotion.Active = false;
_slotNumber.Value = 0;
_altSlotNumber.Value = 0;
_sensitivity.Value = 100;
_gyroDeadzone.Value = 1;
_dsuServerHost.Buffer.Text = "";
_dsuServerPort.Buffer.Text = "";
} }
private void SetValues(InputConfig config) private void SetValues(InputConfig config)
@ -304,34 +325,42 @@ namespace Ryujinx.Ui
: ControllerType.ProController.ToString()); : ControllerType.ProController.ToString());
} }
_lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString(); _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString();
_lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString(); _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString();
_lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString(); _lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString();
_lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString(); _lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString();
_lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString(); _lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString();
_dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString(); _dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString();
_dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString(); _dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString();
_dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString(); _dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString();
_dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString(); _dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString();
_minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString();
_l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString();
_zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString();
_lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString();
_lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString();
_rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString(); _rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString();
_rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString(); _rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString();
_rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString(); _rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString();
_rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString(); _rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString();
_rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString(); _rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString();
_a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString();
_b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString();
_x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString();
_y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString();
_plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString();
_r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString();
_zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString();
_rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString();
_rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString();
_slotNumber.Value = keyboardConfig.Slot;
_altSlotNumber.Value = keyboardConfig.AltSlot;
_sensitivity.Value = keyboardConfig.Sensitivity;
_gyroDeadzone.Value = keyboardConfig.GyroDeadzone;
_enableMotion.Active = keyboardConfig.EnableMotion;
_mirrorInput.Active = keyboardConfig.MirrorInput;
_dsuServerHost.Buffer.Text = keyboardConfig.DsuServerHost;
_dsuServerPort.Buffer.Text = keyboardConfig.DsuServerPort.ToString();
break; break;
case ControllerConfig controllerConfig: case ControllerConfig controllerConfig:
if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString()))
@ -372,6 +401,14 @@ namespace Ryujinx.Ui
_controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
_controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
_controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
_slotNumber.Value = controllerConfig.Slot;
_altSlotNumber.Value = controllerConfig.AltSlot;
_sensitivity.Value = controllerConfig.Sensitivity;
_gyroDeadzone.Value = controllerConfig.GyroDeadzone;
_enableMotion.Active = controllerConfig.EnableMotion;
_mirrorInput.Active = controllerConfig.MirrorInput;
_dsuServerHost.Buffer.Text = controllerConfig.DsuServerHost;
_dsuServerPort.Buffer.Text = controllerConfig.DsuServerPort.ToString();
break; break;
} }
} }
@ -448,7 +485,15 @@ namespace Ryujinx.Ui
ButtonZr = rButtonZr, ButtonZr = rButtonZr,
ButtonSl = rButtonSl, ButtonSl = rButtonSl,
ButtonSr = rButtonSr ButtonSr = rButtonSr
} },
EnableMotion = _enableMotion.Active,
MirrorInput = _mirrorInput.Active,
Slot = (int)_slotNumber.Value,
AltSlot = (int)_slotNumber.Value,
Sensitivity = (int)_sensitivity.Value,
GyroDeadzone = _gyroDeadzone.Value,
DsuServerHost = _dsuServerHost.Buffer.Text,
DsuServerPort = int.Parse(_dsuServerPort.Buffer.Text)
}; };
} }
@ -521,7 +566,15 @@ namespace Ryujinx.Ui
ButtonZr = rButtonZr, ButtonZr = rButtonZr,
ButtonSl = rButtonSl, ButtonSl = rButtonSl,
ButtonSr = rButtonSr ButtonSr = rButtonSr
} },
EnableMotion = _enableMotion.Active,
MirrorInput = _mirrorInput.Active,
Slot = (int)_slotNumber.Value,
AltSlot = (int)_slotNumber.Value,
Sensitivity = (int)_sensitivity.Value,
GyroDeadzone = _gyroDeadzone.Value,
DsuServerHost = _dsuServerHost.Buffer.Text,
DsuServerPort = int.Parse(_dsuServerPort.Buffer.Text)
}; };
} }
@ -779,7 +832,15 @@ namespace Ryujinx.Ui
ButtonZr = Key.O, ButtonZr = Key.O,
ButtonSl = Key.Unbound, ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound ButtonSr = Key.Unbound
} },
EnableMotion = false,
MirrorInput = false,
Slot = 0,
AltSlot = 0,
Sensitivity = 100,
GyroDeadzone = 1,
DsuServerHost = "127.0.0.1",
DsuServerPort = 26760
}; };
} }
else if (_inputDevice.ActiveId.StartsWith("controller")) else if (_inputDevice.ActiveId.StartsWith("controller"))
@ -824,7 +885,15 @@ namespace Ryujinx.Ui
ButtonSr = ControllerInputId.Unbound, ButtonSr = ControllerInputId.Unbound,
InvertStickX = false, InvertStickX = false,
InvertStickY = false InvertStickY = false
} },
EnableMotion = false,
MirrorInput = false,
Slot = 0,
AltSlot = 0,
Sensitivity = 100,
GyroDeadzone = 1,
DsuServerHost = "127.0.0.1",
DsuServerPort = 26760
}; };
} }
} }

View file

@ -1,24 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 --> <!-- Generated with glade 3.36.0 -->
<interface> <interface>
<requires lib="gtk+" version="3.20"/> <requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="_altSlotNumber">
<property name="upper">4</property>
<property name="step_increment">1</property>
<property name="page_increment">4</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.05</property>
<property name="step_increment">0.01</property> <property name="step_increment">0.01</property>
<property name="page_increment">0.10000000000000001</property> <property name="page_increment">0.1</property>
</object> </object>
<object class="GtkAdjustment" id="_controllerDeadzoneRight"> <object class="GtkAdjustment" id="_controllerDeadzoneRight">
<property name="upper">1</property> <property name="upper">1</property>
<property name="value">0.050000000000000003</property> <property name="value">0.05</property>
<property name="step_increment">0.01</property> <property name="step_increment">0.01</property>
<property name="page_increment">0.10000000000000001</property> <property name="page_increment">0.1</property>
</object> </object>
<object class="GtkAdjustment" id="_controllerTriggerThreshold"> <object class="GtkAdjustment" id="_controllerTriggerThreshold">
<property name="upper">1</property> <property name="upper">1</property>
<property name="value">0.5</property> <property name="value">0.5</property>
<property name="step_increment">0.01</property> <property name="step_increment">0.01</property>
<property name="page_increment">0.10000000000000001</property> <property name="page_increment">0.1</property>
</object>
<object class="GtkAdjustment" id="_gyroDeadzone">
<property name="upper">100</property>
<property name="value">0.01</property>
<property name="step_increment">0.01</property>
<property name="page_increment">0.1</property>
<property name="page_size">0.1</property>
</object>
<object class="GtkAdjustment" id="_sensitivity">
<property name="upper">1000</property>
<property name="value">100</property>
<property name="step_increment">1</property>
<property name="page_increment">4</property>
</object>
<object class="GtkAdjustment" id="_slotNumber">
<property name="upper">4</property>
<property name="step_increment">1</property>
<property name="page_increment">4</property>
</object> </object>
<object class="GtkWindow" id="_controllerWin"> <object class="GtkWindow" id="_controllerWin">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@ -27,9 +50,6 @@
<property name="window_position">center</property> <property name="window_position">center</property>
<property name="default_width">1100</property> <property name="default_width">1100</property>
<property name="default_height">600</property> <property name="default_height">600</property>
<child>
<placeholder/>
</child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="visible">True</property> <property name="visible">True</property>
@ -1617,12 +1637,301 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel"> <object class="GtkBox" id="MotionBox">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Motion</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="_enableMotion">
<property name="label" translatable="yes">Enable Motion Controls</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">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">17</property>
<property name="label" translatable="yes">Controller Slot</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="_slot">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">10</property>
<property name="adjustment">_slotNumber</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="expand">False</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="padding">5</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<property name="label" translatable="yes">Gyro Sensitivity %</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">0</property>
<property name="adjustment">_sensitivity</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="expand">False</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="padding">5</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="_altBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkCheckButton" id="_mirrorInput">
<property name="label" translatable="yes">Mirror Input</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">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Right JoyCon Slot</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="_slotRight">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">0</property>
<property name="climb_rate">1</property>
<property name="snap_to_ticks">True</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="expand">False</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="padding">5</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">30</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Server Host</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_dsuServerHost">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</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="padding">5</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">30</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Server Port</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="_dsuServerPort">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</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="padding">5</property>
<property name="position">6</property>
</packing>
</child>
<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">Gyro Deadzone</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkScale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="adjustment">_gyroDeadzone</property>
<property name="round_digits">2</property>
<property name="digits">2</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">4</property>
</packing> </packing>
@ -1721,5 +2030,8 @@
</child> </child>
</object> </object>
</child> </child>
<child type="titlebar">
<placeholder/>
</child>
</object> </object>
</interface> </interface>

View file

@ -13,6 +13,7 @@ using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
using Ryujinx.Motion;
namespace Ryujinx.Ui namespace Ryujinx.Ui
{ {
@ -48,6 +49,8 @@ namespace Ryujinx.Ui
private HotkeyButtons _prevHotkeyButtons; private HotkeyButtons _prevHotkeyButtons;
private Client _dsuClient;
private GraphicsDebugLevel _glLogLevel; private GraphicsDebugLevel _glLogLevel;
public GlRenderer(Switch device, GraphicsDebugLevel glLogLevel) public GlRenderer(Switch device, GraphicsDebugLevel glLogLevel)
@ -79,6 +82,8 @@ namespace Ryujinx.Ui
this.Shown += Renderer_Shown; this.Shown += Renderer_Shown;
_dsuClient = new Client();
_glLogLevel = glLogLevel; _glLogLevel = glLogLevel;
} }
@ -90,6 +95,7 @@ namespace Ryujinx.Ui
private void GLRenderer_ShuttingDown(object sender, EventArgs args) private void GLRenderer_ShuttingDown(object sender, EventArgs args)
{ {
_device.DisposeGpu(); _device.DisposeGpu();
_dsuClient?.Dispose();
} }
private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args) private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
@ -104,6 +110,7 @@ namespace Ryujinx.Ui
private void GLRenderer_Destroyed(object sender, EventArgs e) private void GLRenderer_Destroyed(object sender, EventArgs e)
{ {
_dsuClient?.Dispose();
Dispose(); Dispose();
} }
@ -287,6 +294,7 @@ namespace Ryujinx.Ui
public void Exit() public void Exit()
{ {
_dsuClient?.Dispose();
if (IsStopped) if (IsStopped)
{ {
return; return;
@ -406,6 +414,9 @@ namespace Ryujinx.Ui
} }
List<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers); List<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers);
List<SixAxisInput> motionInputs = new List<SixAxisInput>(NpadDevices.MaxControllers);
MotionDevice motionDevice = new MotionDevice(_dsuClient);
foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value) foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value)
{ {
@ -419,6 +430,11 @@ namespace Ryujinx.Ui
int rightJoystickDx = 0; int rightJoystickDx = 0;
int rightJoystickDy = 0; int rightJoystickDy = 0;
if (inputConfig.EnableMotion)
{
motionDevice.RegisterController(inputConfig.PlayerIndex);
}
if (inputConfig is KeyboardConfig keyboardConfig) if (inputConfig is KeyboardConfig keyboardConfig)
{ {
if (IsFocused) if (IsFocused)
@ -488,6 +504,19 @@ namespace Ryujinx.Ui
currentButton |= _device.Hid.UpdateStickButtons(leftJoystick, rightJoystick); currentButton |= _device.Hid.UpdateStickButtons(leftJoystick, rightJoystick);
motionDevice.Poll(inputConfig.PlayerIndex, inputConfig.Slot);
SixAxisInput sixAxisInput = new SixAxisInput()
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
Accelerometer = motionDevice.Accelerometer,
Gyroscope = motionDevice.Gyroscope,
Rotation = motionDevice.Rotation,
Orientation = motionDevice.Orientation
};
motionInputs.Add(sixAxisInput);
gamepadInputs.Add(new GamepadInput gamepadInputs.Add(new GamepadInput
{ {
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex, PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
@ -495,9 +524,29 @@ namespace Ryujinx.Ui
LStick = leftJoystick, LStick = leftJoystick,
RStick = rightJoystick RStick = rightJoystick
}); });
if (inputConfig.ControllerType == Common.Configuration.Hid.ControllerType.JoyconPair)
{
if (!inputConfig.MirrorInput)
{
motionDevice.Poll(inputConfig.PlayerIndex, inputConfig.AltSlot);
sixAxisInput = new SixAxisInput()
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
Accelerometer = motionDevice.Accelerometer,
Gyroscope = motionDevice.Gyroscope,
Rotation = motionDevice.Rotation,
Orientation = motionDevice.Orientation
};
}
motionInputs.Add(sixAxisInput);
}
} }
_device.Hid.Npads.Update(gamepadInputs); _device.Hid.Npads.Update(gamepadInputs);
_device.Hid.Npads.UpdateSixAxis(motionInputs);
if(IsFocused) if(IsFocused)
{ {

View file

@ -82,6 +82,7 @@ namespace Ryujinx.Ui
[GUI] ToggleButton _configureController7; [GUI] ToggleButton _configureController7;
[GUI] ToggleButton _configureController8; [GUI] ToggleButton _configureController8;
[GUI] ToggleButton _configureControllerH; [GUI] ToggleButton _configureControllerH;
#pragma warning restore CS0649, IDE0044 #pragma warning restore CS0649, IDE0044
public SettingsWindow(VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(new Builder("Ryujinx.Ui.SettingsWindow.glade"), virtualFileSystem, contentManager) { } public SettingsWindow(VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(new Builder("Ryujinx.Ui.SettingsWindow.glade"), virtualFileSystem, contentManager) { }

View file

@ -7,11 +7,6 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">10</property> <property name="page_increment">10</property>
</object> </object>
<object class="GtkEntryCompletion" id="_systemTimeZoneCompletion">
<property name="inline-completion">True</property>
<property name="inline-selection">True</property>
<property name="minimum-key-length">0</property>
</object>
<object class="GtkAdjustment" id="_systemTimeDaySpinAdjustment"> <object class="GtkAdjustment" id="_systemTimeDaySpinAdjustment">
<property name="lower">1</property> <property name="lower">1</property>
<property name="upper">31</property> <property name="upper">31</property>
@ -40,6 +35,11 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">10</property> <property name="page_increment">10</property>
</object> </object>
<object class="GtkEntryCompletion" id="_systemTimeZoneCompletion">
<property name="minimum_key_length">0</property>
<property name="inline_completion">True</property>
<property name="inline_selection">True</property>
</object>
<object class="GtkWindow" id="_settingsWin"> <object class="GtkWindow" id="_settingsWin">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Ryujinx - Settings</property> <property name="title" translatable="yes">Ryujinx - Settings</property>
@ -1062,6 +1062,17 @@
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="position">1</property> <property name="position">1</property>
@ -1737,8 +1748,8 @@
<property name="tooltip_text" translatable="yes">Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.</property> <property name="tooltip_text" translatable="yes">Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.</property>
<property name="valign">center</property> <property name="valign">center</property>
<property name="caps_lock_warning">False</property> <property name="caps_lock_warning">False</property>
<property name="placeholder-text">1.0</property> <property name="placeholder_text">1.0</property>
<property name="input-purpose">GTK_INPUT_PURPOSE_NUMBER</property> <property name="input_purpose">number</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View file

@ -460,6 +460,110 @@
"default": "O" "default": "O"
} }
} }
},
"enable_motion": {
"$id": "#/definitions/keyboard_config/properties/enable_motion",
"type": "boolean",
"title": "Enable Motion Controls",
"description": "Enables Motion Controls",
"default": false,
"examples": [
true,
false
]
},
"sensitivity": {
"$id": "#/definitions/keyboard_config/properties/sensitivity",
"type": "integer",
"title": "Sensitivity",
"description": "Gyro sensitivity",
"default": 100,
"minimum": 0,
"maximum": 1000,
"examples": [
90,
100,
150
]
},
"gyro_deadzone": {
"$id": "#/definitions/keyboard_config/properties/gyro_deadzone",
"type": "number",
"title": "Gyro Deadzone",
"description": "Controller Left Analog Stick Deadzone",
"default": 1,
"minimum": 0.00,
"maximum": 100.00,
"examples": [
0.01
]
},
"slot": {
"$id": "#/definitions/keyboard_config/properties/slot",
"type": "integer",
"title": "Slot",
"description": "DSU motion client slot for main controller",
"default": 0,
"minimum": 0,
"maximum": 4,
"examples": [
0,
1,
2,
3
]
},
"alt_slot": {
"$id": "#/definitions/keyboard_config/properties/alt_slot",
"type": "integer",
"title": "Alternate Slot",
"description": "DSU motion client slot for secondary controller, eg Right Joycon in Paired mode",
"default": 0,
"minimum": 0,
"maximum": 4,
"examples": [
0,
1,
2,
3
]
},
"mirror_input": {
"$id": "#/definitions/keyboard_config/properties/mirror_input",
"type": "boolean",
"title": "Mirror Motion Input",
"description": "Mirrors main motion input in Paired mode",
"default": true,
"examples": [
true,
false
]
},
"dsu_server_port": {
"$id": "#/definitions/keyboard_config/properties/dsu_server_port",
"type": "integer",
"title": "DSU Server Port",
"description": "DSU motion server port",
"default": 26760,
"minimum": 0,
"maximum": 36654,
"examples": [
0,
1,
2,
3
]
},
"dsu_server_host": {
"$id": "#/definitions/keyboard_config/properties/dsu_server_host",
"type": "string",
"title": "DSU Server Host Address",
"description": "DSU motion server host address",
"default": "127.0.0.1",
"examples": [
"127.0.0.1",
"example.host.com"
]
} }
} }
}, },
@ -695,6 +799,110 @@
"default": "Button9" "default": "Button9"
} }
} }
},
"enable_motion": {
"$id": "#/definitions/controller_config/properties/enable_motion",
"type": "boolean",
"title": "Enable Motion Controls",
"description": "Enables Motion Controls",
"default": false,
"examples": [
true,
false
]
},
"sensitivity": {
"$id": "#/definitions/controller_config/properties/sensitivity",
"type": "integer",
"title": "Sensitivity",
"description": "Gyro sensitivity",
"default": 100,
"minimum": 0,
"maximum": 1000,
"examples": [
90,
100,
150
]
},
"gyro_deadzone": {
"$id": "#/definitions/controller_config/properties/gyro_deadzone",
"type": "number",
"title": "Gyro Deadzone",
"description": "Controller Left Analog Stick Deadzone",
"default": 1,
"minimum": 0.00,
"maximum": 100.00,
"examples": [
0.01
]
},
"slot": {
"$id": "#/definitions/controller_config/properties/slot",
"type": "integer",
"title": "Slot",
"description": "DSU motion client slot for main controller",
"default": 0,
"minimum": 0,
"maximum": 4,
"examples": [
0,
1,
2,
3
]
},
"alt_slot": {
"$id": "#/definitions/controller_config/properties/alt_slot",
"type": "integer",
"title": "Alternate Slot",
"description": "DSU motion client slot for secondary controller, eg Right Joycon in Paired mode",
"default": 0,
"minimum": 0,
"maximum": 4,
"examples": [
0,
1,
2,
3
]
},
"mirror_input": {
"$id": "#/definitions/controller_config/properties/mirror_input",
"type": "boolean",
"title": "Mirror Motion Input",
"description": "Mirrors main motion input in Paired mode",
"default": true,
"examples": [
true,
false
]
},
"dsu_server_port": {
"$id": "#/definitions/controller_config/properties/dsu_server_port",
"type": "integer",
"title": "DSU Server Port",
"description": "DSU motion server port",
"default": 26760,
"minimum": 0,
"maximum": 36654,
"examples": [
0,
1,
2,
3
]
},
"dsu_server_host": {
"$id": "#/definitions/controller_config/properties/dsu_server_host",
"type": "string",
"title": "DSU Server Host Address",
"description": "DSU motion server host address",
"default": "127.0.0.1",
"examples": [
"127.0.0.1",
"example.host.com"
]
} }
} }
} }
@ -1241,7 +1449,15 @@
"button_zr": "O", "button_zr": "O",
"button_sl": "Unbound", "button_sl": "Unbound",
"button_sr": "Unbound" "button_sr": "Unbound"
} },
"slot": 0,
"alt_slot": 0,
"mirror_input": false,
"dsu_server_host": "127.0.0.1",
"dsu_server_port": 26760,
"sensitivity": 100,
"gyro_deadzone": 1,
"enable_motion": false
} }
] ]
}, },