From 26319d5ab3a4d2f93fc7acb70760d9f96575ee07 Mon Sep 17 00:00:00 2001
From: emmauss <emmausssss@gmail.com>
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 <Acoustik666@gmail.com>

* cleanup

* add reference orientation and derive relative orientation from it

* cleanup

* remove unused variable and strange file

* Review_2.

* change GetInput to TryGetInput

* Review_3.

Co-authored-by: Ac_K <Acoustik666@gmail.com>
Co-authored-by: LDj3SNuD <dvitiello@gmail.com>
---
 .../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 cab38046e2..7ea38bace2 100644
--- a/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
+++ b/Ryujinx.Common/Configuration/ConfigurationFileFormat.cs
@@ -14,7 +14,7 @@ namespace Ryujinx.Configuration
         /// <summary>
         /// The current version of the file format
         /// </summary>
-        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 df07019dcb..d83d07d3c3 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<InputConfig>
             {
                 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 540506d5e0..7ccb989b46 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
         /// </summary>
         public PlayerIndex PlayerIndex { get; set; }
+
+        /// <summary>
+        /// Motion Controller Slot
+        /// </summary>
+        public int Slot { get; set; }
+        
+        /// <summary>
+        /// Motion Controller Alternative Slot, for RightJoyCon in Pair mode
+        /// </summary>
+        public int AltSlot { get; set; }
+
+        /// <summary>
+        /// Mirror motion input in Pair mode
+        /// </summary>
+        public bool MirrorInput { get; set; }
+
+        /// <summary>
+        /// Host address of the DSU Server
+        /// </summary>
+        public string DsuServerHost { get; set; }
+
+        /// <summary>
+        /// Port of the DSU Server
+        /// </summary>
+        public int DsuServerPort { get; set; }
+
+        /// <summary>
+        /// Gyro Sensitivity
+        /// </summary>
+        public int Sensitivity { get; set; }
+
+        /// <summary>
+        /// Gyro Deadzone
+        /// </summary>
+        public double GyroDeadzone { get; set; }
+        
+        /// <summary>
+        /// Enable Motion Controls
+        /// </summary>
+        public bool EnableMotion { get; set; }
     }
 }
\ 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 334af975bb..0decbfea95 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<SixAxisInput> states)
+        {
+            for (int i = 0; i < states.Count; ++i)
+            {
+                if (SetSixAxisState(states[i]))
+                {
+                    i++;
+
+                    SetSixAxisState(states[i], true);
+                }
+            }
+        }
+
+        private bool SetSixAxisState(SixAxisInput state, bool isRightPair = false)
+        {
+            if (state.PlayerId == PlayerIndex.Unknown)
+            {
+                return false;
+            }
+
+            ref ShMemNpad currentNpad = ref _device.Hid.SharedMemory.Npads[(int)state.PlayerId];
+
+            if (currentNpad.Header.Type == ControllerType.None)
+            {
+                return false;
+            }
+
+            HidVector accel = new HidVector()
+            {
+                X = state.Accelerometer.X,
+                Y = state.Accelerometer.Y,
+                Z = state.Accelerometer.Z
+            };
+
+            HidVector gyro = new HidVector()
+            {
+                X = state.Gyroscope.X,
+                Y = state.Gyroscope.Y,
+                Z = state.Gyroscope.Z
+            };
+
+            HidVector rotation = new HidVector()
+            {
+                X = state.Rotation.X,
+                Y = state.Rotation.Y,
+                Z = state.Rotation.Z
+            };
+
+            ref NpadSixAxis currentLayout = ref currentNpad.Sixaxis[(int)ControllerTypeToSixAxisLayout(currentNpad.Header.Type) + (isRightPair ? 1 : 0)];
+            ref SixAxisState currentEntry = ref currentLayout.Entries[(int)currentLayout.Header.LatestEntry];
+
+            int previousEntryIndex = (int)(currentLayout.Header.LatestEntry == 0 ?
+                                           currentLayout.Header.MaxEntryIndex : currentLayout.Header.LatestEntry - 1);
+
+            ref SixAxisState previousEntry = ref currentLayout.Entries[previousEntryIndex];
+
+            currentEntry.Accelerometer = accel;
+            currentEntry.Gyroscope     = gyro;
+            currentEntry.Rotations     = rotation;
+
+            unsafe
+            {
+                for (int i = 0; i < 9; i++)
+                {
+                    currentEntry.Orientation[i] = state.Orientation[i];
+                }
+            }
+
+            return currentNpad.Header.Type == ControllerType.JoyconPair && !isRightPair;
+        }
+
         private void UpdateAllEntries()
         {
             ref Array10<ShMemNpad> controllers = ref _device.Hid.SharedMemory.Npads;
@@ -359,6 +442,21 @@ namespace Ryujinx.HLE.HOS.Services.Hid
                             break;
                     }
                 }
