diff options
Diffstat (limited to 'Ryujinx.Input')
25 files changed, 2486 insertions, 0 deletions
diff --git a/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs b/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs new file mode 100644 index 00000000..e3aaf8b1 --- /dev/null +++ b/Ryujinx.Input/Assigner/GamepadButtonAssigner.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System; +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/Ryujinx.Input/Assigner/IButtonAssigner.cs b/Ryujinx.Input/Assigner/IButtonAssigner.cs new file mode 100644 index 00000000..021736df --- /dev/null +++ b/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/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs b/Ryujinx.Input/Assigner/KeyboardKeyAssigner.cs new file mode 100644 index 00000000..23ae3655 --- /dev/null +++ b/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/Ryujinx.Input/GamepadButtonInputId.cs b/Ryujinx.Input/GamepadButtonInputId.cs new file mode 100644 index 00000000..d1e4b9ac --- /dev/null +++ b/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/Ryujinx.Input/GamepadFeaturesFlag.cs b/Ryujinx.Input/GamepadFeaturesFlag.cs new file mode 100644 index 00000000..87310a32 --- /dev/null +++ b/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/Ryujinx.Input/GamepadStateSnapshot.cs b/Ryujinx.Input/GamepadStateSnapshot.cs new file mode 100644 index 00000000..cf3e3e28 --- /dev/null +++ b/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/Ryujinx.Input/HLE/InputManager.cs b/Ryujinx.Input/HLE/InputManager.cs new file mode 100644 index 00000000..277e8ec2 --- /dev/null +++ b/Ryujinx.Input/HLE/InputManager.cs @@ -0,0 +1,35 @@ +using System; + +namespace Ryujinx.Input.HLE +{ + public class InputManager : IDisposable + { + public IGamepadDriver KeyboardDriver { get; private set; } + public IGamepadDriver GamepadDriver { get; private set; } + + public InputManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) + { + KeyboardDriver = keyboardDriver; + GamepadDriver = gamepadDriver; + } + + public NpadManager CreateNpadManager() + { + return new NpadManager(KeyboardDriver, GamepadDriver); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + KeyboardDriver?.Dispose(); + GamepadDriver?.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + } + } +} diff --git a/Ryujinx.Input/HLE/NpadController.cs b/Ryujinx.Input/HLE/NpadController.cs new file mode 100644 index 00000000..d3553d64 --- /dev/null +++ b/Ryujinx.Input/HLE/NpadController.cs @@ -0,0 +1,496 @@ +using Ryujinx.Common; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.HLE.HOS.Services.Hid; +using System; +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 _motionInput; + + 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. + _motionInput = null; + } + + _config = config; + + if (_isValid) + { + _gamepad.SetConfiguration(config); + } + } + + private void UpdateMotionInput(MotionConfigController motionConfig) + { + if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook) + { + _motionInput = new MotionInput(); + } + else + { + _motionInput = 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); + + _motionInput.Update(accelerometer, gyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone); + } + } + 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 data + _cemuHookClient.RequestData(clientId, cemuControllerConfig.Slot); + + if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair && !cemuControllerConfig.MirrorInput) + { + _cemuHookClient.RequestData(clientId, cemuControllerConfig.AltSlot); + } + + // Finally, get motion input data + _cemuHookClient.TryGetData(clientId, cemuControllerConfig.Slot, out _motionInput); + } + } + } + else + { + // Reset states + State = default; + _motionInput = null; + } + } + + private static short ClampAxis(float value) + { + if (value <= -short.MaxValue) + { + return -short.MaxValue; + } + else + { + return (short)(value * short.MaxValue); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static JoystickPosition ApplyDeadzone(float x, float y, float deadzone) + { + return new JoystickPosition + { + Dx = ClampAxis(MathF.Abs(x) > deadzone ? x : 0.0f), + Dy = ClampAxis(MathF.Abs(y) > deadzone ? y : 0.0f) + }; + } + + 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 = ApplyDeadzone(leftAxisX, leftAxisY, controllerConfig.DeadzoneLeft); + state.RStick = ApplyDeadzone(rightAxisX, rightAxisY, controllerConfig.DeadzoneRight); + } + + return state; + } + + public SixAxisInput GetHLEMotionState() + { + float[] orientationForHLE = new float[9]; + Vector3 gyroscope; + Vector3 accelerometer; + Vector3 rotation; + + 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 int[0x8] + }; + + foreach (HLEKeyboardMappingEntry entry in KeyMapping) + { + int value = keyboardState.IsPressed(entry.TargetKey) ? 1 : 0; + + hidKeyboard.Keys[entry.Target / 0x20] |= (value << (entry.Target % 0x20)); + } + + 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); + } + } +} diff --git a/Ryujinx.Input/HLE/NpadManager.cs b/Ryujinx.Input/HLE/NpadManager.cs new file mode 100644 index 00000000..fdb87f9b --- /dev/null +++ b/Ryujinx.Input/HLE/NpadManager.cs @@ -0,0 +1,222 @@ +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Configuration; +using Ryujinx.HLE.HOS; +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; + +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 bool _isDisposed; + + private List<InputConfig> _inputConfig; + + public NpadManager(IGamepadDriver keyboardDriver, IGamepadDriver gamepadDriver) + { + _controllers = new NpadController[MaxControllers]; + _cemuHookClient = new CemuHookClient(); + + _keyboardDriver = keyboardDriver; + _gamepadDriver = gamepadDriver; + _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value; + + _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + } + + private void HandleOnGamepadDisconnected(string obj) + { + // Force input reload + ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value); + } + + private void HandleOnGamepadConnected(string id) + { + // Force input reload + ReloadConfiguration(ConfigurationState.Instance.Hid.InputConfig.Value); + } + + [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) + { + lock (_lock) + { + for (int i = 0; i < _controllers.Length; i++) + { + _controllers[i]?.Dispose(); + _controllers[i] = null; + } + + 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; + } + } + + _inputConfig = inputConfig; + + // Enforce an update of the property that will be updated by HLE. + // TODO: Move that in the input manager maybe? + ConfigurationState.Instance.Hid.InputConfig.Value = inputConfig; + } + } + + public void UnblockInputUpdates() + { + lock (_lock) + { + _blockInputUpdates = false; + } + } + + public void BlockInputUpdates() + { + lock (_lock) + { + _blockInputUpdates = true; + } + } + + public void Update(Hid hleHid, TamperMachine tamperMachine) + { + lock (_lock) + { + List<GamepadInput> hleInputStates = new List<GamepadInput>(); + List<SixAxisInput> hleMotionStates = new List<SixAxisInput>(NpadDevices.MaxControllers); + + foreach (InputConfig inputConfig in _inputConfig) + { + GamepadInput inputState = default; + SixAxisInput motionState = default; + + NpadController controller = _controllers[(int)inputConfig.PlayerIndex]; + + // Do we allow input updates and is a controller connected? + if (!_blockInputUpdates && controller != null) + { + DriverConfigurationUpdate(ref controller, inputConfig); + + controller.UpdateUserConfiguration(inputConfig); + controller.Update(); + + inputState = controller.GetHLEInputState(); + + inputState.Buttons |= hleHid.UpdateStickButtons(inputState.LStick, inputState.RStick); + + motionState = controller.GetHLEMotionState(); + } + else + { + // Ensure that orientation isn't null + motionState.Orientation = new float[9]; + } + + inputState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex; + motionState.PlayerId = (Ryujinx.HLE.HOS.Services.Hid.PlayerIndex)inputConfig.PlayerIndex; + + hleInputStates.Add(inputState); + hleMotionStates.Add(motionState); + + if (ConfigurationState.Instance.Hid.EnableKeyboard) + { + KeyboardInput? hleKeyboardInput = controller.GetHLEKeyboardInput(); + + if (hleKeyboardInput.HasValue) + { + hleHid.Keyboard.Update(hleKeyboardInput.Value); + } + } + } + + hleHid.Npads.Update(hleInputStates); + hleHid.Npads.UpdateSixAxis(hleMotionStates); + tamperMachine.UpdateInput(hleInputStates); + } + } + + 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/Ryujinx.Input/IGamepad.cs b/Ryujinx.Input/IGamepad.cs new file mode 100644 index 00000000..cc788333 --- /dev/null +++ b/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 gampead. + /// </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/Ryujinx.Input/IGamepadDriver.cs b/Ryujinx.Input/IGamepadDriver.cs new file mode 100644 index 00000000..792aef00 --- /dev/null +++ b/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/Ryujinx.Input/IKeyboard.cs b/Ryujinx.Input/IKeyboard.cs new file mode 100644 index 00000000..506ec099 --- /dev/null +++ b/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/Ryujinx.Input/Key.cs b/Ryujinx.Input/Key.cs new file mode 100644 index 00000000..0cadf4ec --- /dev/null +++ b/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 + } +} diff --git a/Ryujinx.Input/KeyboardStateSnapshot.cs b/Ryujinx.Input/KeyboardStateSnapshot.cs new file mode 100644 index 00000000..da77a461 --- /dev/null +++ b/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/Ryujinx.Input/Motion/CemuHook/Client.cs b/Ryujinx.Input/Motion/CemuHook/Client.cs new file mode 100644 index 00000000..395bd0b3 --- /dev/null +++ b/Ryujinx.Input/Motion/CemuHook/Client.cs @@ -0,0 +1,473 @@ +using Force.Crc32; +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.Configuration; +using Ryujinx.Input.Motion.CemuHook.Protocol; +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.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(typeof(PlayerIndex)).Length]; + private readonly long[] _clientRetryTimer = new long[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(); + } + 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 = ConfigurationState.Instance.Hid.InputConfig.Value.Find(x => x.PlayerIndex == (PlayerIndex)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 = new MemoryStream()) + 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); + + header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); + + writer.Seek(8, SeekOrigin.Begin); + writer.Write(header.Crc32); + + 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 = new MemoryStream()) + 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); + + header.Crc32 = Crc32Algorithm.Compute(stream.ToArray()); + + writer.Seek(8, SeekOrigin.Begin); + writer.Write(header.Crc32); + + 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, + Crc32 = 0 + }; + + return header; + } + + public void Dispose() + { + _active = false; + + CloseClients(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs b/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs new file mode 100644 index 00000000..9ff8dc91 --- /dev/null +++ b/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerData.cs @@ -0,0 +1,51 @@ +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; + + [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, + Slot, + Mac + } +}
\ No newline at end of file diff --git a/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs b/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs new file mode 100644 index 00000000..d483633e --- /dev/null +++ b/Ryujinx.Input/Motion/CemuHook/Protocol/ControllerInfo.cs @@ -0,0 +1,21 @@ +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; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] PortIndices; + } +}
\ No newline at end of file diff --git a/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs b/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs new file mode 100644 index 00000000..94cf4bb6 --- /dev/null +++ b/Ryujinx.Input/Motion/CemuHook/Protocol/Header.cs @@ -0,0 +1,14 @@ +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 uint Crc32; + public uint Id; + } +}
\ No newline at end of file diff --git a/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs b/Ryujinx.Input/Motion/CemuHook/Protocol/MessageType.cs new file mode 100644 index 00000000..de1e5e90 --- /dev/null +++ b/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/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs b/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs new file mode 100644 index 00000000..0593286d --- /dev/null +++ b/Ryujinx.Input/Motion/CemuHook/Protocol/SharedResponse.cs @@ -0,0 +1,51 @@ +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; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public 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/Ryujinx.Input/Motion/MotionInput.cs b/Ryujinx.Input/Motion/MotionInput.cs new file mode 100644 index 00000000..d92c3d7f --- /dev/null +++ b/Ryujinx.Input/Motion/MotionInput.cs @@ -0,0 +1,86 @@ +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; + 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); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Input/Motion/MotionSensorFilter.cs b/Ryujinx.Input/Motion/MotionSensorFilter.cs new file mode 100644 index 00000000..440fa7ac --- /dev/null +++ b/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/Ryujinx.Input/MotionInputId.cs b/Ryujinx.Input/MotionInputId.cs new file mode 100644 index 00000000..3176a987 --- /dev/null +++ b/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/Ryujinx.Input/Ryujinx.Input.csproj b/Ryujinx.Input/Ryujinx.Input.csproj new file mode 100644 index 00000000..b2de3ac2 --- /dev/null +++ b/Ryujinx.Input/Ryujinx.Input.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Crc32.NET" Version="1.2.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" /> + <ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" /> + </ItemGroup> + +</Project> diff --git a/Ryujinx.Input/StickInputId.cs b/Ryujinx.Input/StickInputId.cs new file mode 100644 index 00000000..fc9d8043 --- /dev/null +++ b/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 + } +} |
