diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs index 39503157..4f806225 100644 --- a/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs @@ -32,7 +32,7 @@ namespace Ryujinx.HLE.HOS.Applets byte[] controllerSupportArgPrivate = _normalSession.Pop(); ControllerSupportArgPrivate privateArg = IApplet.ReadStruct(controllerSupportArgPrivate); - Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ArgPriv {privateArg.PrivateSize} {privateArg.ArgSize} {privateArg.Mode}" + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ArgPriv {privateArg.PrivateSize} {privateArg.ArgSize} {privateArg.Mode} " + $"HoldType:{(NpadJoyHoldType)privateArg.NpadJoyHoldType} StyleSets:{(ControllerType)privateArg.NpadStyleSet}"); if (privateArg.Mode != ControllerSupportMode.ShowControllerSupport) @@ -47,33 +47,57 @@ namespace Ryujinx.HLE.HOS.Applets ControllerSupportArgHeader argHeader; - if (privateArg.ArgSize == Marshal.SizeOf()) + if (privateArg.ArgSize == Marshal.SizeOf()) { - ControllerSupportArg arg = IApplet.ReadStruct(controllerSupportArg); + ControllerSupportArgV7 arg = IApplet.ReadStruct(controllerSupportArg); argHeader = arg.Header; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version 7 EnableExplainText={arg.EnableExplainText != 0}"); + // Read enable text here? + } + else if (privateArg.ArgSize == Marshal.SizeOf()) + { + ControllerSupportArgVPre7 arg = IApplet.ReadStruct(controllerSupportArg); + argHeader = arg.Header; + + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Pre-7 EnableExplainText={arg.EnableExplainText != 0}"); // Read enable text here? } else { - Logger.Stub?.PrintStub(LogClass.ServiceHid, $"Unknown revision of ControllerSupportArg."); + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Unknown"); argHeader = IApplet.ReadStruct(controllerSupportArg); // Read just the header } - Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet Arg {argHeader.PlayerCountMin} {argHeader.PlayerCountMax} {argHeader.EnableTakeOverConnection} {argHeader.EnableSingleMode}"); + int playerMin = argHeader.PlayerCountMin; + int playerMax = argHeader.PlayerCountMax; - // Currently, the only purpose of this applet is to help - // choose the primary input controller for the game - // TODO: Ideally should hook back to HID.Controller. When applet is called, can choose appropriate controller and attach to appropriate id. - if (argHeader.PlayerCountMin > 1) + Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet Arg {playerMin} {playerMax} {argHeader.EnableTakeOverConnection} {argHeader.EnableSingleMode}"); + + int configuredCount = 0; + PlayerIndex primaryIndex = PlayerIndex.Unknown; + while (!_system.Device.Hid.Npads.Validate(playerMin, playerMax, (ControllerType)privateArg.NpadStyleSet, out configuredCount, out primaryIndex)) { - Logger.Warning?.Print(LogClass.ServiceHid, "More than one controller was requested."); + ControllerAppletUiArgs uiArgs = new ControllerAppletUiArgs + { + PlayerCountMin = playerMin, + PlayerCountMax = playerMax, + SupportedStyles = (ControllerType)privateArg.NpadStyleSet, + SupportedPlayers = _system.Device.Hid.Npads.GetSupportedPlayers(), + IsDocked = _system.State.DockedMode + }; + + if (!_system.Device.UiHandler.DisplayMessageDialog(uiArgs)) + { + break; + } } ControllerSupportResultInfo result = new ControllerSupportResultInfo { - PlayerCount = 1, - SelectedId = (uint)GetNpadIdTypeFromIndex(_system.Device.Hid.Npads.PrimaryController) + PlayerCount = (sbyte)configuredCount, + SelectedId = (uint)GetNpadIdTypeFromIndex(primaryIndex) }; Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ReturnResult {result.PlayerCount} {result.SelectedId}"); diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs new file mode 100644 index 00000000..cc15a406 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerAppletUiArgs.cs @@ -0,0 +1,14 @@ +using Ryujinx.HLE.HOS.Services.Hid; +using System.Collections.Generic; + +namespace Ryujinx.HLE.HOS.Applets +{ + public struct ControllerAppletUiArgs + { + public int PlayerCountMin; + public int PlayerCountMax; + public ControllerType SupportedStyles; + public IEnumerable SupportedPlayers; + public bool IsDocked; + } +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs index 945f0ef6..8eaf1d44 100644 --- a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgHeader.cs @@ -1,6 +1,9 @@ +using System.Runtime.InteropServices; + namespace Ryujinx.HLE.HOS.Applets { #pragma warning disable CS0649 + [StructLayout(LayoutKind.Sequential, Pack=1)] struct ControllerSupportArgHeader { public sbyte PlayerCountMin; diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArg.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs similarity index 71% rename from Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArg.cs rename to Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs index 908e9049..a01e7c04 100644 --- a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArg.cs +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgV7.cs @@ -1,8 +1,11 @@ +using System.Runtime.InteropServices; + namespace Ryujinx.HLE.HOS.Applets { #pragma warning disable CS0649 // (8.0.0+ version) - unsafe struct ControllerSupportArg + [StructLayout(LayoutKind.Sequential, Pack=1)] + unsafe struct ControllerSupportArgV7 { public ControllerSupportArgHeader Header; public fixed uint IdentificationColor[8]; diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs new file mode 100644 index 00000000..6d46aea5 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportArgVPre7.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Applets +{ +#pragma warning disable CS0649 + // (1.0.0+ version) + [StructLayout(LayoutKind.Sequential, Pack=1)] + unsafe struct ControllerSupportArgVPre7 + { + public ControllerSupportArgHeader Header; + public fixed uint IdentificationColor[4]; + public byte EnableExplainText; + public fixed byte ExplainText[4 * 0x81]; + } +#pragma warning restore CS0649 +} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs index 09a19bf0..c213e592 100644 --- a/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs +++ b/Ryujinx.HLE/HOS/Applets/Controller/ControllerSupportResultInfo.cs @@ -1,6 +1,9 @@ +using System.Runtime.InteropServices; + namespace Ryujinx.HLE.HOS.Applets { #pragma warning disable CS0649 + [StructLayout(LayoutKind.Sequential, Pack=1)] unsafe struct ControllerSupportResultInfo { public sbyte PlayerCount; diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs index 22152e77..eab6256e 100644 --- a/Ryujinx.HLE/HOS/Horizon.cs +++ b/Ryujinx.HLE/HOS/Horizon.cs @@ -249,6 +249,9 @@ namespace Ryujinx.HLE.HOS AppletState.EnqueueMessage(MessageInfo.OperationModeChanged); AppletState.EnqueueMessage(MessageInfo.PerformanceModeChanged); SignalDisplayResolutionChange(); + + // Reconfigure controllers + Device.Hid.RefreshInputConfig(ConfigurationState.Instance.Hid.InputConfig.Value); } } diff --git a/Ryujinx.HLE/HOS/Services/Hid/Hid.cs b/Ryujinx.HLE/HOS/Services/Hid/Hid.cs index c355a050..2db67619 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Hid.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Hid.cs @@ -1,5 +1,7 @@ using Ryujinx.Common; using Ryujinx.HLE.Exceptions; +using Ryujinx.Common.Configuration.Hid; +using System.Collections.Generic; using System.Runtime.CompilerServices; namespace Ryujinx.HLE.HOS.Services.Hid @@ -65,6 +67,24 @@ namespace Ryujinx.HLE.HOS.Services.Hid Npads = new NpadDevices(_device, true); } + internal void RefreshInputConfig(List inputConfig) + { + ControllerConfig[] npadConfig = new ControllerConfig[inputConfig.Count]; + + for (int i = 0; i < npadConfig.Length; ++i) + { + npadConfig[i].Player = (PlayerIndex)inputConfig[i].PlayerIndex; + npadConfig[i].Type = (ControllerType)inputConfig[i].ControllerType; + } + + _device.Hid.Npads.Configure(npadConfig); + } + + internal void RefreshInputConfigEvent(object _, ReactiveEventArgs> args) + { + RefreshInputConfig(args.NewValue); + } + public ControllerKeys UpdateStickButtons(JoystickPosition leftStick, JoystickPosition rightStick) { ControllerKeys result = 0; diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs index 1c882887..334af975 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/HidDevices/NpadDevices.cs @@ -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; - internal bool SixAxisActive = false; // TODO: link to hidserver when implemented + private const BatteryCharge DefaultBatteryCharge = BatteryCharge.Percent100; - private enum FilterState - { - Unconfigured = 0, - Configured = 1, - Accepted = 2 - } - - private struct NpadConfig - { - public ControllerType ConfiguredType; - public FilterState State; - } - - private const int _maxControllers = 9; // Players1-8 and Handheld - private NpadConfig[] _configuredNpads; - - private ControllerType _supportedStyleSets = ControllerType.ProController | - ControllerType.JoyconPair | - ControllerType.JoyconLeft | - ControllerType.JoyconRight | - ControllerType.Handheld; - - public ControllerType SupportedStyleSets - { - get => _supportedStyleSets; - set - { - if (_supportedStyleSets != value) // Deal with spamming - { - _supportedStyleSets = value; - MatchControllers(); - } - } - } - - public PlayerIndex PrimaryController { get; set; } = PlayerIndex.Unknown; + 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; - private static readonly Array3 _fullBattery; + internal NpadJoyHoldType JoyHold { get; set; } + internal bool SixAxisActive = false; // TODO: link to hidserver when implemented + internal ControllerType SupportedStyleSets { get; set; } public NpadDevices(Switch device, bool active = true) : base(device, active) { - _configuredNpads = new NpadConfig[_maxControllers]; + _configuredTypes = new ControllerType[MaxControllers]; - _styleSetUpdateEvents = new KEvent[_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); } - _fullBattery[0] = _fullBattery[1] = _fullBattery[2] = BatteryCharge.Percent100; + _activeCount = 0; + + JoyHold = NpadJoyHoldType.Vertical; } - public void AddControllers(params ControllerConfig[] configs) + internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player) { + return ref _styleSetUpdateEvents[(int)player]; + } + + internal void ClearSupportedPlayers() + { + _supportedPlayers.AsSpan().Clear(); + } + + internal void SetSupportedPlayer(PlayerIndex player, bool supported = true) + { + _supportedPlayers[(int)player] = supported; + } + + internal IEnumerable GetSupportedPlayers() + { + for (int i = 0; i < _supportedPlayers.Length; ++i) + { + if (_supportedPlayers[i]) + { + yield return (PlayerIndex)i; + } + } + } + + public bool Validate(int playerMin, int playerMax, ControllerType acceptedTypes, out int configuredCount, out PlayerIndex primaryIndex) + { + primaryIndex = PlayerIndex.Unknown; + configuredCount = 0; + + for (int i = 0; i < MaxControllers; ++i) + { + ControllerType npad = _configuredTypes[i]; + + if (npad == ControllerType.Handheld && _device.System.State.DockedMode) + { + continue; + } + + ControllerType currentType = _device.Hid.SharedMemory.Npads[i].Header.Type; + + if (currentType != ControllerType.None && (npad & acceptedTypes) != 0 && _supportedPlayers[i]) + { + configuredCount++; + if (primaryIndex == PlayerIndex.Unknown) + { + primaryIndex = (PlayerIndex)i; + } + } + } + + if (configuredCount < playerMin || configuredCount > playerMax || primaryIndex == PlayerIndex.Unknown) + { + return false; + } + + return true; + } + + 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(); - } - - private void MatchControllers() - { - PrimaryController = PlayerIndex.Unknown; - - for (int i = 0; i < _configuredNpads.Length; ++i) - { - ref NpadConfig config = ref _configuredNpads[i]; - - if (config.State == FilterState.Unconfigured) - { - continue; // Ignore unconfigured - } - - if ((config.ConfiguredType & _supportedStyleSets) == 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 - - continue; - } - - InitController((PlayerIndex)i, config.ConfiguredType); - } - - // Couldn't find any matching configuration. Reassign to something that works. - if (PrimaryController == PlayerIndex.Unknown) - { - 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.Info?.Print(LogClass.Hid, $"Configured Controller {controllerType} to {player}"); } } - internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player) + public void Update(IList states) { - return ref _styleSetUpdateEvents[(int)player]; + Remap(); + + UpdateAllEntries(); + + // Update configured inputs + for (int i = 0; i < states.Count; ++i) + { + UpdateInput(states[i]); + } } - private void InitController(PlayerIndex player, ControllerType type) + private void Remap() { - if (type == ControllerType.Handheld) + // Remap/Init if necessary + for (int i = 0; i < MaxControllers; ++i) { - player = PlayerIndex.Handheld; + ControllerType config = _configuredTypes[i]; + + // Remove Handheld config when Docked + if (config == ControllerType.Handheld && _device.System.State.DockedMode) + { + config = ControllerType.None; + } + + // Auto-remap ProController and JoyconPair + if (config == ControllerType.JoyconPair && (SupportedStyleSets & ControllerType.JoyconPair) == 0 && (SupportedStyleSets & ControllerType.ProController) != 0) + { + config = ControllerType.ProController; + } + else if (config == ControllerType.ProController && (SupportedStyleSets & ControllerType.ProController) == 0 && (SupportedStyleSets & ControllerType.JoyconPair) != 0) + { + config = ControllerType.JoyconPair; + } + + // Check StyleSet and PlayerSet + if ((config & SupportedStyleSets) == 0 || !_supportedPlayers[i]) + { + config = ControllerType.None; + } + + SetupNpad((PlayerIndex)i, config); } + if (_activeCount == 0 && PerformanceCounter.ElapsedMilliseconds > _lastNotifyTimestamp + NoMatchNotifyFrequencyMs) + { + Logger.Warning?.Print(LogClass.Hid, $"No matching controllers found. Application requests '{SupportedStyleSets}' on '{string.Join(", ", GetSupportedPlayers())}'"); + _lastNotifyTimestamp = PerformanceCounter.ElapsedMilliseconds; + } + } + + private void SetupNpad(PlayerIndex player, ControllerType type) + { ref ShMemNpad controller = ref _device.Hid.SharedMemory.Npads[(int)player]; + ControllerType oldType = controller.Header.Type; + + if (oldType == type) + { + return; // Already configured + } + 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]; diff --git a/Ryujinx.HLE/HOS/Services/Hid/HidServer/HidUtils.cs b/Ryujinx.HLE/HOS/Services/Hid/HidServer/HidUtils.cs index c2cd8432..9db5b518 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/HidServer/HidUtils.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/HidServer/HidUtils.cs @@ -35,5 +35,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid.HidServer PlayerIndex.Unknown => NpadIdType.Unknown, _ => throw new ArgumentOutOfRangeException(nameof(index)) }; + + public static long GetLedPatternFromNpadId(NpadIdType npadIdType) + => npadIdType switch + { + NpadIdType.Player1 => 0b0001, + NpadIdType.Player2 => 0b0011, + NpadIdType.Player3 => 0b0111, + NpadIdType.Player4 => 0b1111, + NpadIdType.Player5 => 0b1001, + NpadIdType.Player6 => 0b0101, + NpadIdType.Player7 => 0b1101, + NpadIdType.Player8 => 0b0110, + _ => 0b0000 + }; } } \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs index 3059d947..ec4c0980 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs @@ -594,9 +594,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid NpadIdType[] supportedPlayerIds = new NpadIdType[arraySize]; + context.Device.Hid.Npads.ClearSupportedPlayers(); + for (int i = 0; i < arraySize; ++i) { - supportedPlayerIds[i] = context.Memory.Read((ulong)(context.Request.PtrBuff[0].Position + i * 4)); + NpadIdType id = context.Memory.Read((ulong)(context.Request.PtrBuff[0].Position + i * 4)); + + if (id >= 0) + { + context.Device.Hid.Npads.SetSupportedPlayer(HidUtils.GetIndexFromNpadIdType(id)); + } + + supportedPlayerIds[i] = id; } Logger.Stub?.PrintStub(LogClass.ServiceHid, $"{arraySize} " + string.Join(",", supportedPlayerIds)); @@ -665,9 +674,9 @@ namespace Ryujinx.HLE.HOS.Services.Hid // GetPlayerLedPattern(uint NpadId) -> ulong LedPattern public ResultCode GetPlayerLedPattern(ServiceCtx context) { - int npadId = context.RequestData.ReadInt32(); + NpadIdType npadId = (NpadIdType)context.RequestData.ReadInt32(); - long ledPattern = 0; + long ledPattern = HidUtils.GetLedPatternFromNpadId(npadId); context.ResponseData.Write(ledPattern); diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/DebugPad/DebugPad.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/DebugPad/DebugPad.cs index e68924da..3fbaa304 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/DebugPad/DebugPad.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/DebugPad/DebugPad.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { unsafe struct ShMemDebugPad diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/HidSharedMemory.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/HidSharedMemory.cs index d2bd8a23..d950425d 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/HidSharedMemory.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/HidSharedMemory.cs @@ -1,5 +1,4 @@ -using System; -using System.Runtime.InteropServices; +using Ryujinx.Common.Memory; namespace Ryujinx.HLE.HOS.Services.Hid { diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Keyboard/Keyboard.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Keyboard/Keyboard.cs index dcfa5aa1..e2c1844f 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Keyboard/Keyboard.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Keyboard/Keyboard.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { unsafe struct ShMemKeyboard diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Mouse/Mouse.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Mouse/Mouse.cs index c1f45c5c..6b99e04a 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Mouse/Mouse.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Mouse/Mouse.cs @@ -1,4 +1,6 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { unsafe struct ShMemMouse diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/Npad.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/Npad.cs index 0bb2628f..4ef83f3d 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/Npad.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/Npad.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { // TODO: Add missing structs diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadLayout.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadLayout.cs index 9851a6b1..24c4f4d4 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadLayout.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadLayout.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { struct NpadLayout diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadSixAxis.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadSixAxis.cs index 5f65db39..a0a39fdc 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadSixAxis.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Npad/NpadSixAxis.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { struct NpadSixAxis diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/StructArrayHelpers.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/StructArrayHelpers.cs deleted file mode 100644 index f40d16a0..00000000 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/StructArrayHelpers.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Ryujinx.HLE.HOS.Services.Hid -{ -#pragma warning disable CS0169 - struct Array2 where T : unmanaged - { - T e0, e1; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 2)[index]; - public int Length => 2; - } - - struct Array3 where T : unmanaged - { - T e0, e1, e2; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 3)[index]; - public int Length => 3; - } - - struct Array6 where T : unmanaged - { - T e0, e1, e2, e3, e4, e5; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 6)[index]; - public int Length => 6; - } - - struct Array7 where T : unmanaged - { - T e0, e1, e2, e3, e4, e5, e6; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 7)[index]; - public int Length => 7; - } - - struct Array10 where T : unmanaged - { - T e0, e1, e2, e3, e4, e5, e6, e7, e8, e9; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 10)[index]; - public int Length => 10; - } - - struct Array16 where T : unmanaged - { - T e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 16)[index]; - public int Length => 16; - } - - struct Array17 where T : unmanaged - { - T e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16; - public ref T this[int index] => ref MemoryMarshal.CreateSpan(ref e0, 17)[index]; - public int Length => 17; - } -#pragma warning restore CS0169 -} \ No newline at end of file diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreen.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreen.cs index 618ddd98..5f12295c 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreen.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreen.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { unsafe struct ShMemTouchScreen diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenState.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenState.cs index a0a9cf23..1c85e291 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenState.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenState.cs @@ -1,3 +1,5 @@ +using Ryujinx.Common.Memory; + namespace Ryujinx.HLE.HOS.Services.Hid { struct TouchScreenState diff --git a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenStateData.cs b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenStateData.cs index 872064b3..4d4c48d1 100644 --- a/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenStateData.cs +++ b/Ryujinx.HLE/HOS/Services/Hid/Types/SharedMem/Touchscreen/TouchScreenStateData.cs @@ -3,13 +3,17 @@ namespace Ryujinx.HLE.HOS.Services.Hid struct TouchScreenStateData { public ulong SampleTimestamp; +#pragma warning disable CS0169 uint _padding; +#pragma warning restore CS0169 public uint TouchIndex; public uint X; public uint Y; public uint DiameterX; public uint DiameterY; public uint Angle; +#pragma warning disable CS0169 uint _padding2; +#pragma warning restore CS0169 } } \ No newline at end of file diff --git a/Ryujinx.HLE/IHostUiHandler.cs b/Ryujinx.HLE/IHostUiHandler.cs index 13b4b4c1..bd64da87 100644 --- a/Ryujinx.HLE/IHostUiHandler.cs +++ b/Ryujinx.HLE/IHostUiHandler.cs @@ -10,5 +10,17 @@ namespace Ryujinx.HLE /// Text that the user entered. Set to `null` on internal errors /// True when OK is pressed, False otherwise. Also returns True on internal errors bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText); + + /// + /// Displays a Message Dialog box to the user and blocks until it is closed. + /// + /// True when OK is pressed, False otherwise. + bool DisplayMessageDialog(string title, string message); + + /// + /// Displays a Message Dialog box specific to Controller Applet and blocks until it is closed. + /// + /// True when OK is pressed, False otherwise. + bool DisplayMessageDialog(ControllerAppletUiArgs args); } } \ No newline at end of file diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs index 0bdcdabd..df02e5e5 100644 --- a/Ryujinx.HLE/Switch.cs +++ b/Ryujinx.HLE/Switch.cs @@ -114,6 +114,10 @@ namespace Ryujinx.HLE System.GlobalAccessLogMode = ConfigurationState.Instance.System.FsGlobalAccessLogMode; ServiceConfiguration.IgnoreMissingServices = ConfigurationState.Instance.System.IgnoreMissingServices; + + // Configure controllers + Hid.RefreshInputConfig(ConfigurationState.Instance.Hid.InputConfig.Value); + ConfigurationState.Instance.Hid.InputConfig.Event += Hid.RefreshInputConfigEvent; } public static IntegrityCheckLevel GetIntegrityCheckLevel() @@ -177,6 +181,8 @@ namespace Ryujinx.HLE { if (disposing) { + ConfigurationState.Instance.Hid.InputConfig.Event -= Hid.RefreshInputConfigEvent; + System.Dispose(); Host1x.Dispose(); AudioOut.Dispose(); diff --git a/Ryujinx/Ui/ControllerWindow.cs b/Ryujinx/Ui/ControllerWindow.cs index 35c3859f..9518ba98 100644 --- a/Ryujinx/Ui/ControllerWindow.cs +++ b/Ryujinx/Ui/ControllerWindow.cs @@ -5,6 +5,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.Configuration; using Ryujinx.HLE.FileSystem; using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text.Json; @@ -91,6 +92,23 @@ namespace Ryujinx.Ui _virtualFileSystem = virtualFileSystem; _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); + Title = $"Ryujinx - Controller Settings - {_playerIndex}"; + + if (_playerIndex == PlayerIndex.Handheld) + { + _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); + _controllerType.Sensitive = false; + } + else + { + _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); + _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); + _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); + _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); + } + + _controllerType.Active = 0; // Set initial value to first in list. + //Bind Events _lStickX.Clicked += Button_Pressed; _lStickY.Clicked += Button_Pressed; @@ -278,7 +296,12 @@ namespace Ryujinx.Ui switch (config) { case KeyboardConfig keyboardConfig: - _controllerType.SetActiveId(keyboardConfig.ControllerType.ToString()); + if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString(); _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString(); @@ -310,7 +333,12 @@ namespace Ryujinx.Ui _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); break; case ControllerConfig controllerConfig: - _controllerType.SetActiveId(controllerConfig.ControllerType.ToString()); + if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } _lStickX.Label = controllerConfig.LeftJoycon.StickX.ToString(); _invertLStickX.Active = controllerConfig.LeftJoycon.InvertStickX; @@ -894,24 +922,31 @@ namespace Ryujinx.Ui { InputConfig inputConfig = GetValues(); + var newConfig = new List(); + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + if (_inputConfig == null && inputConfig != null) { - ConfigurationState.Instance.Hid.InputConfig.Value.Add(inputConfig); + newConfig.Add(inputConfig); } else { if (_inputDevice.ActiveId == "disabled") { - ConfigurationState.Instance.Hid.InputConfig.Value.Remove(_inputConfig); + newConfig.Remove(_inputConfig); } else if (inputConfig != null) { - int index = ConfigurationState.Instance.Hid.InputConfig.Value.IndexOf(_inputConfig); + int index = newConfig.IndexOf(_inputConfig); - ConfigurationState.Instance.Hid.InputConfig.Value[index] = inputConfig; + newConfig[index] = inputConfig; } } + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + MainWindow.SaveConfig(); Dispose(); diff --git a/Ryujinx/Ui/ControllerWindow.glade b/Ryujinx/Ui/ControllerWindow.glade index 2b780f13..c0532d90 100644 --- a/Ryujinx/Ui/ControllerWindow.glade +++ b/Ryujinx/Ui/ControllerWindow.glade @@ -138,13 +138,6 @@ False The controller's type 0 - - Handheld - Pro Controller - Paired Joycons - Left Joycon - Right Joycon - diff --git a/Ryujinx/Ui/GLRenderer.cs b/Ryujinx/Ui/GLRenderer.cs index 867401ad..203df72a 100644 --- a/Ryujinx/Ui/GLRenderer.cs +++ b/Ryujinx/Ui/GLRenderer.cs @@ -405,9 +405,9 @@ namespace Ryujinx.Ui }); } - List gamepadInputs = new List(); + List gamepadInputs = new List(NpadDevices.MaxControllers); - foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value.ToArray()) + foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value) { ControllerKeys currentButton = 0; JoystickPosition leftJoystick = new JoystickPosition(); @@ -497,18 +497,21 @@ namespace Ryujinx.Ui }); } - _device.Hid.Npads.SetGamepadsInput(gamepadInputs.ToArray()); + _device.Hid.Npads.Update(gamepadInputs); - // Hotkeys - HotkeyButtons currentHotkeyButtons = KeyboardController.GetHotkeyButtons(OpenTK.Input.Keyboard.GetState()); - - if (currentHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync) && - !_prevHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync)) + if(IsFocused) { - _device.EnableDeviceVsync = !_device.EnableDeviceVsync; - } + // Hotkeys + HotkeyButtons currentHotkeyButtons = KeyboardController.GetHotkeyButtons(OpenTK.Input.Keyboard.GetState()); - _prevHotkeyButtons = currentHotkeyButtons; + if (currentHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync) && + !_prevHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync)) + { + _device.EnableDeviceVsync = !_device.EnableDeviceVsync; + } + + _prevHotkeyButtons = currentHotkeyButtons; + } //Touchscreen bool hasTouch = false; diff --git a/Ryujinx/Ui/GtkHostUiHandler.cs b/Ryujinx/Ui/GtkHostUiHandler.cs index 989fe14d..90830056 100644 --- a/Ryujinx/Ui/GtkHostUiHandler.cs +++ b/Ryujinx/Ui/GtkHostUiHandler.cs @@ -16,6 +16,62 @@ namespace Ryujinx.Ui _parent = parent; } + public bool DisplayMessageDialog(ControllerAppletUiArgs args) + { + string playerCount = args.PlayerCountMin == args.PlayerCountMax + ? $"exactly {args.PlayerCountMin}" + : $"{args.PlayerCountMin}-{args.PlayerCountMax}"; + + string message = + $"Application requests {playerCount} player(s) with:\n\n" + + $"TYPES: {args.SupportedStyles}\n\n" + + $"PLAYERS: {string.Join(", ", args.SupportedPlayers)}\n\n" + + (args.IsDocked ? "Docked mode set. Handheld is also invalid.\n\n" : "") + + "Please reconfigure Input now and then press OK."; + + return DisplayMessageDialog("Controller Applet", message); + } + + public bool DisplayMessageDialog(string title, string message) + { + ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); + bool okPressed = false; + + Application.Invoke(delegate + { + MessageDialog msgDialog = null; + try + { + msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null) + { + Title = title, + Text = message, + UseMarkup = true + }; + + msgDialog.SetDefaultSize(400, 0); + + msgDialog.Response += (object o, ResponseArgs args) => + { + if (args.ResponseId == ResponseType.Ok) okPressed = true; + dialogCloseEvent.Set(); + msgDialog?.Dispose(); + }; + + msgDialog.Show(); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.Application, $"Error displaying Message Dialog: {e}"); + dialogCloseEvent.Set(); + } + }); + + dialogCloseEvent.WaitOne(); + + return okPressed; + } + public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText) { ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); diff --git a/Ryujinx/Ui/MainWindow.cs b/Ryujinx/Ui/MainWindow.cs index f077e9bd..d2b303e4 100644 --- a/Ryujinx/Ui/MainWindow.cs +++ b/Ryujinx/Ui/MainWindow.cs @@ -507,14 +507,6 @@ namespace Ryujinx.Ui _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); } - device.Hid.Npads.AddControllers(ConfigurationState.Instance.Hid.InputConfig.Value.Select(inputConfig => - new HLE.HOS.Services.Hid.ControllerConfig - { - Player = (PlayerIndex)inputConfig.PlayerIndex, - Type = (ControllerType)inputConfig.ControllerType - } - ).ToArray()); - _glWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel); Application.Invoke(delegate