From 26319d5ab3a4d2f93fc7acb70760d9f96575ee07 Mon Sep 17 00:00:00 2001 From: emmauss Date: Tue, 29 Sep 2020 21:32:42 +0000 Subject: [PATCH] Add Motion controls (#1363) * Add motion controls Apply suggestions from code review Co-authored-by: Ac_K * 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 Co-authored-by: LDj3SNuD --- .../Configuration/ConfigurationFileFormat.cs | 2 +- .../Configuration/ConfigurationState.cs | 22 +- .../Configuration/Hid/InputConfig.cs | 40 ++ .../Services/Hid/HidDevices/NpadDevices.cs | 102 ++++- .../Hid/HidDevices/Types/SixAxisInput.cs | 13 + .../SharedMem/Npad/SixAxisLayoutsIndex.cs | 14 + .../Hid/Types/SharedMem/Npad/SixAxisState.cs | 4 +- Ryujinx/Config.json | 11 +- Ryujinx/Motion/Client.cs | 393 ++++++++++++++++++ Ryujinx/Motion/MotionDevice.cs | 83 ++++ Ryujinx/Motion/MotionInput.cs | 85 ++++ Ryujinx/Motion/MotionSensorFilter.cs | 166 ++++++++ Ryujinx/Motion/Protocol/ControllerData.cs | 50 +++ Ryujinx/Motion/Protocol/ControllerInfo.cs | 21 + Ryujinx/Motion/Protocol/Header.cs | 14 + Ryujinx/Motion/Protocol/MessageType.cs | 9 + Ryujinx/Motion/Protocol/SharedResponse.cs | 51 +++ Ryujinx/Ryujinx.csproj | 1 + Ryujinx/Ui/ControllerWindow.cs | 133 ++++-- Ryujinx/Ui/ControllerWindow.glade | 334 ++++++++++++++- Ryujinx/Ui/GLRenderer.cs | 51 ++- Ryujinx/Ui/SettingsWindow.cs | 1 + Ryujinx/Ui/SettingsWindow.glade | 25 +- Ryujinx/_schema.json | 218 +++++++++- 24 files changed, 1780 insertions(+), 63 deletions(-) create mode 100644 Ryujinx.HLE/HOS/Services/Hid/HidDevices/Types/SixAxisInput.cs create mode 100644 Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/SixAxisLayoutsIndex.cs create mode 100644 Ryujinx/Motion/Client.cs create mode 100644 Ryujinx/Motion/MotionDevice.cs create mode 100644 Ryujinx/Motion/MotionInput.cs create mode 100644 Ryujinx/Motion/MotionSensorFilter.cs create mode 100644 Ryujinx/Motion/Protocol/ControllerData.cs create mode 100644 Ryujinx/Motion/Protocol/ControllerInfo.cs create mode 100644 Ryujinx/Motion/Protocol/Header.cs create mode 100644 Ryujinx/Motion/Protocol/MessageType.cs create mode 100644 Ryujinx/Motion/Protocol/SharedResponse.cs 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 @@ - + + + 4 + 1 + 4 + 1 - 0.050000000000000003 + 0.05 0.01 - 0.10000000000000001 + 0.1 1 - 0.050000000000000003 + 0.05 0.01 - 0.10000000000000001 + 0.1 1 0.5 0.01 - 0.10000000000000001 + 0.1 + + + 100 + 0.01 + 0.01 + 0.1 + 0.1 + + + 1000 + 100 + 1 + 4 + + + 4 + 1 + 4 False @@ -27,9 +50,6 @@ center 1100 600 - - - True @@ -1617,12 +1637,301 @@ - + True False + 10 + 10 + vertical + 5 + + + True + False + 5 + 5 + Motion + + + + + + False + True + 0 + + + + + Enable Motion Controls + True + True + False + True + + + False + True + 1 + + + + + True + False + 10 + + + True + False + 17 + Controller Slot + + + False + True + 5 + 0 + + + + + True + True + 10 + _slotNumber + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 2 + + + + + True + False + 10 + + + True + False + 5 + Gyro Sensitivity % + + + False + True + 5 + 0 + + + + + True + True + 0 + _sensitivity + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 3 + + + + + True + False + vertical + + + Mirror Input + True + True + False + True + + + False + True + 0 + + + + + True + False + 10 + + + True + False + Right JoyCon Slot + + + False + True + 5 + 0 + + + + + True + True + 0 + 1 + True + True + + + False + True + 1 + + + + + False + True + 5 + 1 + + + + + False + True + 4 + + + + + True + False + 30 + + + True + False + Server Host + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 5 + + + + + True + False + 30 + + + True + False + Server Port + + + False + True + 5 + 0 + + + + + True + True + + + False + True + 1 + + + + + False + True + 5 + 6 + + + + + True + False + start + Gyro Deadzone + + + False + True + 7 + + + + + True + True + _gyroDeadzone + 2 + 2 + + + True + True + 8 + + - True + False True 4 @@ -1721,5 +2030,8 @@ + + + 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 } ] },