+
+                ref Array6<NpadSixAxis> sixaxis = ref controllers[i].Sixaxis;
+                for (int l = 0; l < sixaxis.Length; ++l)
+                {
+                    ref NpadSixAxis currentLayout = ref sixaxis[l];
+                    int currentIndex = UpdateEntriesHeader(ref currentLayout.Header, out int previousIndex);
+
+                    ref SixAxisState currentEntry = ref currentLayout.Entries[currentIndex];
+                    SixAxisState previousEntry = currentLayout.Entries[previousIndex];
+
+                    currentEntry.SampleTimestamp  = previousEntry.SampleTimestamp + 1;
+                    currentEntry.SampleTimestamp2 = previousEntry.SampleTimestamp2 + 1;
+
+                    currentEntry._unknown2 = 1;
+                }
             }
         }
     }
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 0000000000..4dda82c72c
--- /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 0000000000..a8795fc05b
--- /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 0a79991625..12974e7e31 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 55aaa9c2ea..fdbd9c1aff 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 0000000000..07241ecd8d
--- /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<int, IPEndPoint> _hosts;
+        private readonly Dictionary<int, Dictionary<int, MotionInput>> _motionData;
+        private readonly Dictionary<int, UdpClient> _clients;
+
+        private bool[] _clientErrorStatus = new bool[Enum.GetValues(typeof(PlayerIndex)).Length];
+
+        public Client()
+        {
+            _hosts      = new Dictionary<int, IPEndPoint>();
+            _motionData = new Dictionary<int, Dictionary<int, MotionInput>>();
+            _clients    = new Dictionary<int, UdpClient>();
+
+            CloseClients();
+        }
+
+        public void CloseClients()
+        {
+            _active = false;
+
+            lock (_clients)
+            {
+                foreach (var client in _clients)
+                {
+                    try
+                    {
+                        client.Value?.Dispose();
+                    }
+#pragma warning disable CS0168
+                    catch (SocketException ex)
+#pragma warning restore CS0168
+                    {
+                        Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error code {ex.ErrorCode}");
+                    }
+                }
+
+                _hosts.Clear();
+                _clients.Clear();
+                _motionData.Clear();
+            }
+        }
+
+        public void RegisterClient(int player, string host, int port)
+        {
+            if (_clients.ContainsKey(player))
+            {
+                return;
+            }
+
+            try
+            {
+                lock (_clients)
+                {
+                    IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port);
+
+                    UdpClient client = new UdpClient(host, port);
+
+                    _clients.Add(player, client);
+                    _hosts.Add(player, endPoint);
+
+                    _active = true;
+
+                    Task.Run(() =>
+                    {
+                        ReceiveLoop(player);
+                    });
+                }
+            }
+            catch (FormatException fex)
+            {
+                if (!_clientErrorStatus[player])
+                {
+                    Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error {fex.Message}");
+
+                    _clientErrorStatus[player] = true;
+                }
+            }
+            catch (SocketException ex)
+            {
+                if (!_clientErrorStatus[player])
+                {
+                    Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error code {ex.ErrorCode}");
+
+                    _clientErrorStatus[player] = true;
+                }
+            }
+        }
+
+        public bool TryGetData(int player, int slot, out MotionInput input)
+        {
+            lock (_motionData)
+            {
+                if (_motionData.ContainsKey(player))
+                {
+                    input = _motionData[player][slot];
+
+                    return true;
+                }
+            }
+
+            input = null;
+
+            return false;
+        }
+
+        private void Send(byte[] data, int clientId)
+        {
+            if (_clients.TryGetValue(clientId, out UdpClient _client))
+            {
+                if (_client != null && _client.Client != null && _client.Client.Connected)
+                {
+                    try
+                    {
+                        _client?.Send(data, data.Length);
+                    }
+                    catch (SocketException ex)
+                    {
+                        if (!_clientErrorStatus[clientId])
+                        {
+                            Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error code {ex.ErrorCode}");
+                        }
+
+                        _clientErrorStatus[clientId] = true;
+
+                        _clients.Remove(clientId);
+
+                        _hosts.Remove(clientId);
+
+                        _client?.Dispose();
+                    }
+                }
+            }
+        }
+
+        private byte[] Receive(int clientId)
+        {
+            if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint))
+            {
+                if (_clients.TryGetValue(clientId, out UdpClient _client))
+                {
+                    if (_client != null && _client.Client != null)
+                    {
+                        if (_client.Client.Connected)
+                        {
+                            try
+                            {
+                                var result = _client?.Receive(ref endPoint);
+
+                                if (result.Length > 0)
+                                {
+                                    _clientErrorStatus[clientId] = false;
+                                }
+
+                                return result;
+                            }
+                            catch (SocketException ex)
+                            {
+                                if (!_clientErrorStatus[clientId])
+                                {
+                                    Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error code {ex.ErrorCode}");
+                                }
+
+                                _clientErrorStatus[clientId] = true;
+
+                                _clients.Remove(clientId);
+
+                                _hosts.Remove(clientId);
+
+                                _client?.Dispose();
+                            }
+                        }
+                    }
+                }
+            }
+            
+            return new byte[0];
+        }
+
+        public void ReceiveLoop(int clientId)
+        {
+            while (_active)
+            {
+                byte[] data = Receive(clientId);
+
+                if (data.Length == 0)
+                {
+                    continue;
+                }
+
+#pragma warning disable CS4014
+                HandleResponse(data, clientId);
+#pragma warning restore CS4014
+            }
+        }
+
+#pragma warning disable CS1998
+        public async Task HandleResponse(byte[] data, int clientId)
+#pragma warning restore CS1998
+        {
+            MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4));
+
+            data = data.AsSpan().Slice(16).ToArray();
+
+            using (MemoryStream mem = new MemoryStream(data))
+            {
+                using (BinaryReader reader = new BinaryReader(mem))
+                {
+                    switch (type)
+                    {
+                        case MessageType.Protocol:
+                            break;
+                        case MessageType.Info:
+                            ControllerInfoResponse contollerInfo = reader.ReadStruct<ControllerInfoResponse>();
+                            break;
+                        case MessageType.Data:
+                            ControllerDataResponse inputData = reader.ReadStruct<ControllerDataResponse>();
+
+                            Vector3 accelerometer = new Vector3()
+                            {
+                                X = -inputData.AccelerometerX,
+                                Y = inputData.AccelerometerZ,
+                                Z = -inputData.AccelerometerY
+                            };
+
+                            Vector3 gyroscrope = new Vector3()
+                            {
+                                X = inputData.GyroscopePitch,
+                                Y = inputData.GyroscopeRoll,
+                                Z = -inputData.GyroscopeYaw
+                            };
+
+                            ulong timestamp = inputData.MotionTimestamp;
+
+                            InputConfig config = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)clientId);
+
+                            lock (_motionData)
+                            {
+                                int slot = inputData.Shared.Slot;
+
+                                if (_motionData.ContainsKey(clientId))
+                                {
+                                    if (_motionData[clientId].ContainsKey(slot))
+                                    {
+                                        var previousData = _motionData[clientId][slot];
+
+                                        previousData.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
+                                    }
+                                    else
+                                    {
+                                        MotionInput input = new MotionInput();
+                                        input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
+                                        _motionData[clientId].Add(slot, input);
+                                    }
+                                }
+                                else
+                                {
+                                    MotionInput input = new MotionInput();
+                                    input.Update(accelerometer, gyroscrope, timestamp, config.Sensitivity, (float)config.GyroDeadzone);
+                                    _motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
+                                }
+                            }
+                            break;
+                    }
+                }
+            }
+        }
+
+        public void RequestInfo(int clientId, int slot)
+        {
+            if (!_active)
+            {
+                return;
+            }
+
+            Header header = GenerateHeader(clientId);
+
+            using (MemoryStream mem = new MemoryStream())
+            {
+                using (BinaryWriter writer = new BinaryWriter(mem))
+                {
+                    writer.WriteStruct(header);
+
+                    ControllerInfoRequest request = new ControllerInfoRequest()
+                    {
+                        Type = MessageType.Info,
+                        PortsCount = 4
+                    };
+
+                    request.PortIndices[0] = (byte)slot;
+
+                    writer.WriteStruct(request);
+
+                    header.Length = (ushort)(mem.Length - 16);
+
+                    writer.Seek(6, SeekOrigin.Begin);
+                    writer.Write(header.Length);
+
+                    header.Crc32 = Crc32Algorithm.Compute(mem.ToArray());
+
+                    writer.Seek(8, SeekOrigin.Begin);
+                    writer.Write(header.Crc32);
+
+                    byte[] data = mem.ToArray();
+
+                    Send(data, clientId);
+                }
+            }
+        }
+
+        public unsafe  void RequestData(int clientId, int slot)
+        {
+            if (!_active)
+            {
+                return;
+            }
+
+            Header header = GenerateHeader(clientId);
+
+            using (MemoryStream mem = new MemoryStream())
+            {
+                using (BinaryWriter writer = new BinaryWriter(mem))
+                {
+                    writer.WriteStruct(header);
+
+                    ControllerDataRequest request = new ControllerDataRequest()
+                    {
+                        Type = MessageType.Data,
+                        Slot = (byte)slot,
+                        SubscriberType = SubscriberType.Slot
+                    };
+
+                    writer.WriteStruct(request);
+
+                    header.Length = (ushort)(mem.Length - 16);
+
+                    writer.Seek(6, SeekOrigin.Begin);
+                    writer.Write(header.Length);
+
+                    header.Crc32 = Crc32Algorithm.Compute(mem.ToArray());
+
+                    writer.Seek(8, SeekOrigin.Begin);
+                    writer.Write(header.Crc32);
+
+                    byte[] data = mem.ToArray();
+
+                    Send(data, clientId);
+                }
+            }
+        }
+
+        private Header GenerateHeader(int clientId)
+        {
+            Header header = new Header()
+            {
+                ID          = (uint)clientId,
+                MagicString = Magic,
+                Version     = Version,
+                Length      = 0,
+                Crc32       = 0
+            };
+
+            return header;
+        }
+
+        public void Dispose()
+        {
+            _active = false;
+
+            CloseClients();
+        }
+    }
+}
diff --git a/Ryujinx/Motion/MotionDevice.cs b/Ryujinx/Motion/MotionDevice.cs
new file mode 100644
index 0000000000..82d84eb017
--- /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 0000000000..f767d8cc71
--- /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 0000000000..5173a191be
--- /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
+    {
+        /// <summary>
+        /// Sample rate coefficient.
+        /// </summary>
+        public const float SampleRateCoefficient = 0.45f;
+
+        /// <summary>
+        /// Gets or sets the sample period.
+        /// </summary>
+        public float SamplePeriod { get; set; }
+
+        /// <summary>
+        /// Gets or sets the algorithm proportional gain.
+        /// </summary>
+        public float Kp { get; set; }
+
+        /// <summary>
+        /// Gets or sets the algorithm integral gain.
+        /// </summary>
+        public float Ki { get; set; }
+
+        /// <summary>
+        /// Gets the Quaternion output.
+        /// </summary>
+        public Quaternion Quaternion { get; private set; }
+
+        /// <summary>
+        /// Integral error.
+        /// </summary>
+        private Vector3 _intergralError;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
+        /// </summary>
+        /// <param name="samplePeriod">
+        /// Sample period.
+        /// </param>
+        public MotionSensorFilter(float samplePeriod) : this(samplePeriod, 1f, 0f)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
+        /// </summary>
+        /// <param name="samplePeriod">
+        /// Sample period.
+        /// </param>
+        /// <param name="kp">
+        /// Algorithm proportional gain.
+        /// </param>
+        public MotionSensorFilter(float samplePeriod, float kp) : this(samplePeriod, kp, 0f)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MotionSensorFilter"/> class.
+        /// </summary>
+        /// <param name="samplePeriod">
+        /// Sample period.
+        /// </param>
+        /// <param name="kp">
+        /// Algorithm proportional gain.
+        /// </param>
+        /// <param name="ki">
+        /// Algorithm integral gain.
+        /// </param>
+        public MotionSensorFilter(float samplePeriod, float kp, float ki)
+        {
+            SamplePeriod = samplePeriod;
+            Kp = kp;
+            Ki = ki;
+
+            Reset();
+
+            _intergralError = new Vector3();
+        }
+
+        /// <summary>
+        /// Algorithm IMU update method. Requires only gyroscope and accelerometer data.
+        /// </summary>
+        /// <param name="accel">
+        /// Accelerometer measurement in any calibrated units.
+        /// </param>
+        /// <param name="gyro">
+        /// Gyroscope measurement in radians.
+        /// </param>
+        public void Update(Vector3 accel, Vector3 gyro)
+        {
+            // Normalise accelerometer measurement.
+            float norm = 1f / accel.Length();
+
+            if (!float.IsFinite(norm))
+            {
+                return;
+            }
+
+            accel *= norm;
+
+            float q2 = Quaternion.X;
+            float q3 = Quaternion.Y;
+            float q4 = Quaternion.Z;
+            float q1 = Quaternion.W;
+
+            // Estimated direction of gravity.
+            Vector3 gravity = new Vector3()
+            {
+                X = 2f * (q2 * q4 - q1 * q3),
+                Y = 2f * (q1 * q2 + q3 * q4),
+                Z = q1 * q1 - q2 * q2 - q3 * q3 + q4 * q4
+            };
+
+            // Error is cross product between estimated direction and measured direction of gravity.
+            Vector3 error = new Vector3()
+            {
+                X = accel.Y * gravity.Z - accel.Z * gravity.Y,
+                Y = accel.Z * gravity.X - accel.X * gravity.Z,
+                Z = accel.X * gravity.Y - accel.Y * gravity.X
+            };
+
+            if (Ki > 0f)
+            {
+                _intergralError += error; // Accumulate integral error.
+            }
+            else
+            {
+                _intergralError = Vector3.Zero; // Prevent integral wind up.
+            }
+
+            // Apply feedback terms.
+            gyro += (Kp * error) + (Ki * _intergralError);
+
+            // Integrate rate of change of quaternion.
+            Vector3 delta = new Vector3(q2, q3, q4);
+
+            q1 += (-q2 * gyro.X - q3 * gyro.Y - q4 * gyro.Z) * (SampleRateCoefficient * SamplePeriod);
+            q2 += (q1 * gyro.X + delta.Y * gyro.Z - delta.Z * gyro.Y) * (SampleRateCoefficient * SamplePeriod);
+            q3 += (q1 * gyro.Y - delta.X * gyro.Z + delta.Z * gyro.X) * (SampleRateCoefficient * SamplePeriod);
+            q4 += (q1 * gyro.Z + delta.X * gyro.Y - delta.Y * gyro.X) * (SampleRateCoefficient * SamplePeriod);
+
+            // Normalise quaternion.
+            Quaternion quaternion = new Quaternion(q2, q3, q4, q1);
+
+            norm = 1f / quaternion.Length();
+
+            if (!float.IsFinite(norm))
+            {
+                return;
+            }
+
+            Quaternion = quaternion * norm;
+        }
+
+        public void Reset()
+        {
+            Quaternion = Quaternion.Identity;
+        }
+    }
+}
diff --git a/Ryujinx/Motion/Protocol/ControllerData.cs b/Ryujinx/Motion/Protocol/ControllerData.cs
new file mode 100644
index 0000000000..4b4919a19e
--- /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 0000000000..34177ff826
--- /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 0000000000..1f6ea70564
--- /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 0000000000..507910dd79
--- /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 0000000000..8f918ccbbc
--- /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 c4cba10867..cd4b207feb 100644
--- a/Ryujinx/Ryujinx.csproj
+++ b/Ryujinx/Ryujinx.csproj
@@ -71,6 +71,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="Crc32.NET" Version="1.2.0" />
     <PackageReference Include="DiscordRichPresence" Version="1.0.150" />
     <PackageReference Include="GLWidget" Version="1.0.2" />
     <PackageReference Include="GtkSharp" Version="3.22.25.56" />
