diff --git a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
index cab38046..7ea38bac 100644
--- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
@@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
///
/// The current version of the file format
///
- public const int CurrentVersion = 14;
+ public const int CurrentVersion = 15;
public int Version { get; set; }
diff --git a/Ryujinx.Common/Configuration/ConfigurationState.cs b/Ryujinx.Common/Configuration/ConfigurationState.cs
index df07019d..d83d07d3 100644
--- a/Ryujinx.Common/Configuration/ConfigurationState.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationState.cs
@@ -483,12 +483,10 @@ namespace Ryujinx.Configuration
Ui.EnableCustomTheme.Value = false;
Ui.CustomThemePath.Value = "";
Hid.EnableKeyboard.Value = false;
-
Hid.Hotkeys.Value = new KeyboardHotkeys
{
ToggleVsync = Key.Tab
};
-
Hid.InputConfig.Value = new List
{
new KeyboardConfig
@@ -529,7 +527,15 @@ namespace Ryujinx.Configuration
ButtonZr = Key.O,
ButtonSl = Key.PageUp,
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,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound
- }
+ },
+ EnableMotion = false,
+ MirrorInput = false,
+ Slot = 0,
+ AltSlot = 0,
+ Sensitivity = 100,
+ GyroDeadzone = 1,
+ DsuServerHost = "127.0.0.1",
+ DsuServerPort = 26760
}
};
diff --git a/Ryujinx.Common/Configuration/Hid/InputConfig.cs b/Ryujinx.Common/Configuration/Hid/InputConfig.cs
index 540506d5..7ccb989b 100644
--- a/Ryujinx.Common/Configuration/Hid/InputConfig.cs
+++ b/Ryujinx.Common/Configuration/Hid/InputConfig.cs
@@ -16,5 +16,45 @@ namespace Ryujinx.Common.Configuration.Hid
/// Player's Index for the controller
///
public PlayerIndex PlayerIndex { get; set; }
+
+ ///
+ /// Motion Controller Slot
+ ///
+ public int Slot { get; set; }
+
+ ///
+ /// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
+ ///
+ public int AltSlot { get; set; }
+
+ ///
+ /// Mirror motion input in Pair mode
+ ///
+ public bool MirrorInput { get; set; }
+
+ ///
+ /// Host address of the DSU Server
+ ///
+ public string DsuServerHost { get; set; }
+
+ ///
+ /// Port of the DSU Server
+ ///
+ public int DsuServerPort { get; set; }
+
+ ///
+ /// Gyro Sensitivity
+ ///
+ public int Sensitivity { get; set; }
+
+ ///
+ /// Gyro Deadzone
+ ///
+ public double GyroDeadzone { get; set; }
+
+ ///
+ /// Enable Motion Controls
+ ///
+ public bool EnableMotion { get; set; }
}
}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
index 334af975..0decbfea 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs
@@ -1,9 +1,9 @@
+using System;
+using System.Collections.Generic;
using Ryujinx.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Kernel.Threading;
-using System;
-using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Hid
{
@@ -317,6 +317,89 @@ namespace Ryujinx.HLE.HOS.Services.Hid
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 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()
{
ref Array10 controllers = ref _device.Hid.SharedMemory.Npads;
@@ -359,6 +442,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
break;
}
}
+
+ ref Array6 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;
+ }
}
}
}
diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/Types/SixAxisInput.cs b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/Types/SixAxisInput.cs
new file mode 100644
index 00000000..4dda82c7
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/Types/SixAxisInput.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisLayoutsIndex.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisLayoutsIndex.cs
new file mode 100644
index 00000000..a8795fc0
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisLayoutsIndex.cs
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisState.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisState.cs
index 0a799916..12974e7e 100644
--- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisState.cs
+++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisState.cs
@@ -7,8 +7,8 @@ namespace Ryujinx.HLE.HOS.Services.Hid
public ulong SampleTimestamp2;
public HidVector Accelerometer;
public HidVector Gyroscope;
- HidVector unknownSensor;
+ public HidVector Rotations;
public fixed float Orientation[9];
- ulong _unknown2;
+ public ulong _unknown2;
}
}
\ No newline at end of file
diff --git a/Ryujinx/Config.json b/Ryujinx/Config.json
index 55aaa9c2..fdbd9c1a 100644
--- a/Ryujinx/Config.json
+++ b/Ryujinx/Config.json
@@ -1,5 +1,5 @@
{
- "version": 14,
+ "version": 15,
"res_scale": 1,
"res_scale_custom": 1,
"max_anisotropy": -1,
@@ -86,7 +86,14 @@
"button_zr": "O",
"button_sl": "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": []
diff --git a/Ryujinx/Motion/Client.cs b/Ryujinx/Motion/Client.cs
new file mode 100644
index 00000000..07241ecd
--- /dev/null
+++ b/Ryujinx/Motion/Client.cs
@@ -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 _hosts;
+ private readonly Dictionary> _motionData;
+ private readonly Dictionary _clients;
+
+ private bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length];
+
+ public Client()
+ {
+ _hosts = new Dictionary();
+ _motionData = new Dictionary>();
+ _clients = new Dictionary();
+
+ 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();
+ break;
+ case MessageType.Data:
+ ControllerDataResponse inputData = reader.ReadStruct();
+
+ 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() { { 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();
+ }
+ }
+}
diff --git a/Ryujinx/Motion/MotionDevice.cs b/Ryujinx/Motion/MotionDevice.cs
new file mode 100644
index 00000000..82d84eb0
--- /dev/null
+++ b/Ryujinx/Motion/MotionDevice.cs
@@ -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;
+ }
+ }
+}
diff --git a/Ryujinx/Motion/MotionInput.cs b/Ryujinx/Motion/MotionInput.cs
new file mode 100644
index 00000000..f767d8cc
--- /dev/null
+++ b/Ryujinx/Motion/MotionInput.cs
@@ -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);
+ }
+ }
+}
diff --git a/Ryujinx/Motion/MotionSensorFilter.cs b/Ryujinx/Motion/MotionSensorFilter.cs
new file mode 100644
index 00000000..5173a191
--- /dev/null
+++ b/Ryujinx/Motion/MotionSensorFilter.cs
@@ -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
+ {
+ ///
+ /// Sample rate coefficient.
+ ///
+ public const float SampleRateCoefficient = 0.45f;
+
+ ///
+ /// Gets or sets the sample period.
+ ///
+ public float SamplePeriod { get; set; }
+
+ ///
+ /// Gets or sets the algorithm proportional gain.
+ ///
+ public float Kp { get; set; }
+
+ ///
+ /// Gets or sets the algorithm integral gain.
+ ///
+ public float Ki { get; set; }
+
+ ///
+ /// Gets the Quaternion output.
+ ///
+ public Quaternion Quaternion { get; private set; }
+
+ ///
+ /// Integral error.
+ ///
+ private Vector3 _intergralError;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Sample period.
+ ///
+ public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Sample period.
+ ///
+ ///
+ /// Algorithm proportional gain.
+ ///
+ public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Sample period.
+ ///
+ ///
+ /// Algorithm proportional gain.
+ ///
+ ///
+ /// Algorithm integral gain.
+ ///
+ public MotionSensorFilter(float samplePeriod, float kp, float ki)
+ {
+ SamplePeriod = samplePeriod;
+ Kp = kp;
+ Ki = ki;
+
+ Reset();
+
+ _intergralError = new Vector3();
+ }
+
+ ///
+ /// Algorithm IMU update method. Requires only gyroscope and accelerometer data.
+ ///
+ ///
+ /// Accelerometer measurement in any calibrated units.
+ ///
+ ///
+ /// Gyroscope measurement in radians.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/Ryujinx/Motion/Protocol/ControllerData.cs b/Ryujinx/Motion/Protocol/ControllerData.cs
new file mode 100644
index 00000000..4b4919a1
--- /dev/null
+++ b/Ryujinx/Motion/Protocol/ControllerData.cs
@@ -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
+ }
+}
diff --git a/Ryujinx/Motion/Protocol/ControllerInfo.cs b/Ryujinx/Motion/Protocol/ControllerInfo.cs
new file mode 100644
index 00000000..34177ff8
--- /dev/null
+++ b/Ryujinx/Motion/Protocol/ControllerInfo.cs
@@ -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;
+ }
+}
diff --git a/Ryujinx/Motion/Protocol/Header.cs b/Ryujinx/Motion/Protocol/Header.cs
new file mode 100644
index 00000000..1f6ea705
--- /dev/null
+++ b/Ryujinx/Motion/Protocol/Header.cs
@@ -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;
+ }
+}
diff --git a/Ryujinx/Motion/Protocol/MessageType.cs b/Ryujinx/Motion/Protocol/MessageType.cs
new file mode 100644
index 00000000..507910dd
--- /dev/null
+++ b/Ryujinx/Motion/Protocol/MessageType.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Motion
+{
+ public enum MessageType : uint
+ {
+ Protocol = 0x100000,
+ Info,
+ Data
+ }
+}
diff --git a/Ryujinx/Motion/Protocol/SharedResponse.cs b/Ryujinx/Motion/Protocol/SharedResponse.cs
new file mode 100644
index 00000000..8f918ccb
--- /dev/null
+++ b/Ryujinx/Motion/Protocol/SharedResponse.cs
@@ -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
+ }
+}
diff --git a/Ryujinx/Ryujinx.csproj b/Ryujinx/Ryujinx.csproj
index c4cba108..cd4b207f 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -71,6 +71,7 @@
+
diff --git a/Ryujinx/Ui/ControllerWindow.cs b/Ryujinx/Ui/ControllerWindow.cs
index 1d879eb5..40612874 100644
--- a/Ryujinx/Ui/ControllerWindow.cs
+++ b/Ryujinx/Ui/ControllerWindow.cs
@@ -28,10 +28,19 @@ namespace Ryujinx.Ui
[GUI] Adjustment _controllerDeadzoneLeft;
[GUI] Adjustment _controllerDeadzoneRight;
[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 _profile;
[GUI] ToggleButton _refreshInputDevicesButton;
[GUI] Box _settingsBox;
+ [GUI] Box _altBox;
[GUI] Grid _leftStickKeyboard;
[GUI] Grid _leftStickController;
[GUI] Box _deadZoneLeftBox;
@@ -225,6 +234,7 @@ namespace Ryujinx.Ui
{
_leftSideTriggerBox.Hide();
_rightSideTriggerBox.Hide();
+ _altBox.Hide();
switch (_controllerType.ActiveId)
{
@@ -234,6 +244,9 @@ namespace Ryujinx.Ui
case "JoyconRight":
_rightSideTriggerBox.Show();
break;
+ case "JoyconPair":
+ _altBox.Show();
+ break;
}
switch (_controllerType.ActiveId)
@@ -290,6 +303,14 @@ namespace Ryujinx.Ui
_controllerDeadzoneLeft.Value = 0;
_controllerDeadzoneRight.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)
@@ -304,34 +325,42 @@ namespace Ryujinx.Ui
: ControllerType.ProController.ToString());
}
- _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString();
- _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString();
- _lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString();
- _lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString();
- _lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString();
- _dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString();
- _dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString();
- _dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString();
- _dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString();
- _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString();
- _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString();
- _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString();
- _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString();
- _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString();
- _rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString();
- _rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString();
- _rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString();
- _rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString();
- _rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString();
- _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString();
- _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString();
- _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString();
- _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString();
- _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString();
- _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString();
- _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString();
- _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString();
- _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString();
+ _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString();
+ _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString();
+ _lStickLeft.Label = keyboardConfig.LeftJoycon.StickLeft.ToString();
+ _lStickRight.Label = keyboardConfig.LeftJoycon.StickRight.ToString();
+ _lStickButton.Label = keyboardConfig.LeftJoycon.StickButton.ToString();
+ _dpadUp.Label = keyboardConfig.LeftJoycon.DPadUp.ToString();
+ _dpadDown.Label = keyboardConfig.LeftJoycon.DPadDown.ToString();
+ _dpadLeft.Label = keyboardConfig.LeftJoycon.DPadLeft.ToString();
+ _dpadRight.Label = keyboardConfig.LeftJoycon.DPadRight.ToString();
+ _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString();
+ _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString();
+ _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString();
+ _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString();
+ _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString();
+ _rStickUp.Label = keyboardConfig.RightJoycon.StickUp.ToString();
+ _rStickDown.Label = keyboardConfig.RightJoycon.StickDown.ToString();
+ _rStickLeft.Label = keyboardConfig.RightJoycon.StickLeft.ToString();
+ _rStickRight.Label = keyboardConfig.RightJoycon.StickRight.ToString();
+ _rStickButton.Label = keyboardConfig.RightJoycon.StickButton.ToString();
+ _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString();
+ _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString();
+ _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString();
+ _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString();
+ _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString();
+ _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString();
+ _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString();
+ _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.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;
case ControllerConfig controllerConfig:
if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString()))
@@ -372,6 +401,14 @@ namespace Ryujinx.Ui
_controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
_controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
_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;
}
}
@@ -448,7 +485,15 @@ namespace Ryujinx.Ui
ButtonZr = rButtonZr,
ButtonSl = rButtonSl,
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,
ButtonSl = rButtonSl,
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,
ButtonSl = 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"))
@@ -824,7 +885,15 @@ namespace Ryujinx.Ui
ButtonSr = ControllerInputId.Unbound,
InvertStickX = false,
InvertStickY = false
- }
+ },
+ EnableMotion = false,
+ MirrorInput = false,
+ Slot = 0,
+ AltSlot = 0,
+ Sensitivity = 100,
+ GyroDeadzone = 1,
+ DsuServerHost = "127.0.0.1",
+ DsuServerPort = 26760
};
}
}
diff --git a/Ryujinx/Ui/ControllerWindow.glade b/Ryujinx/Ui/ControllerWindow.glade
index c0532d90..d148cfae 100644
--- a/Ryujinx/Ui/ControllerWindow.glade
+++ b/Ryujinx/Ui/ControllerWindow.glade
@@ -1,24 +1,47 @@
-
+
+
+
+
+
+
+
+
diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs
index 637b0239..d5ddee0f 100644
--- a/Ryujinx/Ui/GLRenderer.cs
+++ b/Ryujinx/Ui/GLRenderer.cs
@@ -13,6 +13,7 @@ using Ryujinx.HLE.HOS.Services.Hid;
using System;
using System.Collections.Generic;
using System.Threading;
+using Ryujinx.Motion;
namespace Ryujinx.Ui
{
@@ -48,6 +49,8 @@ namespace Ryujinx.Ui
private HotkeyButtons _prevHotkeyButtons;
+ private Client _dsuClient;
+
private GraphicsDebugLevel _glLogLevel;
public GlRenderer(Switch device, GraphicsDebugLevel glLogLevel)
@@ -79,6 +82,8 @@ namespace Ryujinx.Ui
this.Shown += Renderer_Shown;
+ _dsuClient = new Client();
+
_glLogLevel = glLogLevel;
}
@@ -90,6 +95,7 @@ namespace Ryujinx.Ui
private void GLRenderer_ShuttingDown(object sender, EventArgs args)
{
_device.DisposeGpu();
+ _dsuClient?.Dispose();
}
private void Parent_FocusOutEvent(object o, Gtk.FocusOutEventArgs args)
@@ -104,6 +110,7 @@ namespace Ryujinx.Ui
private void GLRenderer_Destroyed(object sender, EventArgs e)
{
+ _dsuClient?.Dispose();
Dispose();
}
@@ -287,6 +294,7 @@ namespace Ryujinx.Ui
public void Exit()
{
+ _dsuClient?.Dispose();
if (IsStopped)
{
return;
@@ -406,7 +414,10 @@ namespace Ryujinx.Ui
}
List gamepadInputs = new List(NpadDevices.MaxControllers);
+ List motionInputs = new List(NpadDevices.MaxControllers);
+ MotionDevice motionDevice = new MotionDevice(_dsuClient);
+
foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value)
{
ControllerKeys currentButton = 0;
@@ -419,6 +430,11 @@ namespace Ryujinx.Ui
int rightJoystickDx = 0;
int rightJoystickDy = 0;
+ if (inputConfig.EnableMotion)
+ {
+ motionDevice.RegisterController(inputConfig.PlayerIndex);
+ }
+
if (inputConfig is KeyboardConfig keyboardConfig)
{
if (IsFocused)
@@ -488,6 +504,19 @@ namespace Ryujinx.Ui
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
{
PlayerId = (HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex,
@@ -495,9 +524,29 @@ namespace Ryujinx.Ui
LStick = leftJoystick,
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.UpdateSixAxis(motionInputs);
if(IsFocused)
{
diff --git a/Ryujinx/Ui/SettingsWindow.cs b/Ryujinx/Ui/SettingsWindow.cs
index 9668a4bc..bd4cbbca 100644
--- a/Ryujinx/Ui/SettingsWindow.cs
+++ b/Ryujinx/Ui/SettingsWindow.cs
@@ -82,6 +82,7 @@ namespace Ryujinx.Ui
[GUI] ToggleButton _configureController7;
[GUI] ToggleButton _configureController8;
[GUI] ToggleButton _configureControllerH;
+
#pragma warning restore CS0649, IDE0044
public SettingsWindow(VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : this(new Builder("Ryujinx.Ui.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
diff --git a/Ryujinx/Ui/SettingsWindow.glade b/Ryujinx/Ui/SettingsWindow.glade
index 56a528f0..9a51ba2b 100644
--- a/Ryujinx/Ui/SettingsWindow.glade
+++ b/Ryujinx/Ui/SettingsWindow.glade
@@ -7,11 +7,6 @@
1
10
-
- True
- True
- 0
-
1
31
@@ -40,6 +35,11 @@
1
10
+
+ 0
+ True
+ True
+
False
Ryujinx - Settings
@@ -1062,6 +1062,17 @@
2
+
+
+ True
+ False
+
+
+ False
+ True
+ 3
+
+
1
@@ -1737,8 +1748,8 @@
Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.
center
False
- 1.0
- GTK_INPUT_PURPOSE_NUMBER
+ 1.0
+ number
True
diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json
index d85aa438..4401c03c 100644
--- a/Ryujinx/_schema.json
+++ b/Ryujinx/_schema.json
@@ -460,6 +460,110 @@
"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"
}
}
+ },
+ "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_sl": "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
}
]
},