|
|
@@ -1,74 +1,118 @@
|
|
|
-using System;
|
|
|
+using Ryujinx.Common;
|
|
|
using Ryujinx.Common.Logging;
|
|
|
+using Ryujinx.Common.Memory;
|
|
|
using Ryujinx.HLE.HOS.Kernel.Threading;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
|
|
|
namespace Ryujinx.HLE.HOS.Services.Hid
|
|
|
{
|
|
|
public class NpadDevices : BaseDevice
|
|
|
{
|
|
|
- internal NpadJoyHoldType JoyHold = NpadJoyHoldType.Vertical;
|
|
|
+ private const BatteryCharge DefaultBatteryCharge = BatteryCharge.Percent100;
|
|
|
+
|
|
|
+ private const int NoMatchNotifyFrequencyMs = 2000;
|
|
|
+ private int _activeCount;
|
|
|
+ private long _lastNotifyTimestamp;
|
|
|
+
|
|
|
+ public const int MaxControllers = 9; // Players 1-8 and Handheld
|
|
|
+ private ControllerType[] _configuredTypes;
|
|
|
+ private KEvent[] _styleSetUpdateEvents;
|
|
|
+ private bool[] _supportedPlayers;
|
|
|
+
|
|
|
+ internal NpadJoyHoldType JoyHold { get; set; }
|
|
|
internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
|
|
|
+ internal ControllerType SupportedStyleSets { get; set; }
|
|
|
|
|
|
- private enum FilterState
|
|
|
+ public NpadDevices(Switch device, bool active = true) : base(device, active)
|
|
|
{
|
|
|
- Unconfigured = 0,
|
|
|
- Configured = 1,
|
|
|
- Accepted = 2
|
|
|
+ _configuredTypes = new ControllerType[MaxControllers];
|
|
|
+
|
|
|
+ SupportedStyleSets = ControllerType.Handheld | ControllerType.JoyconPair |
|
|
|
+ ControllerType.JoyconLeft | ControllerType.JoyconRight |
|
|
|
+ ControllerType.ProController;
|
|
|
+
|
|
|
+ _supportedPlayers = new bool[MaxControllers];
|
|
|
+ _supportedPlayers.AsSpan().Fill(true);
|
|
|
+
|
|
|
+ _styleSetUpdateEvents = new KEvent[MaxControllers];
|
|
|
+ for (int i = 0; i < _styleSetUpdateEvents.Length; ++i)
|
|
|
+ {
|
|
|
+ _styleSetUpdateEvents[i] = new KEvent(_device.System.KernelContext);
|
|
|
+ }
|
|
|
+
|
|
|
+ _activeCount = 0;
|
|
|
+
|
|
|
+ JoyHold = NpadJoyHoldType.Vertical;
|
|
|
}
|
|
|
|
|
|
- private struct NpadConfig
|
|
|
+ internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
|
|
|
{
|
|
|
- public ControllerType ConfiguredType;
|
|
|
- public FilterState State;
|
|
|
+ return ref _styleSetUpdateEvents[(int)player];
|
|
|
}
|
|
|
|
|
|
- private const int _maxControllers = 9; // Players1-8 and Handheld
|
|
|
- private NpadConfig[] _configuredNpads;
|
|
|
+ internal void ClearSupportedPlayers()
|
|
|
+ {
|
|
|
+ _supportedPlayers.AsSpan().Clear();
|
|
|
+ }
|
|
|
|
|
|
- private ControllerType _supportedStyleSets = ControllerType.ProController |
|
|
|
- ControllerType.JoyconPair |
|
|
|
- ControllerType.JoyconLeft |
|
|
|
- ControllerType.JoyconRight |
|
|
|
- ControllerType.Handheld;
|
|
|
+ internal void SetSupportedPlayer(PlayerIndex player, bool supported = true)
|
|
|
+ {
|
|
|
+ _supportedPlayers[(int)player] = supported;
|
|
|
+ }
|
|
|
|
|
|
- public ControllerType SupportedStyleSets
|
|
|
+ internal IEnumerable<PlayerIndex> GetSupportedPlayers()
|
|
|
{
|
|
|
- get => _supportedStyleSets;
|
|
|
- set
|
|
|
+ for (int i = 0; i < _supportedPlayers.Length; ++i)
|
|
|
{
|
|
|
- if (_supportedStyleSets != value) // Deal with spamming
|
|
|
+ if (_supportedPlayers[i])
|
|
|
{
|
|
|
- _supportedStyleSets = value;
|
|
|
- MatchControllers();
|
|
|
+ yield return (PlayerIndex)i;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public PlayerIndex PrimaryController { get; set; } = PlayerIndex.Unknown;
|
|
|
+ public bool Validate(int playerMin, int playerMax, ControllerType acceptedTypes, out int configuredCount, out PlayerIndex primaryIndex)
|
|
|
+ {
|
|
|
+ primaryIndex = PlayerIndex.Unknown;
|
|
|
+ configuredCount = 0;
|
|
|
|
|
|
- private KEvent[] _styleSetUpdateEvents;
|
|
|
+ for (int i = 0; i < MaxControllers; ++i)
|
|
|
+ {
|
|
|
+ ControllerType npad = _configuredTypes[i];
|
|
|
|
|
|
- private static readonly Array3<BatteryCharge> _fullBattery;
|
|
|
+ if (npad == ControllerType.Handheld && _device.System.State.DockedMode)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
|
|
|
- public NpadDevices(Switch device, bool active = true) : base(device, active)
|
|
|
- {
|
|
|
- _configuredNpads = new NpadConfig[_maxControllers];
|
|
|
+ ControllerType currentType = _device.Hid.SharedMemory.Npads[i].Header.Type;
|
|
|
|
|
|
- _styleSetUpdateEvents = new KEvent[_maxControllers];
|
|
|
+ if (currentType != ControllerType.None && (npad & acceptedTypes) != 0 && _supportedPlayers[i])
|
|
|
+ {
|
|
|
+ configuredCount++;
|
|
|
+ if (primaryIndex == PlayerIndex.Unknown)
|
|
|
+ {
|
|
|
+ primaryIndex = (PlayerIndex)i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- for (int i = 0; i < _styleSetUpdateEvents.Length; ++i)
|
|
|
+ if (configuredCount < playerMin || configuredCount > playerMax || primaryIndex == PlayerIndex.Unknown)
|
|
|
{
|
|
|
- _styleSetUpdateEvents[i] = new KEvent(_device.System.KernelContext);
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- _fullBattery[0] = _fullBattery[1] = _fullBattery[2] = BatteryCharge.Percent100;
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- public void AddControllers(params ControllerConfig[] configs)
|
|
|
+ public void Configure(params ControllerConfig[] configs)
|
|
|
{
|
|
|
+ _configuredTypes = new ControllerType[MaxControllers];
|
|
|
+
|
|
|
for (int i = 0; i < configs.Length; ++i)
|
|
|
{
|
|
|
- PlayerIndex player = configs[i].Player;
|
|
|
+ PlayerIndex player = configs[i].Player;
|
|
|
ControllerType controllerType = configs[i].Type;
|
|
|
|
|
|
if (player > PlayerIndex.Handheld)
|
|
|
@@ -81,77 +125,87 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|
|
player = PlayerIndex.Handheld;
|
|
|
}
|
|
|
|
|
|
- _configuredNpads[(int)player] = new NpadConfig { ConfiguredType = controllerType, State = FilterState.Configured };
|
|
|
- }
|
|
|
+ _configuredTypes[(int)player] = controllerType;
|
|
|
|
|
|
- MatchControllers();
|
|
|
+ Logger.Info?.Print(LogClass.Hid, $"Configured Controller {controllerType} to {player}");
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private void MatchControllers()
|
|
|
+ public void Update(IList<GamepadInput> states)
|
|
|
{
|
|
|
- PrimaryController = PlayerIndex.Unknown;
|
|
|
+ Remap();
|
|
|
+
|
|
|
+ UpdateAllEntries();
|
|
|
+
|
|
|
+ // Update configured inputs
|
|
|
+ for (int i = 0; i < states.Count; ++i)
|
|
|
+ {
|
|
|
+ UpdateInput(states[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- for (int i = 0; i < _configuredNpads.Length; ++i)
|
|
|
+ private void Remap()
|
|
|
+ {
|
|
|
+ // Remap/Init if necessary
|
|
|
+ for (int i = 0; i < MaxControllers; ++i)
|
|
|
{
|
|
|
- ref NpadConfig config = ref _configuredNpads[i];
|
|
|
+ ControllerType config = _configuredTypes[i];
|
|
|
|
|
|
- if (config.State == FilterState.Unconfigured)
|
|
|
+ // Remove Handheld config when Docked
|
|
|
+ if (config == ControllerType.Handheld && _device.System.State.DockedMode)
|
|
|
{
|
|
|
- continue; // Ignore unconfigured
|
|
|
+ config = ControllerType.None;
|
|
|
}
|
|
|
|
|
|
- if ((config.ConfiguredType & _supportedStyleSets) == 0)
|
|
|
+ // Auto-remap ProController and JoyconPair
|
|
|
+ if (config == ControllerType.JoyconPair && (SupportedStyleSets & ControllerType.JoyconPair) == 0 && (SupportedStyleSets & ControllerType.ProController) != 0)
|
|
|
{
|
|
|
- Logger.Warning?.Print(LogClass.Hid, $"ControllerType {config.ConfiguredType} (connected to {(PlayerIndex)i}) not supported by game. Removing...");
|
|
|
-
|
|
|
- config.State = FilterState.Configured;
|
|
|
- _device.Hid.SharedMemory.Npads[i] = new ShMemNpad(); // Zero it
|
|
|
+ config = ControllerType.ProController;
|
|
|
+ }
|
|
|
+ else if (config == ControllerType.ProController && (SupportedStyleSets & ControllerType.ProController) == 0 && (SupportedStyleSets & ControllerType.JoyconPair) != 0)
|
|
|
+ {
|
|
|
+ config = ControllerType.JoyconPair;
|
|
|
+ }
|
|
|
|
|
|
- continue;
|
|
|
+ // Check StyleSet and PlayerSet
|
|
|
+ if ((config & SupportedStyleSets) == 0 || !_supportedPlayers[i])
|
|
|
+ {
|
|
|
+ config = ControllerType.None;
|
|
|
}
|
|
|
|
|
|
- InitController((PlayerIndex)i, config.ConfiguredType);
|
|
|
+ SetupNpad((PlayerIndex)i, config);
|
|
|
}
|
|
|
|
|
|
- // Couldn't find any matching configuration. Reassign to something that works.
|
|
|
- if (PrimaryController == PlayerIndex.Unknown)
|
|
|
+ if (_activeCount == 0 && PerformanceCounter.ElapsedMilliseconds > _lastNotifyTimestamp + NoMatchNotifyFrequencyMs)
|
|
|
{
|
|
|
- ControllerType[] npadsTypeList = (ControllerType[])Enum.GetValues(typeof(ControllerType));
|
|
|
-
|
|
|
- // Skip None Type
|
|
|
- for (int i = 1; i < npadsTypeList.Length; ++i)
|
|
|
- {
|
|
|
- ControllerType controllerType = npadsTypeList[i];
|
|
|
- if ((controllerType & _supportedStyleSets) != 0)
|
|
|
- {
|
|
|
- Logger.Warning?.Print(LogClass.Hid, $"No matching controllers found. Reassigning input as ControllerType {controllerType}...");
|
|
|
-
|
|
|
- InitController(controllerType == ControllerType.Handheld ? PlayerIndex.Handheld : PlayerIndex.Player1, controllerType);
|
|
|
-
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Logger.Error?.Print(LogClass.Hid, "Couldn't find any appropriate controller.");
|
|
|
+ Logger.Warning?.Print(LogClass.Hid, $"No matching controllers found. Application requests '{SupportedStyleSets}' on '{string.Join(", ", GetSupportedPlayers())}'");
|
|
|
+ _lastNotifyTimestamp = PerformanceCounter.ElapsedMilliseconds;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player)
|
|
|
+ private void SetupNpad(PlayerIndex player, ControllerType type)
|
|
|
{
|
|
|
- return ref _styleSetUpdateEvents[(int)player];
|
|
|
- }
|
|
|
+ ref ShMemNpad controller = ref _device.Hid.SharedMemory.Npads[(int)player];
|
|
|
|
|
|
- private void InitController(PlayerIndex player, ControllerType type)
|
|
|
- {
|
|
|
- if (type == ControllerType.Handheld)
|
|
|
+ ControllerType oldType = controller.Header.Type;
|
|
|
+
|
|
|
+ if (oldType == type)
|
|
|
{
|
|
|
- player = PlayerIndex.Handheld;
|
|
|
+ return; // Already configured
|
|
|
}
|
|
|
|
|
|
- ref ShMemNpad controller = ref _device.Hid.SharedMemory.Npads[(int)player];
|
|
|
-
|
|
|
controller = new ShMemNpad(); // Zero it
|
|
|
|
|
|
+ if (type == ControllerType.None)
|
|
|
+ {
|
|
|
+ _styleSetUpdateEvents[(int)player].ReadableEvent.Signal(); // Signal disconnect
|
|
|
+ _activeCount--;
|
|
|
+
|
|
|
+ Logger.Info?.Print(LogClass.Hid, $"Disconnected Controller {oldType} from {player}");
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// TODO: Allow customizing colors at config
|
|
|
NpadStateHeader defaultHeader = new NpadStateHeader
|
|
|
{
|
|
|
@@ -168,7 +222,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|
|
NpadSystemProperties.PowerInfo1Connected |
|
|
|
NpadSystemProperties.PowerInfo2Connected;
|
|
|
|
|
|
- controller.BatteryState = _fullBattery;
|
|
|
+ controller.BatteryState.ToSpan().Fill(DefaultBatteryCharge);
|
|
|
|
|
|
switch (type)
|
|
|
{
|
|
|
@@ -217,19 +271,13 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|
|
|
|
|
controller.Header = defaultHeader;
|
|
|
|
|
|
- if (PrimaryController == PlayerIndex.Unknown)
|
|
|
- {
|
|
|
- PrimaryController = player;
|
|
|
- }
|
|
|
-
|
|
|
- _configuredNpads[(int)player].State = FilterState.Accepted;
|
|
|
-
|
|
|
_styleSetUpdateEvents[(int)player].ReadableEvent.Signal();
|
|
|
+ _activeCount++;
|
|
|
|
|
|
- Logger.Info?.Print(LogClass.Hid, $"Connected ControllerType {type} to PlayerIndex {player}");
|
|
|
+ Logger.Info?.Print(LogClass.Hid, $"Connected Controller {type} to {player}");
|
|
|
}
|
|
|
|
|
|
- private static NpadLayoutsIndex ControllerTypeToLayout(ControllerType controllerType)
|
|
|
+ private static NpadLayoutsIndex ControllerTypeToNpadLayout(ControllerType controllerType)
|
|
|
=> controllerType switch
|
|
|
{
|
|
|
ControllerType.ProController => NpadLayoutsIndex.ProController,
|
|
|
@@ -241,43 +289,28 @@ namespace Ryujinx.HLE.HOS.Services.Hid
|
|
|
_ => NpadLayoutsIndex.SystemExternal
|
|
|
};
|
|
|
|
|
|
- public void SetGamepadsInput(params GamepadInput[] states)
|
|
|
+ private void UpdateInput(GamepadInput state)
|
|
|
{
|
|
|
- UpdateAllEntries();
|
|
|
-
|
|
|
- for (int i = 0; i < states.Length; ++i)
|
|
|
- {
|
|
|
- SetGamepadState(states[i].PlayerId, states[i].Buttons, states[i].LStick, states[i].RStick);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private void SetGamepadState(PlayerIndex player, ControllerKeys buttons,
|
|
|
- JoystickPosition leftJoystick, JoystickPosition rightJoystick)
|
|
|
- {
|
|
|
- if (player == PlayerIndex.Auto)
|
|
|
- {
|
|
|
- player = PrimaryController;
|
|
|
- }
|
|
|
-
|
|
|
- if (player == PlayerIndex.Unknown)
|
|
|
+ if (state.PlayerId == PlayerIndex.Unknown)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (_configuredNpads[(int)player].State != FilterState.Accepted)
|
|
|
+ ref ShMemNpad currentNpad = ref _device.Hid.SharedMemory.Npads[(int)state.PlayerId];
|
|
|
+
|
|
|
+ if (currentNpad.Header.Type == ControllerType.None)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- ref ShMemNpad currentNpad = ref _device.Hid.SharedMemory.Npads[(int)player];
|
|
|
- ref NpadLayout currentLayout = ref currentNpad.Layouts[(int)ControllerTypeToLayout(currentNpad.Header.Type)];
|
|
|
+ ref NpadLayout currentLayout = ref currentNpad.Layouts[(int)ControllerTypeToNpadLayout(currentNpad.Header.Type)];
|
|
|
ref NpadState currentEntry = ref currentLayout.Entries[(int)currentLayout.Header.LatestEntry];
|
|
|
|
|
|
- currentEntry.Buttons = buttons;
|
|
|
- currentEntry.LStickX = leftJoystick.Dx;
|
|
|
- currentEntry.LStickY = leftJoystick.Dy;
|
|
|
- currentEntry.RStickX = rightJoystick.Dx;
|
|
|
- currentEntry.RStickY = rightJoystick.Dy;
|
|
|
+ currentEntry.Buttons = state.Buttons;
|
|
|
+ currentEntry.LStickX = state.LStick.Dx;
|
|
|
+ currentEntry.LStickY = state.LStick.Dy;
|
|
|
+ currentEntry.RStickX = state.RStick.Dx;
|
|
|
+ currentEntry.RStickY = state.RStick.Dy;
|
|
|
|
|
|
// Mirror data to Default layout just in case
|
|
|
ref NpadLayout mainLayout = ref currentNpad.Layouts[(int)NpadLayoutsIndex.SystemExternal];
|