Move solution and projects to src

This commit is contained in:
TSR Berry
2023-04-08 01:22:00 +02:00
committed by Mary
parent cd124bda58
commit cee7121058
3466 changed files with 55 additions and 55 deletions

View File

@ -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}";
}
}
}
}

View File

@ -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();
}
}

View File

@ -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 : "";
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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})");
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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() { }
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

104
src/Ryujinx.Input/IMouse.cs Normal file
View File

@ -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();
}
}
}

142
src/Ryujinx.Input/Key.cs Normal file
View File

@ -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
}
}

View File

@ -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];
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Input.Motion.CemuHook.Protocol
{
public enum MessageType : uint
{
Protocol = 0x100000,
Info,
Data
}
}

View File

@ -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
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,16 @@
namespace Ryujinx.Input
{
public enum MouseButton : byte
{
Button1,
Button2,
Button3,
Button4,
Button5,
Button6,
Button7,
Button8,
Button9,
Count
}
}

View File

@ -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];
}
}

View File

@ -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>

View File

@ -0,0 +1,14 @@
namespace Ryujinx.Input
{
/// <summary>
/// Represent a joystick from a gamepad.
/// </summary>
public enum StickInputId : byte
{
Unbound,
Left,
Right,
Count
}
}