diff --git a/Ryujinx/Ui/ControllerWindow.cs b/Ryujinx/Ui/ControllerWindow.cs
index 1d879eb593..406128746a 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 c0532d9071..d148cfaef8 100644
--- a/Ryujinx/Ui/ControllerWindow.glade
+++ b/Ryujinx/Ui/ControllerWindow.glade
@@ -1,24 +1,47 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.22.1 -->
+<!-- Generated with glade 3.36.0 -->
 <interface>
   <requires lib="gtk+" version="3.20"/>
+  <object class="GtkAdjustment" id="_altSlotNumber">
+    <property name="upper">4</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">4</property>
+  </object>
   <object class="GtkAdjustment" id="_controllerDeadzoneLeft">
     <property name="upper">1</property>
-    <property name="value">0.050000000000000003</property>
+    <property name="value">0.05</property>
     <property name="step_increment">0.01</property>
-    <property name="page_increment">0.10000000000000001</property>
+    <property name="page_increment">0.1</property>
   </object>
   <object class="GtkAdjustment" id="_controllerDeadzoneRight">
     <property name="upper">1</property>
-    <property name="value">0.050000000000000003</property>
+    <property name="value">0.05</property>
     <property name="step_increment">0.01</property>
-    <property name="page_increment">0.10000000000000001</property>
+    <property name="page_increment">0.1</property>
   </object>
   <object class="GtkAdjustment" id="_controllerTriggerThreshold">
     <property name="upper">1</property>
     <property name="value">0.5</property>
     <property name="step_increment">0.01</property>
