aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Input
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Input
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Input')
-rw-r--r--src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs198
-rw-r--r--src/Ryujinx.Input/Assigner/IButtonAssigner.cs36
-rw-r--r--src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs50
-rw-r--r--src/Ryujinx.Input/GamepadButtonInputId.cs57
-rw-r--r--src/Ryujinx.Input/GamepadFeaturesFlag.cs28
-rw-r--r--src/Ryujinx.Input/GamepadStateSnapshot.cs70
-rw-r--r--src/Ryujinx.Input/HLE/InputManager.cs54
-rw-r--r--src/Ryujinx.Input/HLE/NpadController.cs569
-rw-r--r--src/Ryujinx.Input/HLE/NpadManager.cs320
-rw-r--r--src/Ryujinx.Input/HLE/TouchScreenManager.cs99
-rw-r--r--src/Ryujinx.Input/IGamepad.cs122
-rw-r--r--src/Ryujinx.Input/IGamepadDriver.cs37
-rw-r--r--src/Ryujinx.Input/IKeyboard.cs41
-rw-r--r--src/Ryujinx.Input/IMouse.cs104
-rw-r--r--src/Ryujinx.Input/Key.cs142
-rw-r--r--src/Ryujinx.Input/KeyboardStateSnapshot.cs29
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Client.cs475
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs47
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs20
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs15
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs9
-rw-r--r--src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs51
-rw-r--r--src/Ryujinx.Input/Motion/MotionInput.cs65
-rw-r--r--src/Ryujinx.Input/Motion/MotionSensorFilter.cs162
-rw-r--r--src/Ryujinx.Input/MotionInputId.cs25
-rw-r--r--src/Ryujinx.Input/MouseButton.cs16
-rw-r--r--src/Ryujinx.Input/MouseStateSnapshot.cs45
-rw-r--r--src/Ryujinx.Input/Ryujinx.Input.csproj17
-rw-r--r--src/Ryujinx.Input/StickInputId.cs14
29 files changed, 2917 insertions, 0 deletions
diff --git a/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs
new file mode 100644
index 00000000..8621b3a5
--- /dev/null
+++ b/src/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Ryujinx.Input.Assigner
+{
+ /// <summary>
+ /// <see cref="IButtonAssigner"/> implementation for regular <see cref="IGamepad"/>.
+ /// </summary>
+ public class GamepadButtonAssigner : IButtonAssigner
+ {
+ private IGamepad _gamepad;
+
+ private GamepadStateSnapshot _currState;
+
+ private GamepadStateSnapshot _prevState;
+
+ private JoystickButtonDetector _detector;
+
+ private bool _forStick;
+
+ public GamepadButtonAssigner(IGamepad gamepad, float triggerThreshold, bool forStick)
+ {
+ _gamepad = gamepad;
+ _detector = new JoystickButtonDetector();
+ _forStick = forStick;
+
+ _gamepad?.SetTriggerThreshold(triggerThreshold);
+ }
+
+ public void Initialize()
+ {
+ if (_gamepad != null)
+ {
+ _currState = _gamepad.GetStateSnapshot();
+ _prevState = _currState;
+ }
+ }
+
+ public void ReadInput()
+ {
+ if (_gamepad != null)
+ {
+ _prevState = _currState;
+ _currState = _gamepad.GetStateSnapshot();
+ }
+
+ CollectButtonStats();
+ }
+
+ public bool HasAnyButtonPressed()
+ {
+ return _detector.HasAnyButtonPressed();
+ }
+
+ public bool ShouldCancel()
+ {
+ return _gamepad == null || !_gamepad.IsConnected;
+ }
+
+ public string GetPressedButton()
+ {
+ IEnumerable<GamepadButtonInputId> pressedButtons = _detector.GetPressedButtons();
+
+ if (pressedButtons.Any())
+ {
+ return !_forStick ? pressedButtons.First().ToString() : ((StickInputId)pressedButtons.First()).ToString();
+ }
+
+ return "";
+ }
+
+ private void CollectButtonStats()
+ {
+ if (_forStick)
+ {
+ for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
+ {
+ (float x, float y) = _currState.GetStick(inputId);
+
+ float value;
+
+ if (x != 0.0f)
+ {
+ value = x;
+ }
+ else if (y != 0.0f)
+ {
+ value = y;
+ }
+ else
+ {
+ continue;
+ }
+
+ _detector.AddInput((GamepadButtonInputId)inputId, value);
+ }
+ }
+ else
+ {
+ for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
+ {
+ if (_currState.IsPressed(inputId) && !_prevState.IsPressed(inputId))
+ {
+ _detector.AddInput(inputId, 1);
+ }
+
+ if (!_currState.IsPressed(inputId) && _prevState.IsPressed(inputId))
+ {
+ _detector.AddInput(inputId, -1);
+ }
+ }
+ }
+ }
+
+ private class JoystickButtonDetector
+ {
+ private Dictionary<GamepadButtonInputId, InputSummary> _stats;
+
+ public JoystickButtonDetector()
+ {
+ _stats = new Dictionary<GamepadButtonInputId, InputSummary>();
+ }
+
+ public bool HasAnyButtonPressed()
+ {
+ return _stats.Values.Any(CheckButtonPressed);
+ }
+
+ public IEnumerable<GamepadButtonInputId> GetPressedButtons()
+ {
+ return _stats.Where(kvp => CheckButtonPressed(kvp.Value)).Select(kvp => kvp.Key);
+ }
+
+ public void AddInput(GamepadButtonInputId button, float value)
+ {
+ InputSummary inputSummary;
+
+ if (!_stats.TryGetValue(button, out inputSummary))
+ {
+ inputSummary = new InputSummary();
+ _stats.Add(button, inputSummary);
+ }
+
+ inputSummary.AddInput(value);
+ }
+
+ public override string ToString()
+ {
+ StringWriter writer = new StringWriter();
+
+ foreach (var kvp in _stats)
+ {
+ writer.WriteLine($"Button {kvp.Key} -> {kvp.Value}");
+ }
+
+ return writer.ToString();
+ }
+
+ private bool CheckButtonPressed(InputSummary sequence)
+ {
+ float distance = Math.Abs(sequence.Min - sequence.Avg) + Math.Abs(sequence.Max - sequence.Avg);
+ return distance > 1.5; // distance range [0, 2]
+ }
+ }
+
+ private class InputSummary
+ {
+ public float Min, Max, Sum, Avg;
+
+ public int NumSamples;
+
+ public InputSummary()
+ {
+ Min = float.MaxValue;
+ Max = float.MinValue;
+ Sum = 0;
+ NumSamples = 0;
+ Avg = 0;
+ }
+
+ public void AddInput(float value)
+ {
+ Min = Math.Min(Min, value);
+ Max = Math.Max(Max, value);
+ Sum += value;
+ NumSamples += 1;
+ Avg = Sum / NumSamples;
+ }
+
+ public override string ToString()
+ {
+ return $"Avg: {Avg} Min: {Min} Max: {Max} Sum: {Sum} NumSamples: {NumSamples}";
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/Assigner/IButtonAssigner.cs b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs
new file mode 100644
index 00000000..021736df
--- /dev/null
+++ b/src/Ryujinx.Input/Assigner/IButtonAssigner.cs
@@ -0,0 +1,36 @@
+namespace Ryujinx.Input.Assigner
+{
+ /// <summary>
+ /// An interface that allows to gather the driver input info to assign to a button on the UI.
+ /// </summary>
+ public interface IButtonAssigner
+ {
+ /// <summary>
+ /// Initialize the button assigner.
+ /// </summary>
+ void Initialize();
+
+ /// <summary>
+ /// Read input.
+ /// </summary>
+ void ReadInput();
+
+ /// <summary>
+ /// Check if a button was pressed.
+ /// </summary>
+ /// <returns>True if a button was pressed</returns>
+ bool HasAnyButtonPressed();
+
+ /// <summary>
+ /// Indicate if the user of this API should cancel operations. This is triggered for example when a gamepad get disconnected or when a user cancel assignation operations.
+ /// </summary>
+ /// <returns>True if the user of this API should cancel operations</returns>
+ bool ShouldCancel();
+
+ /// <summary>
+ /// Get the pressed button that was read in <see cref="ReadInput"/> by the button assigner.
+ /// </summary>
+ /// <returns>The pressed button that was read</returns>
+ string GetPressedButton();
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs
new file mode 100644
index 00000000..23ae3655
--- /dev/null
+++ b/src/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs
@@ -0,0 +1,50 @@
+namespace Ryujinx.Input.Assigner
+{
+ /// <summary>
+ /// <see cref="IButtonAssigner"/> implementation for <see cref="IKeyboard"/>.
+ /// </summary>
+ public class KeyboardKeyAssigner : IButtonAssigner
+ {
+ private IKeyboard _keyboard;
+
+ private KeyboardStateSnapshot _keyboardState;
+
+ public KeyboardKeyAssigner(IKeyboard keyboard)
+ {
+ _keyboard = keyboard;
+ }
+
+ public void Initialize() { }
+
+ public void ReadInput()
+ {
+ _keyboardState = _keyboard.GetKeyboardStateSnapshot();
+ }
+
+ public bool HasAnyButtonPressed()
+ {
+ return GetPressedButton().Length != 0;
+ }
+
+ public bool ShouldCancel()
+ {
+ return _keyboardState.IsPressed(Key.Escape);
+ }
+
+ public string GetPressedButton()
+ {
+ string keyPressed = "";
+
+ for (Key key = Key.Unknown; key < Key.Count; key++)
+ {
+ if (_keyboardState.IsPressed(key))
+ {
+ keyPressed = key.ToString();
+ break;
+ }
+ }
+
+ return !ShouldCancel() ? keyPressed : "";
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/GamepadButtonInputId.cs b/src/Ryujinx.Input/GamepadButtonInputId.cs
new file mode 100644
index 00000000..d1e4b9ac
--- /dev/null
+++ b/src/Ryujinx.Input/GamepadButtonInputId.cs
@@ -0,0 +1,57 @@
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent a button from a gamepad.
+ /// </summary>
+ public enum GamepadButtonInputId : byte
+ {
+ Unbound,
+ A,
+ B,
+ X,
+ Y,
+ LeftStick,
+ RightStick,
+ LeftShoulder,
+ RightShoulder,
+
+ // Likely axis
+ LeftTrigger,
+ // Likely axis
+ RightTrigger,
+
+ DpadUp,
+ DpadDown,
+ DpadLeft,
+ DpadRight,
+
+ // Special buttons
+
+ Minus,
+ Plus,
+
+ Back = Minus,
+ Start = Plus,
+
+ Guide,
+ Misc1,
+
+ // Xbox Elite paddle
+ Paddle1,
+ Paddle2,
+ Paddle3,
+ Paddle4,
+
+ // PS5 touchpad button
+ Touchpad,
+
+ // Virtual buttons for single joycon
+ SingleLeftTrigger0,
+ SingleRightTrigger0,
+
+ SingleLeftTrigger1,
+ SingleRightTrigger1,
+
+ Count
+ }
+}
diff --git a/src/Ryujinx.Input/GamepadFeaturesFlag.cs b/src/Ryujinx.Input/GamepadFeaturesFlag.cs
new file mode 100644
index 00000000..87310a32
--- /dev/null
+++ b/src/Ryujinx.Input/GamepadFeaturesFlag.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent features supported by a <see cref="IGamepad"/>.
+ /// </summary>
+ [Flags]
+ public enum GamepadFeaturesFlag
+ {
+ /// <summary>
+ /// No features are supported
+ /// </summary>
+ None,
+
+ /// <summary>
+ /// Rumble
+ /// </summary>
+ /// <remarks>Also named haptic</remarks>
+ Rumble,
+
+ /// <summary>
+ /// Motion
+ /// <remarks>Also named sixaxis</remarks>
+ /// </summary>
+ Motion
+ }
+}
diff --git a/src/Ryujinx.Input/GamepadStateSnapshot.cs b/src/Ryujinx.Input/GamepadStateSnapshot.cs
new file mode 100644
index 00000000..cf3e3e28
--- /dev/null
+++ b/src/Ryujinx.Input/GamepadStateSnapshot.cs
@@ -0,0 +1,70 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// A snapshot of a <see cref="IGamepad"/>.
+ /// </summary>
+ public struct GamepadStateSnapshot
+ {
+ // NOTE: Update Array size if JoystickInputId is changed.
+ private Array3<Array2<float>> _joysticksState;
+ // NOTE: Update Array size if GamepadInputId is changed.
+ private Array28<bool> _buttonsState;
+
+ /// <summary>
+ /// Create a new instance of <see cref="GamepadStateSnapshot"/>.
+ /// </summary>
+ /// <param name="joysticksState">The joysticks state</param>
+ /// <param name="buttonsState">The buttons state</param>
+ public GamepadStateSnapshot(Array3<Array2<float>> joysticksState, Array28<bool> buttonsState)
+ {
+ _joysticksState = joysticksState;
+ _buttonsState = buttonsState;
+ }
+
+ /// <summary>
+ /// Check if a given input button is pressed.
+ /// </summary>
+ /// <param name="inputId">The button id</param>
+ /// <returns>True if the given button is pressed</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsPressed(GamepadButtonInputId inputId) => _buttonsState[(int)inputId];
+
+
+ /// <summary>
+ /// Set the state of a given button.
+ /// </summary>
+ /// <param name="inputId">The button id</param>
+ /// <param name="value">The state to assign for the given button.</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetPressed(GamepadButtonInputId inputId, bool value) => _buttonsState[(int)inputId] = value;
+
+ /// <summary>
+ /// Get the values of a given input joystick.
+ /// </summary>
+ /// <param name="inputId">The stick id</param>
+ /// <returns>The values of the given input joystick</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public (float, float) GetStick(StickInputId inputId)
+ {
+ var result = _joysticksState[(int)inputId];
+
+ return (result[0], result[1]);
+ }
+
+ /// <summary>
+ /// Set the values of a given input joystick.
+ /// </summary>
+ /// <param name="inputId">The stick id</param>
+ /// <param name="x">The x axis value</param>
+ /// <param name="y">The y axis value</param>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetStick(StickInputId inputId, float x, float y)
+ {
+ _joysticksState[(int)inputId][0] = x;
+ _joysticksState[(int)inputId][1] = y;
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/HLE/InputManager.cs b/src/Ryujinx.Input/HLE/InputManager.cs
new file mode 100644
index 00000000..bc38cf5a
--- /dev/null
+++ b/src/Ryujinx.Input/HLE/InputManager.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace Ryujinx.Input.HLE
+{
+ public class InputManager : IDisposable
+ {
+ public IGamepadDriver KeyboardDriver { get; private set; }
+ public IGamepadDriver GamepadDriver { get; private set; }
+ public IGamepadDriver MouseDriver { get; private set; }
+
+ public InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver)
+ {
+ KeyboardDriver = keyboardDriver;
+ GamepadDriver = gamepadDriver;
+ }
+
+ public void SetMouseDriver(IGamepadDriver mouseDriver)
+ {
+ MouseDriver?.Dispose();
+
+ MouseDriver = mouseDriver;
+ }
+
+ public NpadManager CreateNpadManager()
+ {
+ return new NpadManager(KeyboardDriver, GamepadDriver, MouseDriver);
+ }
+
+ public TouchScreenManager CreateTouchScreenManager()
+ {
+ if (MouseDriver == null)
+ {
+ throw new InvalidOperationException("Mouse Driver has not been initialized.");
+ }
+
+ return new TouchScreenManager(MouseDriver.GetGamepad("0") as IMouse);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ KeyboardDriver?.Dispose();
+ GamepadDriver?.Dispose();
+ MouseDriver?.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/HLE/NpadController.cs b/src/Ryujinx.Input/HLE/NpadController.cs
new file mode 100644
index 00000000..46c0fc33
--- /dev/null
+++ b/src/Ryujinx.Input/HLE/NpadController.cs
@@ -0,0 +1,569 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.HOS.Services.Hid;
+using System;
+using System.Collections.Concurrent;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
+using ConfigControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
+
+namespace Ryujinx.Input.HLE
+{
+ public class NpadController : IDisposable
+ {
+ private class HLEButtonMappingEntry
+ {
+ public readonly GamepadButtonInputId DriverInputId;
+ public readonly ControllerKeys HLEInput;
+
+ public HLEButtonMappingEntry(GamepadButtonInputId driverInputId, ControllerKeys hleInput)
+ {
+ DriverInputId = driverInputId;
+ HLEInput = hleInput;
+ }
+ }
+
+ private static readonly HLEButtonMappingEntry[] _hleButtonMapping = new HLEButtonMappingEntry[]
+ {
+ new HLEButtonMappingEntry(GamepadButtonInputId.A, ControllerKeys.A),
+ new HLEButtonMappingEntry(GamepadButtonInputId.B, ControllerKeys.B),
+ new HLEButtonMappingEntry(GamepadButtonInputId.X, ControllerKeys.X),
+ new HLEButtonMappingEntry(GamepadButtonInputId.Y, ControllerKeys.Y),
+ new HLEButtonMappingEntry(GamepadButtonInputId.LeftStick, ControllerKeys.LStick),
+ new HLEButtonMappingEntry(GamepadButtonInputId.RightStick, ControllerKeys.RStick),
+ new HLEButtonMappingEntry(GamepadButtonInputId.LeftShoulder, ControllerKeys.L),
+ new HLEButtonMappingEntry(GamepadButtonInputId.RightShoulder, ControllerKeys.R),
+ new HLEButtonMappingEntry(GamepadButtonInputId.LeftTrigger, ControllerKeys.Zl),
+ new HLEButtonMappingEntry(GamepadButtonInputId.RightTrigger, ControllerKeys.Zr),
+ new HLEButtonMappingEntry(GamepadButtonInputId.DpadUp, ControllerKeys.DpadUp),
+ new HLEButtonMappingEntry(GamepadButtonInputId.DpadDown, ControllerKeys.DpadDown),
+ new HLEButtonMappingEntry(GamepadButtonInputId.DpadLeft, ControllerKeys.DpadLeft),
+ new HLEButtonMappingEntry(GamepadButtonInputId.DpadRight, ControllerKeys.DpadRight),
+ new HLEButtonMappingEntry(GamepadButtonInputId.Minus, ControllerKeys.Minus),
+ new HLEButtonMappingEntry(GamepadButtonInputId.Plus, ControllerKeys.Plus),
+
+ new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, ControllerKeys.SlLeft),
+ new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, ControllerKeys.SrLeft),
+ new HLEButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, ControllerKeys.SlRight),
+ new HLEButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, ControllerKeys.SrRight),
+ };
+
+ private class HLEKeyboardMappingEntry
+ {
+ public readonly Key TargetKey;
+ public readonly byte Target;
+
+ public HLEKeyboardMappingEntry(Key targetKey, byte target)
+ {
+ TargetKey = targetKey;
+ Target = target;
+ }
+ }
+
+ private static readonly HLEKeyboardMappingEntry[] KeyMapping = new HLEKeyboardMappingEntry[]
+ {
+ new HLEKeyboardMappingEntry(Key.A, 0x4),
+ new HLEKeyboardMappingEntry(Key.B, 0x5),
+ new HLEKeyboardMappingEntry(Key.C, 0x6),
+ new HLEKeyboardMappingEntry(Key.D, 0x7),
+ new HLEKeyboardMappingEntry(Key.E, 0x8),
+ new HLEKeyboardMappingEntry(Key.F, 0x9),
+ new HLEKeyboardMappingEntry(Key.G, 0xA),
+ new HLEKeyboardMappingEntry(Key.H, 0xB),
+ new HLEKeyboardMappingEntry(Key.I, 0xC),
+ new HLEKeyboardMappingEntry(Key.J, 0xD),
+ new HLEKeyboardMappingEntry(Key.K, 0xE),
+ new HLEKeyboardMappingEntry(Key.L, 0xF),
+ new HLEKeyboardMappingEntry(Key.M, 0x10),
+ new HLEKeyboardMappingEntry(Key.N, 0x11),
+ new HLEKeyboardMappingEntry(Key.O, 0x12),
+ new HLEKeyboardMappingEntry(Key.P, 0x13),
+ new HLEKeyboardMappingEntry(Key.Q, 0x14),
+ new HLEKeyboardMappingEntry(Key.R, 0x15),
+ new HLEKeyboardMappingEntry(Key.S, 0x16),
+ new HLEKeyboardMappingEntry(Key.T, 0x17),
+ new HLEKeyboardMappingEntry(Key.U, 0x18),
+ new HLEKeyboardMappingEntry(Key.V, 0x19),
+ new HLEKeyboardMappingEntry(Key.W, 0x1A),
+ new HLEKeyboardMappingEntry(Key.X, 0x1B),
+ new HLEKeyboardMappingEntry(Key.Y, 0x1C),
+ new HLEKeyboardMappingEntry(Key.Z, 0x1D),
+
+ new HLEKeyboardMappingEntry(Key.Number1, 0x1E),
+ new HLEKeyboardMappingEntry(Key.Number2, 0x1F),
+ new HLEKeyboardMappingEntry(Key.Number3, 0x20),
+ new HLEKeyboardMappingEntry(Key.Number4, 0x21),
+ new HLEKeyboardMappingEntry(Key.Number5, 0x22),
+ new HLEKeyboardMappingEntry(Key.Number6, 0x23),
+ new HLEKeyboardMappingEntry(Key.Number7, 0x24),
+ new HLEKeyboardMappingEntry(Key.Number8, 0x25),
+ new HLEKeyboardMappingEntry(Key.Number9, 0x26),
+ new HLEKeyboardMappingEntry(Key.Number0, 0x27),
+
+ new HLEKeyboardMappingEntry(Key.Enter, 0x28),
+ new HLEKeyboardMappingEntry(Key.Escape, 0x29),
+ new HLEKeyboardMappingEntry(Key.BackSpace, 0x2A),
+ new HLEKeyboardMappingEntry(Key.Tab, 0x2B),
+ new HLEKeyboardMappingEntry(Key.Space, 0x2C),
+ new HLEKeyboardMappingEntry(Key.Minus, 0x2D),
+ new HLEKeyboardMappingEntry(Key.Plus, 0x2E),
+ new HLEKeyboardMappingEntry(Key.BracketLeft, 0x2F),
+ new HLEKeyboardMappingEntry(Key.BracketRight, 0x30),
+ new HLEKeyboardMappingEntry(Key.BackSlash, 0x31),
+ new HLEKeyboardMappingEntry(Key.Tilde, 0x32),
+ new HLEKeyboardMappingEntry(Key.Semicolon, 0x33),
+ new HLEKeyboardMappingEntry(Key.Quote, 0x34),
+ new HLEKeyboardMappingEntry(Key.Grave, 0x35),
+ new HLEKeyboardMappingEntry(Key.Comma, 0x36),
+ new HLEKeyboardMappingEntry(Key.Period, 0x37),
+ new HLEKeyboardMappingEntry(Key.Slash, 0x38),
+ new HLEKeyboardMappingEntry(Key.CapsLock, 0x39),
+
+ new HLEKeyboardMappingEntry(Key.F1, 0x3a),
+ new HLEKeyboardMappingEntry(Key.F2, 0x3b),
+ new HLEKeyboardMappingEntry(Key.F3, 0x3c),
+ new HLEKeyboardMappingEntry(Key.F4, 0x3d),
+ new HLEKeyboardMappingEntry(Key.F5, 0x3e),
+ new HLEKeyboardMappingEntry(Key.F6, 0x3f),
+ new HLEKeyboardMappingEntry(Key.F7, 0x40),
+ new HLEKeyboardMappingEntry(Key.F8, 0x41),
+ new HLEKeyboardMappingEntry(Key.F9, 0x42),
+ new HLEKeyboardMappingEntry(Key.F10, 0x43),
+ new HLEKeyboardMappingEntry(Key.F11, 0x44),
+ new HLEKeyboardMappingEntry(Key.F12, 0x45),
+
+ new HLEKeyboardMappingEntry(Key.PrintScreen, 0x46),
+ new HLEKeyboardMappingEntry(Key.ScrollLock, 0x47),
+ new HLEKeyboardMappingEntry(Key.Pause, 0x48),
+ new HLEKeyboardMappingEntry(Key.Insert, 0x49),
+ new HLEKeyboardMappingEntry(Key.Home, 0x4A),
+ new HLEKeyboardMappingEntry(Key.PageUp, 0x4B),
+ new HLEKeyboardMappingEntry(Key.Delete, 0x4C),
+ new HLEKeyboardMappingEntry(Key.End, 0x4D),
+ new HLEKeyboardMappingEntry(Key.PageDown, 0x4E),
+ new HLEKeyboardMappingEntry(Key.Right, 0x4F),
+ new HLEKeyboardMappingEntry(Key.Left, 0x50),
+ new HLEKeyboardMappingEntry(Key.Down, 0x51),
+ new HLEKeyboardMappingEntry(Key.Up, 0x52),
+
+ new HLEKeyboardMappingEntry(Key.NumLock, 0x53),
+ new HLEKeyboardMappingEntry(Key.KeypadDivide, 0x54),
+ new HLEKeyboardMappingEntry(Key.KeypadMultiply, 0x55),
+ new HLEKeyboardMappingEntry(Key.KeypadSubtract, 0x56),
+ new HLEKeyboardMappingEntry(Key.KeypadAdd, 0x57),
+ new HLEKeyboardMappingEntry(Key.KeypadEnter, 0x58),
+ new HLEKeyboardMappingEntry(Key.Keypad1, 0x59),
+ new HLEKeyboardMappingEntry(Key.Keypad2, 0x5A),
+ new HLEKeyboardMappingEntry(Key.Keypad3, 0x5B),
+ new HLEKeyboardMappingEntry(Key.Keypad4, 0x5C),
+ new HLEKeyboardMappingEntry(Key.Keypad5, 0x5D),
+ new HLEKeyboardMappingEntry(Key.Keypad6, 0x5E),
+ new HLEKeyboardMappingEntry(Key.Keypad7, 0x5F),
+ new HLEKeyboardMappingEntry(Key.Keypad8, 0x60),
+ new HLEKeyboardMappingEntry(Key.Keypad9, 0x61),
+ new HLEKeyboardMappingEntry(Key.Keypad0, 0x62),
+ new HLEKeyboardMappingEntry(Key.KeypadDecimal, 0x63),
+
+ new HLEKeyboardMappingEntry(Key.F13, 0x68),
+ new HLEKeyboardMappingEntry(Key.F14, 0x69),
+ new HLEKeyboardMappingEntry(Key.F15, 0x6A),
+ new HLEKeyboardMappingEntry(Key.F16, 0x6B),
+ new HLEKeyboardMappingEntry(Key.F17, 0x6C),
+ new HLEKeyboardMappingEntry(Key.F18, 0x6D),
+ new HLEKeyboardMappingEntry(Key.F19, 0x6E),
+ new HLEKeyboardMappingEntry(Key.F20, 0x6F),
+ new HLEKeyboardMappingEntry(Key.F21, 0x70),
+ new HLEKeyboardMappingEntry(Key.F22, 0x71),
+ new HLEKeyboardMappingEntry(Key.F23, 0x72),
+ new HLEKeyboardMappingEntry(Key.F24, 0x73),
+
+ new HLEKeyboardMappingEntry(Key.ControlLeft, 0xE0),
+ new HLEKeyboardMappingEntry(Key.ShiftLeft, 0xE1),
+ new HLEKeyboardMappingEntry(Key.AltLeft, 0xE2),
+ new HLEKeyboardMappingEntry(Key.WinLeft, 0xE3),
+ new HLEKeyboardMappingEntry(Key.ControlRight, 0xE4),
+ new HLEKeyboardMappingEntry(Key.ShiftRight, 0xE5),
+ new HLEKeyboardMappingEntry(Key.AltRight, 0xE6),
+ new HLEKeyboardMappingEntry(Key.WinRight, 0xE7),
+ };
+
+ private static readonly HLEKeyboardMappingEntry[] KeyModifierMapping = new HLEKeyboardMappingEntry[]
+ {
+ new HLEKeyboardMappingEntry(Key.ControlLeft, 0),
+ new HLEKeyboardMappingEntry(Key.ShiftLeft, 1),
+ new HLEKeyboardMappingEntry(Key.AltLeft, 2),
+ new HLEKeyboardMappingEntry(Key.WinLeft, 3),
+ new HLEKeyboardMappingEntry(Key.ControlRight, 4),
+ new HLEKeyboardMappingEntry(Key.ShiftRight, 5),
+ new HLEKeyboardMappingEntry(Key.AltRight, 6),
+ new HLEKeyboardMappingEntry(Key.WinRight, 7),
+ new HLEKeyboardMappingEntry(Key.CapsLock, 8),
+ new HLEKeyboardMappingEntry(Key.ScrollLock, 9),
+ new HLEKeyboardMappingEntry(Key.NumLock, 10),
+ };
+
+ private bool _isValid;
+ private string _id;
+
+ private MotionInput _leftMotionInput;
+ private MotionInput _rightMotionInput;
+
+ private IGamepad _gamepad;
+ private InputConfig _config;
+
+ public IGamepadDriver GamepadDriver { get; private set; }
+ public GamepadStateSnapshot State { get; private set; }
+
+ public string Id => _id;
+
+ private CemuHookClient _cemuHookClient;
+
+ public NpadController(CemuHookClient cemuHookClient)
+ {
+ State = default;
+ _id = null;
+ _isValid = false;
+ _cemuHookClient = cemuHookClient;
+ }
+
+ public bool UpdateDriverConfiguration(IGamepadDriver gamepadDriver, InputConfig config)
+ {
+ GamepadDriver = gamepadDriver;
+
+ _gamepad?.Dispose();
+
+ _id = config.Id;
+ _gamepad = GamepadDriver.GetGamepad(_id);
+ _isValid = _gamepad != null;
+
+ UpdateUserConfiguration(config);
+
+ return _isValid;
+ }
+
+ public void UpdateUserConfiguration(InputConfig config)
+ {
+ if (config is StandardControllerInputConfig controllerConfig)
+ {
+ bool needsMotionInputUpdate = _config == null || (_config is StandardControllerInputConfig oldControllerConfig &&
+ (oldControllerConfig.Motion.EnableMotion != controllerConfig.Motion.EnableMotion) &&
+ (oldControllerConfig.Motion.MotionBackend != controllerConfig.Motion.MotionBackend));
+
+ if (needsMotionInputUpdate)
+ {
+ UpdateMotionInput(controllerConfig.Motion);
+ }
+ }
+ else
+ {
+ // Non-controller doesn't have motions.
+ _leftMotionInput = null;
+ }
+
+ _config = config;
+
+ if (_isValid)
+ {
+ _gamepad.SetConfiguration(config);
+ }
+ }
+
+ private void UpdateMotionInput(MotionConfigController motionConfig)
+ {
+ if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
+ {
+ _leftMotionInput = new MotionInput();
+ }
+ else
+ {
+ _leftMotionInput = null;
+ }
+ }
+
+ public void Update()
+ {
+ if (_isValid && GamepadDriver != null)
+ {
+ State = _gamepad.GetMappedStateSnapshot();
+
+ if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Motion.EnableMotion)
+ {
+ if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.GamepadDriver)
+ {
+ if (_gamepad.Features.HasFlag(GamepadFeaturesFlag.Motion))
+ {
+ Vector3 accelerometer = _gamepad.GetMotionData(MotionInputId.Accelerometer);
+ Vector3 gyroscope = _gamepad.GetMotionData(MotionInputId.Gyroscope);
+
+ accelerometer = new Vector3(accelerometer.X, -accelerometer.Z, accelerometer.Y);
+ gyroscope = new Vector3(gyroscope.X, -gyroscope.Z, gyroscope.Y);
+
+ _leftMotionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
+
+ if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
+ {
+ _rightMotionInput = _leftMotionInput;
+ }
+ }
+ }
+ else if (controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook && controllerConfig.Motion is CemuHookMotionConfigController cemuControllerConfig)
+ {
+ int clientId = (int)controllerConfig.PlayerIndex;
+
+ // First of all ensure we are registered
+ _cemuHookClient.RegisterClient(clientId, cemuControllerConfig.DsuServerHost, cemuControllerConfig.DsuServerPort);
+
+ // Then request and retrieve the data
+ _cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot);
+ _cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _leftMotionInput);
+
+ if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
+ {
+ if (!cemuControllerConfig.MirrorInput)
+ {
+ _cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot);
+ _cemuHookClient.TryGetData(clientId, cemuControllerConfig.AltSlot, out _rightMotionInput);
+ }
+ else
+ {
+ _rightMotionInput = _leftMotionInput;
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ // Reset states
+ State = default;
+ _leftMotionInput = null;
+ }
+ }
+
+ public GamepadInput GetHLEInputState()
+ {
+ GamepadInput state = new GamepadInput();
+
+ // First update all buttons
+ foreach (HLEButtonMappingEntry entry in _hleButtonMapping)
+ {
+ if (State.IsPressed(entry.DriverInputId))
+ {
+ state.Buttons |= entry.HLEInput;
+ }
+ }
+
+ if (_gamepad is IKeyboard)
+ {
+ (float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
+ (float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
+
+ state.LStick = new JoystickPosition
+ {
+ Dx = ClampAxis(leftAxisX),
+ Dy = ClampAxis(leftAxisY)
+ };
+
+ state.RStick = new JoystickPosition
+ {
+ Dx = ClampAxis(rightAxisX),
+ Dy = ClampAxis(rightAxisY)
+ };
+ }
+ else if (_config is StandardControllerInputConfig controllerConfig)
+ {
+ (float leftAxisX, float leftAxisY) = State.GetStick(StickInputId.Left);
+ (float rightAxisX, float rightAxisY) = State.GetStick(StickInputId.Right);
+
+ state.LStick = ClampToCircle(ApplyDeadzone(leftAxisX, leftAxisY, controllerConfig.DeadzoneLeft), controllerConfig.RangeLeft);
+ state.RStick = ClampToCircle(ApplyDeadzone(rightAxisX, rightAxisY, controllerConfig.DeadzoneRight), controllerConfig.RangeRight);
+ }
+
+ return state;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static JoystickPosition ApplyDeadzone(float x, float y, float deadzone)
+ {
+ float magnitudeClamped = Math.Min(MathF.Sqrt(x * x + y * y), 1f);
+
+ if (magnitudeClamped <= deadzone)
+ {
+ return new JoystickPosition() {Dx = 0, Dy = 0};
+ }
+
+ return new JoystickPosition()
+ {
+ Dx = ClampAxis((x / magnitudeClamped) * ((magnitudeClamped - deadzone) / (1 - deadzone))),
+ Dy = ClampAxis((y / magnitudeClamped) * ((magnitudeClamped - deadzone) / (1 - deadzone)))
+ };
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static short ClampAxis(float value)
+ {
+ if (Math.Sign(value) < 0)
+ {
+ return (short)Math.Max(value * -short.MinValue, short.MinValue);
+ }
+
+ return (short)Math.Min(value * short.MaxValue, short.MaxValue);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static JoystickPosition ClampToCircle(JoystickPosition position, float range)
+ {
+ Vector2 point = new Vector2(position.Dx, position.Dy) * range;
+
+ if (point.Length() > short.MaxValue)
+ {
+ point = point / point.Length() * short.MaxValue;
+ }
+
+ return new JoystickPosition
+ {
+ Dx = (int)point.X,
+ Dy = (int)point.Y
+ };
+ }
+
+ public SixAxisInput GetHLEMotionState(bool isJoyconRightPair = false)
+ {
+ float[] orientationForHLE = new float[9];
+ Vector3 gyroscope;
+ Vector3 accelerometer;
+ Vector3 rotation;
+
+ MotionInput motionInput = _leftMotionInput;
+
+ if (isJoyconRightPair)
+ {
+ if (_rightMotionInput == null)
+ {
+ return default;
+ }
+
+ motionInput = _rightMotionInput;
+ }
+
+ if (motionInput != null)
+ {
+ gyroscope = Truncate(motionInput.Gyroscrope * 0.0027f, 3);
+ accelerometer = Truncate(motionInput.Accelerometer, 3);
+ rotation = Truncate(motionInput.Rotation * 0.0027f, 3);
+
+ Matrix4x4 orientation = motionInput.GetOrientation();
+
+ orientationForHLE[0] = Math.Clamp(orientation.M11, -1f, 1f);
+ orientationForHLE[1] = Math.Clamp(orientation.M12, -1f, 1f);
+ orientationForHLE[2] = Math.Clamp(orientation.M13, -1f, 1f);
+ orientationForHLE[3] = Math.Clamp(orientation.M21, -1f, 1f);
+ orientationForHLE[4] = Math.Clamp(orientation.M22, -1f, 1f);
+ orientationForHLE[5] = Math.Clamp(orientation.M23, -1f, 1f);
+ orientationForHLE[6] = Math.Clamp(orientation.M31, -1f, 1f);
+ orientationForHLE[7] = Math.Clamp(orientation.M32, -1f, 1f);
+ orientationForHLE[8] = Math.Clamp(orientation.M33, -1f, 1f);
+ }
+ else
+ {
+ gyroscope = new Vector3();
+ accelerometer = new Vector3();
+ rotation = new Vector3();
+ }
+
+ return new SixAxisInput()
+ {
+ Accelerometer = accelerometer,
+ Gyroscope = gyroscope,
+ Rotation = rotation,
+ Orientation = orientationForHLE
+ };
+ }
+
+ 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;
+ }
+
+ public KeyboardInput? GetHLEKeyboardInput()
+ {
+ if (_gamepad is IKeyboard keyboard)
+ {
+ KeyboardStateSnapshot keyboardState = keyboard.GetKeyboardStateSnapshot();
+
+ KeyboardInput hidKeyboard = new KeyboardInput
+ {
+ Modifier = 0,
+ Keys = new ulong[0x4]
+ };
+
+ foreach (HLEKeyboardMappingEntry entry in KeyMapping)
+ {
+ ulong value = keyboardState.IsPressed(entry.TargetKey) ? 1UL : 0UL;
+
+ hidKeyboard.Keys[entry.Target / 0x40] |= (value << (entry.Target % 0x40));
+ }
+
+ foreach (HLEKeyboardMappingEntry entry in KeyModifierMapping)
+ {
+ int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0;
+
+ hidKeyboard.Modifier |= value << entry.Target;
+ }
+
+ return hidKeyboard;
+ }
+
+ return null;
+ }
+
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _gamepad?.Dispose();
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+
+ public void UpdateRumble(ConcurrentQueue<(VibrationValue, VibrationValue)> queue)
+ {
+ if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
+ {
+ if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
+ {
+ VibrationValue leftVibrationValue = dualVibrationValue.Item1;
+ VibrationValue rightVibrationValue = dualVibrationValue.Item2;
+
+ float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
+ float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
+
+ _gamepad.Rumble(low, high, uint.MaxValue);
+
+ Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
+ $"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
+ $"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
+ $"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
+ $"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
+ $"--> ({low}, {high})");
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
new file mode 100644
index 00000000..5290ecbb
--- /dev/null
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -0,0 +1,320 @@
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.HLE.HOS.Services.Hid;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
+using Switch = Ryujinx.HLE.Switch;
+
+namespace Ryujinx.Input.HLE
+{
+ public class NpadManager : IDisposable
+ {
+ private CemuHookClient _cemuHookClient;
+
+ private object _lock = new object();
+
+ private bool _blockInputUpdates;
+
+ private const int MaxControllers = 9;
+
+ private NpadController[] _controllers;
+
+ private readonly IGamepadDriver _keyboardDriver;
+ private readonly IGamepadDriver _gamepadDriver;
+ private readonly IGamepadDriver _mouseDriver;
+ private bool _isDisposed;
+
+ private List<InputConfig> _inputConfig;
+ private bool _enableKeyboard;
+ private bool _enableMouse;
+ private Switch _device;
+
+ public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver, IGamepadDriver mouseDriver)
+ {
+ _controllers = new NpadController[MaxControllers];
+ _cemuHookClient = new CemuHookClient(this);
+
+ _keyboardDriver = keyboardDriver;
+ _gamepadDriver = gamepadDriver;
+ _mouseDriver = mouseDriver;
+ _inputConfig = new List<InputConfig>();
+
+ _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
+ _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
+ }
+
+ private void RefreshInputConfigForHLE()
+ {
+ lock (_lock)
+ {
+ List<InputConfig> validInputs = new List<InputConfig>();
+ foreach (var inputConfigEntry in _inputConfig)
+ {
+ if (_controllers[(int)inputConfigEntry.PlayerIndex] != null)
+ {
+ validInputs.Add(inputConfigEntry);
+ }
+ }
+
+ _device.Hid.RefreshInputConfig(validInputs);
+ }
+ }
+
+ private void HandleOnGamepadDisconnected(string obj)
+ {
+ // Force input reload
+ ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
+ }
+
+ private void HandleOnGamepadConnected(string id)
+ {
+ // Force input reload
+ ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
+ {
+ IGamepadDriver targetDriver = _gamepadDriver;
+
+ if (config is StandardControllerInputConfig)
+ {
+ targetDriver = _gamepadDriver;
+ }
+ else if (config is StandardKeyboardInputConfig)
+ {
+ targetDriver = _keyboardDriver;
+ }
+
+ Debug.Assert(targetDriver != null, "Unknown input configuration!");
+
+ if (controller.GamepadDriver != targetDriver || controller.Id != config.Id)
+ {
+ return controller.UpdateDriverConfiguration(targetDriver, config);
+ }
+ else
+ {
+ return controller.GamepadDriver != null;
+ }
+ }
+
+ public void ReloadConfiguration(List<InputConfig> inputConfig, bool enableKeyboard, bool enableMouse)
+ {
+ lock (_lock)
+ {
+ for (int i = 0; i < _controllers.Length; i++)
+ {
+ _controllers[i]?.Dispose();
+ _controllers[i] = null;
+ }
+
+ List<InputConfig> validInputs = new List<InputConfig>();
+
+ foreach (InputConfig inputConfigEntry in inputConfig)
+ {
+ NpadController controller = new NpadController(_cemuHookClient);
+
+ bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
+
+ if (!isValid)
+ {
+ controller.Dispose();
+ }
+ else
+ {
+ _controllers[(int)inputConfigEntry.PlayerIndex] = controller;
+ validInputs.Add(inputConfigEntry);
+ }
+ }
+
+ _inputConfig = inputConfig;
+ _enableKeyboard = enableKeyboard;
+ _enableMouse = enableMouse;
+
+ _device.Hid.RefreshInputConfig(validInputs);
+ }
+ }
+
+ public void UnblockInputUpdates()
+ {
+ lock (_lock)
+ {
+ _blockInputUpdates = false;
+ }
+ }
+
+ public void BlockInputUpdates()
+ {
+ lock (_lock)
+ {
+ _blockInputUpdates = true;
+ }
+ }
+
+ public void Initialize(Switch device, List<InputConfig> inputConfig, bool enableKeyboard, bool enableMouse)
+ {
+ _device = device;
+ _device.Configuration.RefreshInputConfig = RefreshInputConfigForHLE;
+
+ ReloadConfiguration(inputConfig, enableKeyboard, enableMouse);
+ }
+
+ public void Update(float aspectRatio = 1)
+ {
+ lock (_lock)
+ {
+ List<GamepadInput> hleInputStates = new List<GamepadInput>();
+ List<SixAxisInput> hleMotionStates = new List<SixAxisInput>(NpadDevices.MaxControllers);
+
+ KeyboardInput? hleKeyboardInput = null;
+
+ foreach (InputConfig inputConfig in _inputConfig)
+ {
+ GamepadInput inputState = default;
+ (SixAxisInput, SixAxisInput) motionState = default;
+
+ NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
+ Ryujinx.HLE.HOS.Services.Hid.PlayerIndex playerIndex = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex;
+
+ bool isJoyconPair = false;
+
+ // Do we allow input updates and is a controller connected?
+ if (!_blockInputUpdates && controller != null)
+ {
+ DriverConfigurationUpdate(ref controller, inputConfig);
+
+ controller.UpdateUserConfiguration(inputConfig);
+ controller.Update();
+ controller.UpdateRumble(_device.Hid.Npads.GetRumbleQueue(playerIndex));
+
+ inputState = controller.GetHLEInputState();
+
+ inputState.Buttons |= _device.Hid.UpdateStickButtons(inputState.LStick, inputState.RStick);
+
+ isJoyconPair = inputConfig.ControllerType == Common.Configuration.Hid.ControllerType.JoyconPair;
+
+ var altMotionState = isJoyconPair ? controller.GetHLEMotionState(true) : default;
+
+ motionState = (controller.GetHLEMotionState(), altMotionState);
+
+ if (_enableKeyboard)
+ {
+ hleKeyboardInput = controller.GetHLEKeyboardInput();
+ }
+ }
+ else
+ {
+ // Ensure that orientation isn't null
+ motionState.Item1.Orientation = new float[9];
+ }
+
+ inputState.PlayerId = playerIndex;
+ motionState.Item1.PlayerId = playerIndex;
+
+ hleInputStates.Add(inputState);
+ hleMotionStates.Add(motionState.Item1);
+
+ if (isJoyconPair && !motionState.Item2.Equals(default))
+ {
+ motionState.Item2.PlayerId = playerIndex;
+
+ hleMotionStates.Add(motionState.Item2);
+ }
+ }
+
+ _device.Hid.Npads.Update(hleInputStates);
+ _device.Hid.Npads.UpdateSixAxis(hleMotionStates);
+
+ if (hleKeyboardInput.HasValue)
+ {
+ _device.Hid.Keyboard.Update(hleKeyboardInput.Value);
+ }
+
+ if (_enableMouse)
+ {
+ var mouse = _mouseDriver.GetGamepad("0") as IMouse;
+
+ var mouseInput = IMouse.GetMouseStateSnapshot(mouse);
+
+ uint buttons = 0;
+
+ if (mouseInput.IsPressed(MouseButton.Button1))
+ {
+ buttons |= 1 << 0;
+ }
+
+ if (mouseInput.IsPressed(MouseButton.Button2))
+ {
+ buttons |= 1 << 1;
+ }
+
+ if (mouseInput.IsPressed(MouseButton.Button3))
+ {
+ buttons |= 1 << 2;
+ }
+
+ if (mouseInput.IsPressed(MouseButton.Button4))
+ {
+ buttons |= 1 << 3;
+ }
+
+ if (mouseInput.IsPressed(MouseButton.Button5))
+ {
+ buttons |= 1 << 4;
+ }
+
+ var position = IMouse.GetScreenPosition(mouseInput.Position, mouse.ClientSize, aspectRatio);
+
+ _device.Hid.Mouse.Update((int)position.X, (int)position.Y, buttons, (int)mouseInput.Scroll.X, (int)mouseInput.Scroll.Y, true);
+ }
+ else
+ {
+ _device.Hid.Mouse.Update(0, 0);
+ }
+
+ _device.TamperMachine.UpdateInput(hleInputStates);
+ }
+ }
+
+ internal InputConfig GetPlayerInputConfigByIndex(int index)
+ {
+ lock (_lock)
+ {
+ return _inputConfig.Find(x => x.PlayerIndex == (Ryujinx.Common.Configuration.Hid.PlayerIndex)index);
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ lock (_lock)
+ {
+ if (!_isDisposed)
+ {
+ _cemuHookClient.Dispose();
+
+ _gamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
+ _gamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
+
+ for (int i = 0; i < _controllers.Length; i++)
+ {
+ _controllers[i]?.Dispose();
+ }
+
+ _isDisposed = true;
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/HLE/TouchScreenManager.cs b/src/Ryujinx.Input/HLE/TouchScreenManager.cs
new file mode 100644
index 00000000..e4b0f8fc
--- /dev/null
+++ b/src/Ryujinx.Input/HLE/TouchScreenManager.cs
@@ -0,0 +1,99 @@
+using Ryujinx.HLE;
+using Ryujinx.HLE.HOS.Services.Hid;
+using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.TouchScreen;
+using System;
+
+namespace Ryujinx.Input.HLE
+{
+ public class TouchScreenManager : IDisposable
+ {
+ private readonly IMouse _mouse;
+ private Switch _device;
+ private bool _wasClicking;
+
+ public TouchScreenManager(IMouse mouse)
+ {
+ _mouse = mouse;
+ }
+
+ public void Initialize(Switch device)
+ {
+ _device = device;
+ }
+
+ public bool Update(bool isFocused, bool isClicking = false, float aspectRatio = 0)
+ {
+ if (!isFocused || (!_wasClicking && !isClicking))
+ {
+ // In case we lost focus, send the end touch.
+ if (_wasClicking && !isClicking)
+ {
+ MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse);
+ var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio);
+
+ TouchPoint currentPoint = new TouchPoint
+ {
+ Attribute = TouchAttribute.End,
+
+ X = (uint)touchPosition.X,
+ Y = (uint)touchPosition.Y,
+
+ // Placeholder values till more data is acquired
+ DiameterX = 10,
+ DiameterY = 10,
+ Angle = 90
+ };
+
+ _device.Hid.Touchscreen.Update(currentPoint);
+
+ }
+
+ _wasClicking = false;
+
+ _device.Hid.Touchscreen.Update();
+
+ return false;
+ }
+
+ if (aspectRatio > 0)
+ {
+ MouseStateSnapshot snapshot = IMouse.GetMouseStateSnapshot(_mouse);
+ var touchPosition = IMouse.GetScreenPosition(snapshot.Position, _mouse.ClientSize, aspectRatio);
+
+ TouchAttribute attribute = TouchAttribute.None;
+
+ if (!_wasClicking && isClicking)
+ {
+ attribute = TouchAttribute.Start;
+ }
+ else if (_wasClicking && !isClicking)
+ {
+ attribute = TouchAttribute.End;
+ }
+
+ TouchPoint currentPoint = new TouchPoint
+ {
+ Attribute = attribute,
+
+ X = (uint)touchPosition.X,
+ Y = (uint)touchPosition.Y,
+
+ // Placeholder values till more data is acquired
+ DiameterX = 10,
+ DiameterY = 10,
+ Angle = 90
+ };
+
+ _device.Hid.Touchscreen.Update(currentPoint);
+
+ _wasClicking = isClicking;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Dispose() { }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/IGamepad.cs b/src/Ryujinx.Input/IGamepad.cs
new file mode 100644
index 00000000..c83ad5f8
--- /dev/null
+++ b/src/Ryujinx.Input/IGamepad.cs
@@ -0,0 +1,122 @@
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Memory;
+using System;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent an emulated gamepad.
+ /// </summary>
+ public interface IGamepad : IDisposable
+ {
+ /// <summary>
+ /// Features supported by the gamepad.
+ /// </summary>
+ GamepadFeaturesFlag Features { get; }
+
+ /// <summary>
+ /// Unique Id of the gamepad.
+ /// </summary>
+ string Id { get; }
+
+ /// <summary>
+ /// The name of the gamepad.
+ /// </summary>
+ string Name { get; }
+
+ /// <summary>
+ /// True if the gamepad is connected.
+ /// </summary>
+ bool IsConnected { get; }
+
+ /// <summary>
+ /// Check if a given input button is pressed on the gamepad.
+ /// </summary>
+ /// <param name="inputId">The button id</param>
+ /// <returns>True if the given button is pressed on the gamepad</returns>
+ bool IsPressed(GamepadButtonInputId inputId);
+
+ /// <summary>
+ /// Get the values of a given input joystick on the gamepad.
+ /// </summary>
+ /// <param name="inputId">The stick id</param>
+ /// <returns>The values of the given input joystick on the gamepad</returns>
+ (float, float) GetStick(StickInputId inputId);
+
+ /// <summary>
+ /// Get the values of a given motion sensors on the gamepad.
+ /// </summary>
+ /// <param name="inputId">The motion id</param>
+ /// <returns> The values of the given motion sensors on the gamepad.</returns>
+ Vector3 GetMotionData(MotionInputId inputId);
+
+ /// <summary>
+ /// Configure the threshold of the triggers on the gamepad.
+ /// </summary>
+ /// <param name="triggerThreshold">The threshold value for the triggers on the gamepad</param>
+ void SetTriggerThreshold(float triggerThreshold);
+
+ /// <summary>
+ /// Set the configuration of the gamepad.
+ /// </summary>
+ /// <remarks>This expect config to be in the format expected by the driver</remarks>
+ /// <param name="configuration">The configuration of the gamepad</param>
+ void SetConfiguration(InputConfig configuration);
+
+ /// <summary>
+ /// Starts a rumble effect on the gamepad.
+ /// </summary>
+ /// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
+ /// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
+ /// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
+ void Rumble(float lowFrequency, float highFrequency, uint durationMs);
+
+ /// <summary>
+ /// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
+ /// </summary>
+ /// <returns>A remapped snaphost of the state of the gamepad.</returns>
+ GamepadStateSnapshot GetMappedStateSnapshot();
+
+ /// <summary>
+ /// Get a snaphost of the state of the gamepad.
+ /// </summary>
+ /// <returns>A snaphost of the state of the gamepad.</returns>
+ GamepadStateSnapshot GetStateSnapshot();
+
+ /// <summary>
+ /// Get a snaphost of the state of a gamepad.
+ /// </summary>
+ /// <param name="gamepad">The gamepad to do a snapshot of</param>
+ /// <returns>A snaphost of the state of the gamepad.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static GamepadStateSnapshot GetStateSnapshot(IGamepad gamepad)
+ {
+ // NOTE: Update Array size if JoystickInputId is changed.
+ Array3<Array2<float>> joysticksState = default;
+
+ for (StickInputId inputId = StickInputId.Left; inputId < StickInputId.Count; inputId++)
+ {
+ (float state0, float state1) = gamepad.GetStick(inputId);
+
+ Array2<float> state = default;
+
+ state[0] = state0;
+ state[1] = state1;
+
+ joysticksState[(int)inputId] = state;
+ }
+
+ // NOTE: Update Array size if GamepadInputId is changed.
+ Array28<bool> buttonsState = default;
+
+ for (GamepadButtonInputId inputId = GamepadButtonInputId.A; inputId < GamepadButtonInputId.Count; inputId++)
+ {
+ buttonsState[(int)inputId] = gamepad.IsPressed(inputId);
+ }
+
+ return new GamepadStateSnapshot(joysticksState, buttonsState);
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs
new file mode 100644
index 00000000..792aef00
--- /dev/null
+++ b/src/Ryujinx.Input/IGamepadDriver.cs
@@ -0,0 +1,37 @@
+using System;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent an emulated gamepad driver used to provide input in the emulator.
+ /// </summary>
+ public interface IGamepadDriver : IDisposable
+ {
+ /// <summary>
+ /// The name of the driver
+ /// </summary>
+ string DriverName { get; }
+
+ /// <summary>
+ /// The unique ids of the gamepads connected.
+ /// </summary>
+ ReadOnlySpan<string> GamepadsIds { get; }
+
+ /// <summary>
+ /// Event triggered when a gamepad is connected.
+ /// </summary>
+ event Action<string> OnGamepadConnected;
+
+ /// <summary>
+ /// Event triggered when a gamepad is disconnected.
+ /// </summary>
+ event Action<string> OnGamepadDisconnected;
+
+ /// <summary>
+ /// Open a gampad by its unique id.
+ /// </summary>
+ /// <param name="id">The unique id of the gamepad</param>
+ /// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
+ IGamepad GetGamepad(string id);
+ }
+}
diff --git a/src/Ryujinx.Input/IKeyboard.cs b/src/Ryujinx.Input/IKeyboard.cs
new file mode 100644
index 00000000..506ec099
--- /dev/null
+++ b/src/Ryujinx.Input/IKeyboard.cs
@@ -0,0 +1,41 @@
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent an emulated keyboard.
+ /// </summary>
+ public interface IKeyboard : IGamepad
+ {
+ /// <summary>
+ /// Check if a given key is pressed on the keyboard.
+ /// </summary>
+ /// <param name="key">The key</param>
+ /// <returns>True if the given key is pressed on the keyboard</returns>
+ bool IsPressed(Key key);
+
+ /// <summary>
+ /// Get a snaphost of the state of the keyboard.
+ /// </summary>
+ /// <returns>A snaphost of the state of the keyboard.</returns>
+ KeyboardStateSnapshot GetKeyboardStateSnapshot();
+
+ /// <summary>
+ /// Get a snaphost of the state of a keyboard.
+ /// </summary>
+ /// <param name="keyboard">The keyboard to do a snapshot of</param>
+ /// <returns>A snaphost of the state of the keyboard.</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static KeyboardStateSnapshot GetStateSnapshot(IKeyboard keyboard)
+ {
+ bool[] keysState = new bool[(int)Key.Count];
+
+ for (Key key = 0; key < Key.Count; key++)
+ {
+ keysState[(int)key] = keyboard.IsPressed(key);
+ }
+
+ return new KeyboardStateSnapshot(keysState);
+ }
+ }
+}
diff --git a/src/Ryujinx.Input/IMouse.cs b/src/Ryujinx.Input/IMouse.cs
new file mode 100644
index 00000000..fde150fc
--- /dev/null
+++ b/src/Ryujinx.Input/IMouse.cs
@@ -0,0 +1,104 @@
+using System.Drawing;
+using System.Numerics;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent an emulated mouse.
+ /// </summary>
+ public interface IMouse : IGamepad
+ {
+ private const int SwitchPanelWidth = 1280;
+ private const int SwitchPanelHeight = 720;
+
+ /// <summary>
+ /// Check if a given button is pressed on the mouse.
+ /// </summary>
+ /// <param name="button">The button</param>
+ /// <returns>True if the given button is pressed on the mouse</returns>
+ bool IsButtonPressed(MouseButton button);
+
+ /// <summary>
+ /// Get the position of the mouse in the client.
+ /// </summary>
+ Vector2 GetPosition();
+
+ /// <summary>
+ /// Get the mouse scroll delta.
+ /// </summary>
+ Vector2 GetScroll();
+
+ /// <summary>
+ /// Get the client size.
+ /// </summary>
+ Size ClientSize { get; }
+
+ /// <summary>
+ /// Get the button states of the mouse.
+ /// </summary>
+ bool[] Buttons { get; }
+
+ /// <summary>
+ /// Get a snaphost of the state of a mouse.
+ /// </summary>
+ /// <param name="mouse">The mouse to do a snapshot of</param>
+ /// <returns>A snaphost of the state of the mouse.</returns>
+ public static MouseStateSnapshot GetMouseStateSnapshot(IMouse mouse)
+ {
+ bool[] buttons = new bool[(int)MouseButton.Count];
+
+ mouse.Buttons.CopyTo(buttons, 0);
+
+ return new MouseStateSnapshot(buttons, mouse.GetPosition(), mouse.GetScroll());
+ }
+
+ /// <summary>
+ /// Get the position of a mouse on screen relative to the app's view
+ /// </summary>
+ /// <param name="mousePosition">The position of the mouse in the client</param>
+ /// <param name="clientSize">The size of the client</param>
+ /// <param name="aspectRatio">The aspect ratio of the view</param>
+ /// <returns>A snaphost of the state of the mouse.</returns>
+ public static Vector2 GetScreenPosition(Vector2 mousePosition, Size clientSize, float aspectRatio)
+ {
+ float mouseX = mousePosition.X;
+ float mouseY = mousePosition.Y;
+
+ float aspectWidth = SwitchPanelHeight * aspectRatio;
+
+ int screenWidth = clientSize.Width;
+ int screenHeight = clientSize.Height;
+
+ if (clientSize.Width > clientSize.Height * aspectWidth / SwitchPanelHeight)
+ {
+ screenWidth = (int)(clientSize.Height * aspectWidth) / SwitchPanelHeight;
+ }
+ else
+ {
+ screenHeight = (clientSize.Width * SwitchPanelHeight) / (int)aspectWidth;
+ }
+
+ int startX = (clientSize.Width - screenWidth) >> 1;
+ int startY = (clientSize.Height - screenHeight) >> 1;
+
+ int endX = startX + screenWidth;
+ int endY = startY + screenHeight;
+
+ if (mouseX >= startX &&
+ mouseY >= startY &&
+ mouseX < endX &&
+ mouseY < endY)
+ {
+ int screenMouseX = (int)mouseX - startX;
+ int screenMouseY = (int)mouseY - startY;
+
+ mouseX = (screenMouseX * (int)aspectWidth) / screenWidth;
+ mouseY = (screenMouseY * SwitchPanelHeight) / screenHeight;
+
+ return new Vector2(mouseX, mouseY);
+ }
+
+ return new Vector2();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Key.cs b/src/Ryujinx.Input/Key.cs
new file mode 100644
index 00000000..5fa04484
--- /dev/null
+++ b/src/Ryujinx.Input/Key.cs
@@ -0,0 +1,142 @@
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent a key from a keyboard.
+ /// </summary>
+ public enum Key
+ {
+ Unknown,
+ ShiftLeft,
+ ShiftRight,
+ ControlLeft,
+ ControlRight,
+ AltLeft,
+ AltRight,
+ WinLeft,
+ WinRight,
+ Menu,
+ F1,
+ F2,
+ F3,
+ F4,
+ F5,
+ F6,
+ F7,
+ F8,
+ F9,
+ F10,
+ F11,
+ F12,
+ F13,
+ F14,
+ F15,
+ F16,
+ F17,
+ F18,
+ F19,
+ F20,
+ F21,
+ F22,
+ F23,
+ F24,
+ F25,
+ F26,
+ F27,
+ F28,
+ F29,
+ F30,
+ F31,
+ F32,
+ F33,
+ F34,
+ F35,
+ Up,
+ Down,
+ Left,
+ Right,
+ Enter,
+ Escape,
+ Space,
+ Tab,
+ BackSpace,
+ Insert,
+ Delete,
+ PageUp,
+ PageDown,
+ Home,
+ End,
+ CapsLock,
+ ScrollLock,
+ PrintScreen,
+ Pause,
+ NumLock,
+ Clear,
+ Keypad0,
+ Keypad1,
+ Keypad2,
+ Keypad3,
+ Keypad4,
+ Keypad5,
+ Keypad6,
+ Keypad7,
+ Keypad8,
+ Keypad9,
+ KeypadDivide,
+ KeypadMultiply,
+ KeypadSubtract,
+ KeypadAdd,
+ KeypadDecimal,
+ KeypadEnter,
+ A,
+ B,
+ C,
+ D,
+ E,
+ F,
+ G,
+ H,
+ I,
+ J,
+ K,
+ L,
+ M,
+ N,
+ O,
+ P,
+ Q,
+ R,
+ S,
+ T,
+ U,
+ V,
+ W,
+ X,
+ Y,
+ Z,
+ Number0,
+ Number1,
+ Number2,
+ Number3,
+ Number4,
+ Number5,
+ Number6,
+ Number7,
+ Number8,
+ Number9,
+ Tilde,
+ Grave,
+ Minus,
+ Plus,
+ BracketLeft,
+ BracketRight,
+ Semicolon,
+ Quote,
+ Comma,
+ Period,
+ Slash,
+ BackSlash,
+ Unbound,
+
+ Count
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/KeyboardStateSnapshot.cs b/src/Ryujinx.Input/KeyboardStateSnapshot.cs
new file mode 100644
index 00000000..da77a461
--- /dev/null
+++ b/src/Ryujinx.Input/KeyboardStateSnapshot.cs
@@ -0,0 +1,29 @@
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// A snapshot of a <see cref="IKeyboard"/>.
+ /// </summary>
+ public class KeyboardStateSnapshot
+ {
+ private bool[] _keysState;
+
+ /// <summary>
+ /// Create a new <see cref="KeyboardStateSnapshot"/>.
+ /// </summary>
+ /// <param name="keysState">The keys state</param>
+ public KeyboardStateSnapshot(bool[] keysState)
+ {
+ _keysState = keysState;
+ }
+
+ /// <summary>
+ /// Check if a given key is pressed.
+ /// </summary>
+ /// <param name="key">The key</param>
+ /// <returns>True if the given key is pressed</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsPressed(Key key) => _keysState[(int)key];
+ }
+}
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Client.cs b/src/Ryujinx.Input/Motion/CemuHook/Client.cs
new file mode 100644
index 00000000..4498b8ca
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Client.cs
@@ -0,0 +1,475 @@
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Memory;
+using Ryujinx.Input.HLE;
+using Ryujinx.Input.Motion.CemuHook.Protocol;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Hashing;
+using System.Net;
+using System.Net.Sockets;
+using System.Numerics;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Input.Motion.CemuHook
+{
+ 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 readonly bool[] _clientErrorStatus = new bool[Enum.GetValues<PlayerIndex>().Length];
+ private readonly long[] _clientRetryTimer = new long[Enum.GetValues<PlayerIndex>().Length];
+ private NpadManager _npadManager;
+
+ public Client(NpadManager npadManager)
+ {
+ _npadManager = npadManager;
+ _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();
+ }
+ catch (SocketException socketException)
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to dispose motion client. Error: {socketException.ErrorCode}");
+ }
+ }
+
+ _hosts.Clear();
+ _clients.Clear();
+ _motionData.Clear();
+ }
+ }
+
+ public void RegisterClient(int player, string host, int port)
+ {
+ if (_clients.ContainsKey(player) || !CanConnect(player))
+ {
+ return;
+ }
+
+ lock (_clients)
+ {
+ if (_clients.ContainsKey(player) || !CanConnect(player))
+ {
+ return;
+ }
+
+ UdpClient client = null;
+
+ try
+ {
+ IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse(host), port);
+
+ client = new UdpClient(host, port);
+
+ _clients.Add(player, client);
+ _hosts.Add(player, endPoint);
+
+ _active = true;
+
+ Task.Run(() =>
+ {
+ ReceiveLoop(player);
+ });
+ }
+ catch (FormatException formatException)
+ {
+ if (!_clientErrorStatus[player])
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {formatException.Message}");
+
+ _clientErrorStatus[player] = true;
+ }
+ }
+ catch (SocketException socketException)
+ {
+ if (!_clientErrorStatus[player])
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to connect to motion source at {host}:{port}. Error: {socketException.ErrorCode}");
+
+ _clientErrorStatus[player] = true;
+ }
+
+ RemoveClient(player);
+
+ client?.Dispose();
+
+ SetRetryTimer(player);
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to register motion client. Error: {exception.Message}");
+
+ _clientErrorStatus[player] = true;
+
+ RemoveClient(player);
+
+ client?.Dispose();
+
+ SetRetryTimer(player);
+ }
+ }
+ }
+
+ public bool TryGetData(int player, int slot, out MotionInput input)
+ {
+ lock (_motionData)
+ {
+ if (_motionData.ContainsKey(player))
+ {
+ if (_motionData[player].TryGetValue(slot, out input))
+ {
+ return true;
+ }
+ }
+ }
+
+ input = null;
+
+ return false;
+ }
+
+ private void RemoveClient(int clientId)
+ {
+ _clients?.Remove(clientId);
+
+ _hosts?.Remove(clientId);
+ }
+
+ 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 socketException)
+ {
+ if (!_clientErrorStatus[clientId])
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to send data request to motion source at {_client.Client.RemoteEndPoint}. Error: {socketException.ErrorCode}");
+ }
+
+ _clientErrorStatus[clientId] = true;
+
+ RemoveClient(clientId);
+
+ _client?.Dispose();
+
+ SetRetryTimer(clientId);
+ }
+ catch (ObjectDisposedException)
+ {
+ _clientErrorStatus[clientId] = true;
+
+ RemoveClient(clientId);
+
+ _client?.Dispose();
+
+ SetRetryTimer(clientId);
+ }
+ }
+ }
+ }
+
+ private byte[] Receive(int clientId, int timeout = 0)
+ {
+ if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
+ {
+ if (_client != null && _client.Client != null && _client.Client.Connected)
+ {
+ _client.Client.ReceiveTimeout = timeout;
+
+ var result = _client?.Receive(ref endPoint);
+
+ if (result.Length > 0)
+ {
+ _clientErrorStatus[clientId] = false;
+ }
+
+ return result;
+ }
+ }
+
+ throw new Exception($"Client {clientId} is not registered.");
+ }
+
+ private void SetRetryTimer(int clientId)
+ {
+ var elapsedMs = PerformanceCounter.ElapsedMilliseconds;
+
+ _clientRetryTimer[clientId] = elapsedMs;
+ }
+
+ private void ResetRetryTimer(int clientId)
+ {
+ _clientRetryTimer[clientId] = 0;
+ }
+
+ private bool CanConnect(int clientId)
+ {
+ return _clientRetryTimer[clientId] == 0 || PerformanceCounter.ElapsedMilliseconds - 5000 > _clientRetryTimer[clientId];
+ }
+
+ public void ReceiveLoop(int clientId)
+ {
+ if (_hosts.TryGetValue(clientId, out IPEndPoint endPoint) && _clients.TryGetValue(clientId, out UdpClient _client))
+ {
+ if (_client != null && _client.Client != null && _client.Client.Connected)
+ {
+ try
+ {
+ while (_active)
+ {
+ byte[] data = Receive(clientId);
+
+ if (data.Length == 0)
+ {
+ continue;
+ }
+
+ Task.Run(() => HandleResponse(data, clientId));
+ }
+ }
+ catch (SocketException socketException)
+ {
+ if (!_clientErrorStatus[clientId])
+ {
+ Logger.Warning?.PrintMsg(LogClass.Hid, $"Unable to receive data from motion source at {endPoint}. Error: {socketException.ErrorCode}");
+ }
+
+ _clientErrorStatus[clientId] = true;
+
+ RemoveClient(clientId);
+
+ _client?.Dispose();
+
+ SetRetryTimer(clientId);
+ }
+ catch (ObjectDisposedException)
+ {
+ _clientErrorStatus[clientId] = true;
+
+ RemoveClient(clientId);
+
+ _client?.Dispose();
+
+ SetRetryTimer(clientId);
+ }
+ }
+ }
+ }
+
+ public void HandleResponse(byte[] data, int clientId)
+ {
+ ResetRetryTimer(clientId);
+
+ MessageType type = (MessageType)BitConverter.ToUInt32(data.AsSpan().Slice(16, 4));
+
+ data = data.AsSpan()[16..].ToArray();
+
+ using MemoryStream stream = new MemoryStream(data);
+ using BinaryReader reader = new BinaryReader(stream);
+
+ 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 = _npadManager.GetPlayerInputConfigByIndex(clientId);
+
+ lock (_motionData)
+ {
+ // Sanity check the configuration state and remove client if needed if needed.
+ if (config is StandardControllerInputConfig controllerConfig &&
+ controllerConfig.Motion.EnableMotion &&
+ controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook &&
+ controllerConfig.Motion is CemuHookMotionConfigController cemuHookConfig)
+ {
+ int slot = inputData.Shared.Slot;
+
+ if (_motionData.ContainsKey(clientId))
+ {
+ if (_motionData[clientId].ContainsKey(slot))
+ {
+ MotionInput previousData = _motionData[clientId][slot];
+
+ previousData.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
+ }
+ else
+ {
+ MotionInput input = new MotionInput();
+
+ input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
+
+ _motionData[clientId].Add(slot, input);
+ }
+ }
+ else
+ {
+ MotionInput input = new MotionInput();
+
+ input.Update(accelerometer, gyroscrope, timestamp, cemuHookConfig.Sensitivity, (float)cemuHookConfig.GyroDeadzone);
+
+ _motionData.Add(clientId, new Dictionary<int, MotionInput>() { { slot, input } });
+ }
+ }
+ else
+ {
+ RemoveClient(clientId);
+ }
+ }
+ break;
+ }
+ }
+
+ public void RequestInfo(int clientId, int slot)
+ {
+ if (!_active)
+ {
+ return;
+ }
+
+ Header header = GenerateHeader(clientId);
+
+ using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
+ using (BinaryWriter writer = new BinaryWriter(stream))
+ {
+ writer.WriteStruct(header);
+
+ ControllerInfoRequest request = new ControllerInfoRequest()
+ {
+ Type = MessageType.Info,
+ PortsCount = 4
+ };
+
+ request.PortIndices[0] = (byte)slot;
+
+ writer.WriteStruct(request);
+
+ header.Length = (ushort)(stream.Length - 16);
+
+ writer.Seek(6, SeekOrigin.Begin);
+ writer.Write(header.Length);
+
+ Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
+
+ writer.Seek(8, SeekOrigin.Begin);
+ writer.Write(header.Crc32.AsSpan());
+
+ byte[] data = stream.ToArray();
+
+ Send(data, clientId);
+ }
+ }
+
+ public unsafe void RequestData(int clientId, int slot)
+ {
+ if (!_active)
+ {
+ return;
+ }
+
+ Header header = GenerateHeader(clientId);
+
+ using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
+ using (BinaryWriter writer = new BinaryWriter(stream))
+ {
+ writer.WriteStruct(header);
+
+ ControllerDataRequest request = new ControllerDataRequest()
+ {
+ Type = MessageType.Data,
+ Slot = (byte)slot,
+ SubscriberType = SubscriberType.Slot
+ };
+
+ writer.WriteStruct(request);
+
+ header.Length = (ushort)(stream.Length - 16);
+
+ writer.Seek(6, SeekOrigin.Begin);
+ writer.Write(header.Length);
+
+ Crc32.Hash(stream.ToArray(), header.Crc32.AsSpan());
+
+ writer.Seek(8, SeekOrigin.Begin);
+ writer.Write(header.Crc32.AsSpan());
+
+ byte[] data = stream.ToArray();
+
+ Send(data, clientId);
+ }
+ }
+
+ private Header GenerateHeader(int clientId)
+ {
+ Header header = new Header()
+ {
+ Id = (uint)clientId,
+ MagicString = Magic,
+ Version = Version,
+ Length = 0
+ };
+
+ return header;
+ }
+
+ public void Dispose()
+ {
+ _active = false;
+
+ CloseClients();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs
new file mode 100644
index 00000000..7fb72344
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs
@@ -0,0 +1,47 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Input.Motion.CemuHook.Protocol
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ struct ControllerDataRequest
+ {
+ public MessageType Type;
+ public SubscriberType SubscriberType;
+ public byte Slot;
+ public Array6<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;
+
+ public Array6<byte> Touch1;
+ public Array6<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,
+ Slot,
+ Mac
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs
new file mode 100644
index 00000000..63d4524a
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs
@@ -0,0 +1,20 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Input.Motion.CemuHook.Protocol
+{
+ [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;
+ public Array4<byte> PortIndices;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs
new file mode 100644
index 00000000..57f58ff0
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs
@@ -0,0 +1,15 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Input.Motion.CemuHook.Protocol
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct Header
+ {
+ public uint MagicString;
+ public ushort Version;
+ public ushort Length;
+ public Array4<byte> Crc32;
+ public uint Id;
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs
new file mode 100644
index 00000000..de1e5e90
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Input.Motion.CemuHook.Protocol
+{
+ public enum MessageType : uint
+ {
+ Protocol = 0x100000,
+ Info,
+ Data
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs b/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs
new file mode 100644
index 00000000..e2e1ee9b
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs
@@ -0,0 +1,51 @@
+using Ryujinx.Common.Memory;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Input.Motion.CemuHook.Protocol
+{
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct SharedResponse
+ {
+ public MessageType Type;
+ public byte Slot;
+ public SlotState State;
+ public DeviceModelType ModelType;
+ public ConnectionType ConnectionType;
+
+ public Array6<byte> MacAddress;
+ public BatteryStatus BatteryStatus;
+ }
+
+ public enum SlotState : byte
+ {
+ Disconnected,
+ Reserved,
+ Connected
+ }
+
+ public enum DeviceModelType : byte
+ {
+ None,
+ PartialGyro,
+ FullGyro
+ }
+
+ public enum ConnectionType : byte
+ {
+ None,
+ USB,
+ Bluetooth
+ }
+
+ public enum BatteryStatus : byte
+ {
+ NA,
+ Dying,
+ Low,
+ Medium,
+ High,
+ Full,
+ Charging,
+ Charged
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/MotionInput.cs b/src/Ryujinx.Input/Motion/MotionInput.cs
new file mode 100644
index 00000000..1923d9cb
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/MotionInput.cs
@@ -0,0 +1,65 @@
+using Ryujinx.Input.Motion;
+using System;
+using System.Numerics;
+
+namespace Ryujinx.Input
+{
+ 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;
+
+ 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)
+ {
+ 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);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Motion/MotionSensorFilter.cs b/src/Ryujinx.Input/Motion/MotionSensorFilter.cs
new file mode 100644
index 00000000..440fa7ac
--- /dev/null
+++ b/src/Ryujinx.Input/Motion/MotionSensorFilter.cs
@@ -0,0 +1,162 @@
+using System.Numerics;
+
+namespace Ryujinx.Input.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
+ 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;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/MotionInputId.cs b/src/Ryujinx.Input/MotionInputId.cs
new file mode 100644
index 00000000..3176a987
--- /dev/null
+++ b/src/Ryujinx.Input/MotionInputId.cs
@@ -0,0 +1,25 @@
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent a motion sensor on a gamepad.
+ /// </summary>
+ public enum MotionInputId : byte
+ {
+ /// <summary>
+ /// Invalid.
+ /// </summary>
+ Invalid,
+
+ /// <summary>
+ /// Accelerometer.
+ /// </summary>
+ /// <remarks>Values are in m/s^2</remarks>
+ Accelerometer,
+
+ /// <summary>
+ /// Gyroscope.
+ /// </summary>
+ /// <remarks>Values are in degrees</remarks>
+ Gyroscope
+ }
+}
diff --git a/src/Ryujinx.Input/MouseButton.cs b/src/Ryujinx.Input/MouseButton.cs
new file mode 100644
index 00000000..ab764216
--- /dev/null
+++ b/src/Ryujinx.Input/MouseButton.cs
@@ -0,0 +1,16 @@
+namespace Ryujinx.Input
+{
+ public enum MouseButton : byte
+ {
+ Button1,
+ Button2,
+ Button3,
+ Button4,
+ Button5,
+ Button6,
+ Button7,
+ Button8,
+ Button9,
+ Count
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/MouseStateSnapshot.cs b/src/Ryujinx.Input/MouseStateSnapshot.cs
new file mode 100644
index 00000000..ddfdebc6
--- /dev/null
+++ b/src/Ryujinx.Input/MouseStateSnapshot.cs
@@ -0,0 +1,45 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// A snapshot of a <see cref="IMouse"/>.
+ /// </summary>
+ public class MouseStateSnapshot
+ {
+ private bool[] _buttonState;
+
+ /// <summary>
+ /// The position of the mouse cursor
+ /// </summary>
+ public Vector2 Position { get; }
+
+ /// <summary>
+ /// The scroll delta of the mouse
+ /// </summary>
+ public Vector2 Scroll { get; }
+
+ /// <summary>
+ /// Create a new <see cref="MouseStateSnapshot"/>.
+ /// </summary>
+ /// <param name="buttonState">The button state</param>
+ /// <param name="position">The position of the cursor</param>
+ /// <param name="scroll">The scroll delta</param>
+ public MouseStateSnapshot(bool[] buttonState, Vector2 position, Vector2 scroll)
+ {
+ _buttonState = buttonState;
+
+ Position = position;
+ Scroll = scroll;
+ }
+
+ /// <summary>
+ /// Check if a given button is pressed.
+ /// </summary>
+ /// <param name="button">The button</param>
+ /// <returns>True if the given button is pressed</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool IsPressed(MouseButton button) => _buttonState[(int)button];
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx.Input/Ryujinx.Input.csproj b/src/Ryujinx.Input/Ryujinx.Input.csproj
new file mode 100644
index 00000000..df462734
--- /dev/null
+++ b/src/Ryujinx.Input/Ryujinx.Input.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="System.IO.Hashing" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
+ <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/src/Ryujinx.Input/StickInputId.cs b/src/Ryujinx.Input/StickInputId.cs
new file mode 100644
index 00000000..fc9d8043
--- /dev/null
+++ b/src/Ryujinx.Input/StickInputId.cs
@@ -0,0 +1,14 @@
+namespace Ryujinx.Input
+{
+ /// <summary>
+ /// Represent a joystick from a gamepad.
+ /// </summary>
+ public enum StickInputId : byte
+ {
+ Unbound,
+ Left,
+ Right,
+
+ Count
+ }
+}