diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.Input | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.Input')
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 + } +} |