-    <property name="page_increment">0.10000000000000001</property>
+    <property name="page_increment">0.1</property>
+  </object>
+  <object class="GtkAdjustment" id="_gyroDeadzone">
+    <property name="upper">100</property>
+    <property name="value">0.01</property>
+    <property name="step_increment">0.01</property>
+    <property name="page_increment">0.1</property>
+    <property name="page_size">0.1</property>
+  </object>
+  <object class="GtkAdjustment" id="_sensitivity">
+    <property name="upper">1000</property>
+    <property name="value">100</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">4</property>
+  </object>
+  <object class="GtkAdjustment" id="_slotNumber">
+    <property name="upper">4</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">4</property>
   </object>
   <object class="GtkWindow" id="_controllerWin">
     <property name="can_focus">False</property>
@@ -27,9 +50,6 @@
     <property name="window_position">center</property>
     <property name="default_width">1100</property>
     <property name="default_height">600</property>
-    <child>
-      <placeholder/>
-    </child>
     <child>
       <object class="GtkBox">
         <property name="visible">True</property>
@@ -1617,12 +1637,301 @@
                                   </packing>
                                 </child>
                                 <child>
-                                  <object class="GtkLabel">
+                                  <object class="GtkBox" id="MotionBox">
                                     <property name="visible">True</property>
                                     <property name="can_focus">False</property>
