Improve multi-controller support in HID and Controller Applet (#1453)

* Initial commit

Enable proper LED patterns
Toggle Hotkeys only on focus
Ignore Handheld on Docked mode
Remove PrimaryController
Validate NpadIdType
Rewrite NpadDevices to process config in update loop
Cleanup

* Notify in log periodically when no matched controllers

* Remove duplicate StructArrayHelpers in favor of Common.Memory

Fix struct padding CS0169 warns in Touchscreen

* Remove GTK markup from Controller Applet

Use IList instead of List
Explicit list capacity in 1ms loop
Fix formatting

* Restrict ControllerWindow to show valid controller types

Add selected player name to ControllerWindow title

* ControllerWindow: Fix controller type initial value

NpadDevices: Simplify default battery charge

* Address AcK's comments

Use explicit types and fix formatting

* Remove HashSet for SupportedPlayers

Fixes potential exceptions due to race

* Fix ControllerSupportArg struct packing

Also comes with two revisions of struct for 4/8 players max.
This commit is contained in:
mageven 2020-08-24 02:24:11 +05:30 committed by GitHub
parent 01ff648bdf
commit 27179d0218
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 445 additions and 242 deletions

View File

@ -32,7 +32,7 @@ namespace Ryujinx.HLE.HOS.Applets
byte[] controllerSupportArgPrivate = _normalSession.Pop(); byte[] controllerSupportArgPrivate = _normalSession.Pop();
ControllerSupportArgPrivate privateArg = IApplet.ReadStruct<ControllerSupportArgPrivate>(controllerSupportArgPrivate); ControllerSupportArgPrivate privateArg = IApplet.ReadStruct<ControllerSupportArgPrivate>(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}"); $"HoldType:{(NpadJoyHoldType)privateArg.NpadJoyHoldType} StyleSets:{(ControllerType)privateArg.NpadStyleSet}");
if (privateArg.Mode != ControllerSupportMode.ShowControllerSupport) if (privateArg.Mode != ControllerSupportMode.ShowControllerSupport)
@ -47,33 +47,57 @@ namespace Ryujinx.HLE.HOS.Applets
ControllerSupportArgHeader argHeader; ControllerSupportArgHeader argHeader;
if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArg>()) if (privateArg.ArgSize == Marshal.SizeOf<ControllerSupportArgV7>())
{ {
ControllerSupportArg arg = IApplet.ReadStruct<ControllerSupportArg>(controllerSupportArg); ControllerSupportArgV7 arg = IApplet.ReadStruct<ControllerSupportArgV7>(controllerSupportArg);
argHeader = arg.Header; 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>())
{
ControllerSupportArgVPre7 arg = IApplet.ReadStruct<ControllerSupportArgVPre7>(controllerSupportArg);
argHeader = arg.Header;
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Pre-7 EnableExplainText={arg.EnableExplainText != 0}");
// Read enable text here? // Read enable text here?
} }
else else
{ {
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"Unknown revision of ControllerSupportArg."); Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerSupportArg Version Unknown");
argHeader = IApplet.ReadStruct<ControllerSupportArgHeader>(controllerSupportArg); // Read just the header argHeader = IApplet.ReadStruct<ControllerSupportArgHeader>(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 Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet Arg {playerMin} {playerMax} {argHeader.EnableTakeOverConnection} {argHeader.EnableSingleMode}");
// 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. int configuredCount = 0;
if (argHeader.PlayerCountMin > 1) 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 ControllerSupportResultInfo result = new ControllerSupportResultInfo
{ {
PlayerCount = 1, PlayerCount = (sbyte)configuredCount,
SelectedId = (uint)GetNpadIdTypeFromIndex(_system.Device.Hid.Npads.PrimaryController) SelectedId = (uint)GetNpadIdTypeFromIndex(primaryIndex)
}; };
Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ReturnResult {result.PlayerCount} {result.SelectedId}"); Logger.Stub?.PrintStub(LogClass.ServiceHid, $"ControllerApplet ReturnResult {result.PlayerCount} {result.SelectedId}");

View File

@ -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<PlayerIndex> SupportedPlayers;
public bool IsDocked;
}
}

View File

