Ryujinx/src/Ryujinx.Input/HLE/NpadManager.cs
riperiperi 90455a05e6
Input: Improve controller identification (#6029)
* Input: Improve controller identification

Controllers were identified before by a combination of their _global_ index in the list of controllers and their GUID. The problem is, disconnecting and reconnecting a controller can change its global index; the controller can appear at the end. This would give it another ID, and the controller would need to be reconfigured.

This happened to me a lot with a switch pro controller and a USB game controller, it was essentially random which appeared first. Now, it consistently detects them.

This PR changes the controller identification to be a combination of an index of controllers with the same GUID (generally 0), and its GUID. It also reworks managing the list of controllers to properly consider instance IDs.

This also changes the NpadManager to attempt to reuse old controllers when refreshing input configuration, which can prevent input from going dead for seconds whenever a controller connects or disconnects (and the switch pro controller just entirely dying).

Testing with different controller types, OS and Avalonia is welcome. Remember that the target is connecting a ton of controllers, and pulling/reconnecting them.

* Remove double empty line
2024-01-22 17:02:44 -03:00

353 lines
12 KiB
C#

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.Linq;
using System.Runtime.CompilerServices;
using CemuHookClient = Ryujinx.Input.Motion.CemuHook.Client;
using ControllerType = Ryujinx.Common.Configuration.Hid.ControllerType;
using PlayerIndex = Ryujinx.HLE.HOS.Services.Hid.PlayerIndex;
using Switch = Ryujinx.HLE.Switch;
namespace Ryujinx.Input.HLE
{
public class NpadManager : IDisposable
{
private readonly CemuHookClient _cemuHookClient;
private readonly object _lock = new();
private bool _blockInputUpdates;
private const int MaxControllers = 9;
private readonly 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();
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
lock (_lock)
{
// Forcibly disconnect any controllers with this ID.
for (int i = 0; i < _controllers.Length; i++)
{
if (_controllers[i]?.Id == obj)
{
_controllers[i]?.Dispose();
_controllers[i] = null;
}
}
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);
}
return controller.GamepadDriver != null;
}
public void ReloadConfiguration(List<InputConfig> inputConfig, bool enableKeyboard, bool enableMouse)
{
lock (_lock)
{
NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = new();
foreach (InputConfig inputConfigEntry in inputConfig)
{
NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex;
if (oldControllers[index] != null)
{
// Try reuse the existing controller.
controller = oldControllers[index];
oldControllers[index] = null;
}
else
{
controller = new(_cemuHookClient);
}
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
if (!isValid)
{
_controllers[index] = null;
controller.Dispose();
}
else
{
_controllers[index] = controller;
validInputs.Add(inputConfigEntry);
}
}
for (int i = 0; i < oldControllers.Length; i++)
{
// Disconnect any controllers that weren't reused by the new configuration.
oldControllers[i]?.Dispose();
oldControllers[i] = null;
}
_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<SixAxisInput> hleMotionStates = new(NpadDevices.MaxControllers);
KeyboardInput? hleKeyboardInput = null;
foreach (InputConfig inputConfig in _inputConfig)
{
GamepadInput inputState = default;
(SixAxisInput, SixAxisInput) motionState = default;
NpadController controller = _controllers[(int)inputConfig.PlayerIndex];
PlayerIndex playerIndex = (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 == 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 == (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()
{
GC.SuppressFinalize(this);
Dispose(true);
}
}
}