+                                    <property name="margin_left">10</property>
+                                    <property name="margin_right">10</property>
+                                    <property name="orientation">vertical</property>
+                                    <property name="spacing">5</property>
+                                    <child>
+                                      <object class="GtkLabel">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="margin_top">5</property>
+                                        <property name="margin_bottom">5</property>
+                                        <property name="label" translatable="yes">Motion</property>
+                                        <attributes>
+                                          <attribute name="weight" value="bold"/>
+                                        </attributes>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">0</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkCheckButton" id="_enableMotion">
+                                        <property name="label" translatable="yes">Enable Motion Controls</property>
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">False</property>
+                                        <property name="draw_indicator">True</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">1</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="spacing">10</property>
+                                        <child>
+                                          <object class="GtkLabel">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="margin_right">17</property>
+                                            <property name="label" translatable="yes">Controller Slot</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="padding">5</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkSpinButton" id="_slot">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                            <property name="margin_left">10</property>
+                                            <property name="adjustment">_slotNumber</property>
+                                            <property name="climb_rate">1</property>
+                                            <property name="snap_to_ticks">True</property>
+                                            <property name="numeric">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">2</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="spacing">10</property>
+                                        <child>
+                                          <object class="GtkLabel">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="margin_right">5</property>
+                                            <property name="label" translatable="yes">Gyro Sensitivity %</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="padding">5</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkSpinButton">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                            <property name="text" translatable="yes">0</property>
+                                            <property name="adjustment">_sensitivity</property>
+                                            <property name="climb_rate">1</property>
+                                            <property name="snap_to_ticks">True</property>
+                                            <property name="numeric">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">3</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkBox" id="_altBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="orientation">vertical</property>
+                                        <child>
+                                          <object class="GtkCheckButton" id="_mirrorInput">
+                                            <property name="label" translatable="yes">Mirror Input</property>
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                            <property name="receives_default">False</property>
+                                            <property name="draw_indicator">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkBox">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="spacing">10</property>
+                                            <child>
+                                              <object class="GtkLabel">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">False</property>
+                                                <property name="label" translatable="yes">Right JoyCon Slot</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">False</property>
+                                                <property name="fill">True</property>
+                                                <property name="padding">5</property>
+                                                <property name="position">0</property>
+                                              </packing>
+                                            </child>
+                                            <child>
+                                              <object class="GtkSpinButton" id="_slotRight">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">True</property>
+                                                <property name="text" translatable="yes">0</property>
+                                                <property name="climb_rate">1</property>
+                                                <property name="snap_to_ticks">True</property>
+                                                <property name="numeric">True</property>
+                                              </object>
+                                              <packing>
+                                                <property name="expand">False</property>
+                                                <property name="fill">True</property>
+                                                <property name="position">1</property>
+                                              </packing>
+                                            </child>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="padding">5</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">4</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="spacing">30</property>
+                                        <child>
+                                          <object class="GtkLabel">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="label" translatable="yes">Server Host</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="padding">5</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkEntry" id="_dsuServerHost">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">5</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkBox">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="spacing">30</property>
+                                        <child>
+                                          <object class="GtkLabel">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="label" translatable="yes">Server Port</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="padding">5</property>
+                                            <property name="position">0</property>
+                                          </packing>
+                                        </child>
+                                        <child>
+                                          <object class="GtkEntry" id="_dsuServerPort">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">True</property>
+                                          </object>
+                                          <packing>
+                                            <property name="expand">False</property>
+                                            <property name="fill">True</property>
+                                            <property name="position">1</property>
+                                          </packing>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">6</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkLabel">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="halign">start</property>
+                                        <property name="label" translatable="yes">Gyro Deadzone</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">7</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkScale">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="adjustment">_gyroDeadzone</property>
+                                        <property name="round_digits">2</property>
+                                        <property name="digits">2</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">True</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">8</property>
+                                      </packing>
+                                    </child>
                                   </object>
                                   <packing>