@ -1,6 +1,9 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets namespace Ryujinx.HLE.HOS.Applets
{ {
#pragma warning disable CS0649 #pragma warning disable CS0649
[StructLayout(LayoutKind.Sequential, Pack=1)]
struct ControllerSupportArgHeader struct ControllerSupportArgHeader
{ {
public sbyte PlayerCountMin; public sbyte PlayerCountMin;

View File

@ -1,8 +1,11 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets namespace Ryujinx.HLE.HOS.Applets
{ {
#pragma warning disable CS0649 #pragma warning disable CS0649
// (8.0.0+ version) // (8.0.0+ version)
unsafe struct ControllerSupportArg [StructLayout(LayoutKind.Sequential, Pack=1)]
unsafe struct ControllerSupportArgV7
{ {
public ControllerSupportArgHeader Header; public ControllerSupportArgHeader Header;
public fixed uint IdentificationColor[8]; public fixed uint IdentificationColor[8];

View File

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

View File

@ -1,6 +1,9 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets namespace Ryujinx.HLE.HOS.Applets
{ {
#pragma warning disable CS0649 #pragma warning disable CS0649
[StructLayout(LayoutKind.Sequential, Pack=1)]
unsafe struct ControllerSupportResultInfo unsafe struct ControllerSupportResultInfo
{ {
public sbyte PlayerCount; public sbyte PlayerCount;

View File

@ -249,6 +249,9 @@ namespace Ryujinx.HLE.HOS
AppletState.EnqueueMessage(MessageInfo.OperationModeChanged); AppletState.EnqueueMessage(MessageInfo.OperationModeChanged);
AppletState.EnqueueMessage(MessageInfo.PerformanceModeChanged); AppletState.EnqueueMessage(MessageInfo.PerformanceModeChanged);
SignalDisplayResolutionChange(); SignalDisplayResolutionChange();
// Reconfigure controllers
Device.Hid.RefreshInputConfig(ConfigurationState.Instance.Hid.InputConfig.Value);
} }
} }

View File

@ -1,5 +1,7 @@
using Ryujinx.Common; using Ryujinx.Common;
using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.Exceptions;
using Ryujinx.Common.Configuration.Hid;
using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
@ -65,6 +67,24 @@ namespace Ryujinx.HLE.HOS.Services.Hid
Npads = new NpadDevices(_device, true); Npads = new NpadDevices(_device, true);
} }
internal void RefreshInputConfig(List<InputConfig> 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<List<InputConfig>> args)
{
RefreshInputConfig(args.NewValue);
}
public ControllerKeys UpdateStickButtons(JoystickPosition leftStick, JoystickPosition rightStick) public ControllerKeys UpdateStickButtons(JoystickPosition leftStick, JoystickPosition rightStick)
{ {
ControllerKeys result = 0; ControllerKeys result = 0;

View File

@ -1,74 +1,118 @@
using System; using Ryujinx.Common;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.HOS.Kernel.Threading;
using System;
using System.Collections.Generic;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
public class NpadDevices : BaseDevice public class NpadDevices : BaseDevice
{ {
internal NpadJoyHoldType JoyHold = NpadJoyHoldType.Vertical; private const BatteryCharge DefaultBatteryCharge = BatteryCharge.Percent100;
internal bool SixAxisActive = false; // TODO: link to hidserver when implemented
private enum FilterState private const int NoMatchNotifyFrequencyMs = 2000;
{ private int _activeCount;
Unconfigured = 0, private long _lastNotifyTimestamp;
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;
public const int MaxControllers = 9; // Players 1-8 and Handheld
private ControllerType[] _configuredTypes;
private KEvent[] _styleSetUpdateEvents; private KEvent[] _styleSetUpdateEvents;
private bool[] _supportedPlayers;
private static readonly Array3<BatteryCharge> _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) 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) for (int i = 0; i < _styleSetUpdateEvents.Length; ++i)
{ {
_styleSetUpdateEvents[i] = new KEvent(_device.System.KernelContext); _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<PlayerIndex> 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) for (int i = 0; i < configs.Length; ++i)
{ {
PlayerIndex player = configs[i].Player; PlayerIndex player = configs[i].Player;
ControllerType controllerType = configs[i].Type; ControllerType controllerType = configs[i].Type;
if (player > PlayerIndex.Handheld) if (player > PlayerIndex.Handheld)
@ -81,77 +125,87 @@ namespace Ryujinx.HLE.HOS.Services.Hid
player = PlayerIndex.Handheld; 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()
{
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.");
} }
} }
internal ref KEvent GetStyleSetUpdateEvent(PlayerIndex player) public void Update(IList<GamepadInput> 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]; 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 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 // TODO: Allow customizing colors at config
NpadStateHeader defaultHeader = new NpadStateHeader NpadStateHeader defaultHeader = new NpadStateHeader
{ {
@ -168,7 +222,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
NpadSystemProperties.PowerInfo1Connected | NpadSystemProperties.PowerInfo1Connected |
NpadSystemProperties.PowerInfo2Connected; NpadSystemProperties.PowerInfo2Connected;
controller.BatteryState = _fullBattery; controller.BatteryState.ToSpan().Fill(DefaultBatteryCharge);
switch (type) switch (type)
{ {
@ -217,19 +271,13 @@ namespace Ryujinx.HLE.HOS.Services.Hid
controller.Header = defaultHeader; controller.Header = defaultHeader;
if (PrimaryController == PlayerIndex.Unknown)
{
PrimaryController = player;
}
_configuredNpads[(int)player].State = FilterState.Accepted;
_styleSetUpdateEvents[(int)player].ReadableEvent.Signal(); _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 switch
{ {
ControllerType.ProController => NpadLayoutsIndex.ProController, ControllerType.ProController => NpadLayoutsIndex.ProController,
@ -241,43 +289,28 @@ namespace Ryujinx.HLE.HOS.Services.Hid
_ => NpadLayoutsIndex.SystemExternal _ => NpadLayoutsIndex.SystemExternal
}; };
public void SetGamepadsInput(params GamepadInput[] states) private void UpdateInput(GamepadInput state)
{ {
UpdateAllEntries(); if (state.PlayerId == PlayerIndex.Unknown)
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)
{ {
return; 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; return;
} }
ref ShMemNpad currentNpad = ref _device.Hid.SharedMemory.Npads[(int)player]; ref NpadLayout currentLayout = ref currentNpad.Layouts[(int)ControllerTypeToNpadLayout(currentNpad.Header.Type)];
ref NpadLayout currentLayout = ref currentNpad.Layouts[(int)ControllerTypeToLayout(currentNpad.Header.Type)];
ref NpadState currentEntry = ref currentLayout.Entries[(int)currentLayout.Header.LatestEntry]; ref NpadState currentEntry = ref currentLayout.Entries[(int)currentLayout.Header.LatestEntry];
currentEntry.Buttons = buttons; currentEntry.Buttons = state.Buttons;
currentEntry.LStickX = leftJoystick.Dx; currentEntry.LStickX = state.LStick.Dx;
currentEntry.LStickY = leftJoystick.Dy; currentEntry.LStickY = state.LStick.Dy;
currentEntry.RStickX = rightJoystick.Dx; currentEntry.RStickX = state.RStick.Dx;
currentEntry.RStickY = rightJoystick.Dy; currentEntry.RStickY = state.RStick.Dy;
// Mirror data to Default layout just in case // Mirror data to Default layout just in case
ref NpadLayout mainLayout = ref currentNpad.Layouts[(int)NpadLayoutsIndex.SystemExternal]; ref NpadLayout mainLayout = ref currentNpad.Layouts[(int)NpadLayoutsIndex.SystemExternal];

View File

@ -35,5 +35,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid.HidServer
PlayerIndex.Unknown => NpadIdType.Unknown, PlayerIndex.Unknown => NpadIdType.Unknown,
_ => throw new ArgumentOutOfRangeException(nameof(index)) _ => 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
};
} }
} }

View File

@ -594,9 +594,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid
NpadIdType[] supportedPlayerIds = new NpadIdType[arraySize]; NpadIdType[] supportedPlayerIds = new NpadIdType[arraySize];
context.Device.Hid.Npads.ClearSupportedPlayers();
for (int i = 0; i < arraySize; ++i) for (int i = 0; i < arraySize; ++i)
{ {
supportedPlayerIds[i] = context.Memory.Read<NpadIdType>((ulong)(context.Request.PtrBuff[0].Position + i * 4)); NpadIdType id = context.Memory.Read<NpadIdType>((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)); Logger.Stub?.PrintStub(LogClass.ServiceHid, $"{arraySize} " + string.Join(",", supportedPlayerIds));
@ -665,9 +674,9 @@ namespace Ryujinx.HLE.HOS.Services.Hid
// GetPlayerLedPattern(uint NpadId) -> ulong LedPattern // GetPlayerLedPattern(uint NpadId) -> ulong LedPattern
public ResultCode GetPlayerLedPattern(ServiceCtx context) 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); context.ResponseData.Write(ledPattern);

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
unsafe struct ShMemDebugPad unsafe struct ShMemDebugPad

View File

@ -1,5 +1,4 @@
using System; using Ryujinx.Common.Memory;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
unsafe struct ShMemKeyboard unsafe struct ShMemKeyboard

View File

@ -1,4 +1,6 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
unsafe struct ShMemMouse unsafe struct ShMemMouse

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
// TODO: Add missing structs // TODO: Add missing structs

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
struct NpadLayout struct NpadLayout

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
struct NpadSixAxis struct NpadSixAxis

View File

@ -1,55 +0,0 @@
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Services.Hid
{
#pragma warning disable CS0169
struct Array2<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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<T> 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
}

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
unsafe struct ShMemTouchScreen unsafe struct ShMemTouchScreen

View File

@ -1,3 +1,5 @@
using Ryujinx.Common.Memory;
namespace Ryujinx.HLE.HOS.Services.Hid namespace Ryujinx.HLE.HOS.Services.Hid
{ {
struct TouchScreenState struct TouchScreenState

View File

@ -3,13 +3,17 @@ namespace Ryujinx.HLE.HOS.Services.Hid
struct TouchScreenStateData struct TouchScreenStateData
{ {
public ulong SampleTimestamp; public ulong SampleTimestamp;
#pragma warning disable CS0169
uint _padding; uint _padding;
#pragma warning restore CS0169
public uint TouchIndex; public uint TouchIndex;
public uint X; public uint X;
public uint Y; public uint Y;
public uint DiameterX; public uint DiameterX;
public uint DiameterY; public uint DiameterY;
public uint Angle; public uint Angle;
#pragma warning disable CS0169
uint _padding2; uint _padding2;
#pragma warning restore CS0169
} }
} }

View File

@ -10,5 +10,17 @@ namespace Ryujinx.HLE
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param> /// <param name="userText">Text that the user entered. Set to `null` on internal errors</param>
/// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns> /// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText); bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText);
/// <summary>
/// Displays a Message Dialog box to the user and blocks until it is closed.
/// </summary>
/// <returns>True when OK is pressed, False otherwise.</returns>
bool DisplayMessageDialog(string title, string message);
/// <summary>
/// Displays a Message Dialog box specific to Controller Applet and blocks until it is closed.
/// </summary>
/// <returns>True when OK is pressed, False otherwise.</returns>
bool DisplayMessageDialog(ControllerAppletUiArgs args);
} }
} }

View File

@ -114,6 +114,10 @@ namespace Ryujinx.HLE
System.GlobalAccessLogMode = ConfigurationState.Instance.System.FsGlobalAccessLogMode; System.GlobalAccessLogMode = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
ServiceConfiguration.IgnoreMissingServices = ConfigurationState.Instance.System.IgnoreMissingServices; 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() public static IntegrityCheckLevel GetIntegrityCheckLevel()
@ -177,6 +181,8 @@ namespace Ryujinx.HLE
{ {
if (disposing) if (disposing)
{ {
ConfigurationState.Instance.Hid.InputConfig.Event -= Hid.RefreshInputConfigEvent;
System.Dispose(); System.Dispose();
Host1x.Dispose(); Host1x.Dispose();
AudioOut.Dispose(); AudioOut.Dispose();

View File

@ -5,6 +5,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.Configuration; using Ryujinx.Configuration;
using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.FileSystem;
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
@ -91,6 +92,23 @@ namespace Ryujinx.Ui
_virtualFileSystem = virtualFileSystem; _virtualFileSystem = virtualFileSystem;
_inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); _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 //Bind Events
_lStickX.Clicked += Button_Pressed; _lStickX.Clicked += Button_Pressed;
_lStickY.Clicked += Button_Pressed; _lStickY.Clicked += Button_Pressed;
@ -278,7 +296,12 @@ namespace Ryujinx.Ui
switch (config) switch (config)
{ {
case KeyboardConfig keyboardConfig: 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(); _lStickUp.Label = keyboardConfig.LeftJoycon.StickUp.ToString();
_lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString(); _lStickDown.Label = keyboardConfig.LeftJoycon.StickDown.ToString();
@ -310,7 +333,12 @@ namespace Ryujinx.Ui
_rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString();
break; break;
case ControllerConfig controllerConfig: 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(); _lStickX.Label = controllerConfig.LeftJoycon.StickX.ToString();
_invertLStickX.Active = controllerConfig.LeftJoycon.InvertStickX; _invertLStickX.Active = controllerConfig.LeftJoycon.InvertStickX;
@ -894,24 +922,31 @@ namespace Ryujinx.Ui
{ {
InputConfig inputConfig = GetValues(); InputConfig inputConfig = GetValues();
var newConfig = new List<InputConfig>();
newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
if (_inputConfig == null && inputConfig != null) if (_inputConfig == null && inputConfig != null)
{ {
ConfigurationState.Instance.Hid.InputConfig.Value.Add(inputConfig); newConfig.Add(inputConfig);
} }
else else
{ {
if (_inputDevice.ActiveId == "disabled") if (_inputDevice.ActiveId == "disabled")
{ {
ConfigurationState.Instance.Hid.InputConfig.Value.Remove(_inputConfig); newConfig.Remove(_inputConfig);
} }
else if (inputConfig != null) 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(); MainWindow.SaveConfig();
Dispose(); Dispose();

View File

@ -138,13 +138,6 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">The controller's type</property> <property name="tooltip_text" translatable="yes">The controller's type</property>
<property name="active">0</property> <property name="active">0</property>
<items>
<item id="Handheld" translatable="yes">Handheld</item>
<item id="ProController" translatable="yes">Pro Controller</item>
<item id="JoyconPair" translatable="yes">Paired Joycons</item>
<item id="JoyconLeft" translatable="yes">Left Joycon</item>
<item id="JoyconRight" translatable="yes">Right Joycon</item>
</items>
<signal name="changed" handler="Controller_Changed" swapped="no"/> <signal name="changed" handler="Controller_Changed" swapped="no"/>
</object> </object>
<packing> <packing>

View File

@ -405,9 +405,9 @@ namespace Ryujinx.Ui
}); });
} }
List<GamepadInput> gamepadInputs = new List<GamepadInput>(); List<GamepadInput> gamepadInputs = new List<GamepadInput>(NpadDevices.MaxControllers);
foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value.ToArray()) foreach (InputConfig inputConfig in ConfigurationState.Instance.Hid.InputConfig.Value)
{ {
ControllerKeys currentButton = 0; ControllerKeys currentButton = 0;
JoystickPosition leftJoystick = new JoystickPosition(); JoystickPosition leftJoystick = new JoystickPosition();
@ -497,18 +497,21 @@ namespace Ryujinx.Ui
}); });
} }
_device.Hid.Npads.SetGamepadsInput(gamepadInputs.ToArray()); _device.Hid.Npads.Update(gamepadInputs);
// Hotkeys if(IsFocused)
HotkeyButtons currentHotkeyButtons = KeyboardController.GetHotkeyButtons(OpenTK.Input.Keyboard.GetState());
if (currentHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync) &&
!_prevHotkeyButtons.HasFlag(HotkeyButtons.ToggleVSync))
{ {
_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 //Touchscreen
bool hasTouch = false; bool hasTouch = false;

View File

@ -16,6 +16,62 @@ namespace Ryujinx.Ui
_parent = parent; _parent = parent;
} }
public bool DisplayMessageDialog(ControllerAppletUiArgs args)
{
string playerCount = args.PlayerCountMin == args.PlayerCountMax
? $"exactly {args.PlayerCountMin}"
: $"{args.PlayerCountMin}-{args.PlayerCountMax}";
string message =
$"Application requests <b>{playerCount}</b> player(s) with:\n\n"
+ $"<tt><b>TYPES:</b> {args.SupportedStyles}</tt>\n\n"
+ $"<tt><b>PLAYERS:</b> {string.Join(", ", args.SupportedPlayers)}</tt>\n\n"
+ (args.IsDocked ? "Docked mode set. <tt>Handheld</tt> is also invalid.\n\n" : "")
+ "<i>Please reconfigure Input now and then press OK.</i>";
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) public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
{ {
ManualResetEvent dialogCloseEvent = new ManualResetEvent(false); ManualResetEvent dialogCloseEvent = new ManualResetEvent(false);

View File

@ -507,14 +507,6 @@ namespace Ryujinx.Ui
_windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1); _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); _glWidget = new GlRenderer(_emulationContext, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
Application.Invoke(delegate Application.Invoke(delegate