-                                    <property name="expand">True</property>
+                                    <property name="expand">False</property>
                                     <property name="fill">True</property>
                                     <property name="position">4</property>
                                   </packing>
@@ -1721,5 +2030,8 @@
         </child>
       </object>
     </child>
+    <child type="titlebar">
+      <placeholder/>
+    </child>
   </object>
 </interface>
diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs
index 637b02392e..d5ddee0fd9 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<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers);
+            List<SixAxisInput> motionInputs  = new List<SixAxisInput>(NpadDevices.MaxControllers);
 
+            MotionDevice motionDevice = new MotionDevice(_dsuClient);
+            
             foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value)
             {
                 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 9668a4bcc9..bd4cbbca36 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 56a528f0f8..9a51ba2b65 100644
--- a/Ryujinx/Ui/SettingsWindow.glade
+++ b/Ryujinx/Ui/SettingsWindow.glade
@@ -7,11 +7,6 @@
     <property name="step_increment">1</property>
     <property name="page_increment">10</property>
   </object>
-  <object class="GtkEntryCompletion" id="_systemTimeZoneCompletion">
-    <property name="inline-completion">True</property>
-    <property name="inline-selection">True</property>
-    <property name="minimum-key-length">0</property>
-  </object>
   <object class="GtkAdjustment" id="_systemTimeDaySpinAdjustment">
     <property name="lower">1</property>
     <property name="upper">31</property>
@@ -40,6 +35,11 @@
     <property name="step_increment">1</property>
     <property name="page_increment">10</property>
   </object>
+  <object class="GtkEntryCompletion" id="_systemTimeZoneCompletion">
+    <property name="minimum_key_length">0</property>
+    <property name="inline_completion">True</property>
+    <property name="inline_selection">True</property>
+  </object>
   <object class="GtkWindow" id="_settingsWin">
     <property name="can_focus">False</property>
     <property name="title" translatable="yes">Ryujinx - Settings</property>
@@ -1062,6 +1062,17 @@
                             <property name="position">2</property>
                           </packing>
                         </child>
+                        <child>
+                          <object class="GtkSeparator">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
                       </object>
                       <packing>
                         <property name="position">1</property>
@@ -1737,8 +1748,8 @@
                                         <property name="tooltip_text" translatable="yes">Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.</property>
                                         <property name="valign">center</property>
                                         <property name="caps_lock_warning">False</property>
-                                        <property name="placeholder-text">1.0</property>
-                                        <property name="input-purpose">GTK_INPUT_PURPOSE_NUMBER</property>
+                                        <property name="placeholder_text">1.0</property>
+                                        <property name="input_purpose">number</property>
                                       </object>
                                       <packing>
                                         <property name="expand">True</property>
diff --git a/Ryujinx/_schema.json b/Ryujinx/_schema.json
index d85aa4382b..4401c03caa 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
         }
       ]
     },