From 29ca1f3f22462ea7358816a2ef40bf391b9d51b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Freire?= Date: Mon, 20 Jan 2025 17:31:04 +0100 Subject: [PATCH] FIX: Set macOS Xbox wireless gamepad button mappings based on PID/VID (#2097) --- .../Tests/InputSystem/APIVerificationTests.cs | 1 + .../Tests/InputSystem/Plugins/XInputTests.cs | 68 ++++++++++- Packages/com.unity.inputsystem/CHANGELOG.md | 1 + .../Debugger/InputDeviceDebuggerWindow.cs | 2 + .../Plugins/XInput/XInputSupport.cs | 29 ++++- .../Plugins/XInput/XboxGamepadMacOS.cs | 115 +++++++++++++++++- 6 files changed, 210 insertions(+), 6 deletions(-) diff --git a/Assets/Tests/InputSystem/APIVerificationTests.cs b/Assets/Tests/InputSystem/APIVerificationTests.cs index 63aef7d580..c96b435404 100644 --- a/Assets/Tests/InputSystem/APIVerificationTests.cs +++ b/Assets/Tests/InputSystem/APIVerificationTests.cs @@ -183,6 +183,7 @@ internal static bool IgnoreTypeForDocsByName(string fullName) #if UNITY_EDITOR_OSX fullName == typeof(UnityEngine.InputSystem.XInput.XboxGamepadMacOS).FullName || fullName == typeof(UnityEngine.InputSystem.XInput.XboxOneGampadMacOSWireless).FullName || + fullName == typeof(UnityEngine.InputSystem.XInput.XboxGamepadMacOSWireless).FullName || #endif #if UNITY_EDITOR_WIN fullName == typeof(UnityEngine.InputSystem.XInput.XInputControllerWindows).FullName || diff --git a/Assets/Tests/InputSystem/Plugins/XInputTests.cs b/Assets/Tests/InputSystem/Plugins/XInputTests.cs index 1532f44aa6..051a48791f 100644 --- a/Assets/Tests/InputSystem/Plugins/XInputTests.cs +++ b/Assets/Tests/InputSystem/Plugins/XInputTests.cs @@ -5,6 +5,7 @@ using UnityEngine.InputSystem.Layouts; using UnityEngine.InputSystem.Utilities; using System.Runtime.InteropServices; +using UnityEngine.InputSystem.HID; using UnityEngine.InputSystem.Processors; #if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_XBOXONE || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN @@ -23,7 +24,7 @@ internal class XInputTests : CoreTestsFixture [Category("Devices")] #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX [TestCase("Xbox One Wired Controller", "Microsoft", "HID", "XboxGamepadMacOS")] - [TestCase("Xbox One Wireless Controller", "Microsoft", "HID", "XboxOneGampadMacOSWireless")] + [TestCase("Xbox Series Wireless Controller", "Microsoft", "HID", "XboxGamepadMacOSWireless")] #endif #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_WSA [TestCase(null, null, "XInput", "XInputControllerWindows")] @@ -148,6 +149,64 @@ public void Devices_SupportXboxControllerOnOSX() AssertButtonPress(gamepad, new XInputControllerOSXState().WithButton(XInputControllerOSXState.Button.Select), gamepad.selectButton); } + [TestCase(0x045E, 0x02E0, 16, 11)] // Xbox One Wireless Controller + [TestCase(0x045E, 0x0B20, 10, 11)] // Xbox Series X|S Wireless Controller + // This test is used to establish the correct button map layout based on the PID and VIDs. The usual difference + // is around the select and start button bits. + // If the layout is changed this test will fail and will need to be adapted either with a new device/layout or + // a new button map. + public void Devices_SupportWirelessXboxOneAndSeriesControllerOnOSX(int vendorId, int productId, int selectBit, int startBit) + { + // Fake a real Xbox Wireless Controller + var xboxGamepad = InputSystem.AddDevice(new InputDeviceDescription + { + interfaceName = "HID", + product = "Xbox Wireless Controller", + manufacturer = "Microsoft", + capabilities = new HID.HIDDeviceDescriptor + { + vendorId = vendorId, + productId = productId, + }.ToJson() + }); + + + Assert.That(xboxGamepad, Is.AssignableTo()); + + var gamepad = (XInputController)xboxGamepad; + Assert.That(gamepad.selectButton.isPressed, Is.False); + + // Check if the controller is an Xbox One from a particular type where we know the select and start buttons are + // different + if (productId == 0x02e0) + { + Assert.That(xboxGamepad, Is.AssignableTo()); + + InputSystem.QueueStateEvent(gamepad, + new XInputControllerWirelessOSXState + { + buttons = (uint)(1 << selectBit | + 1 << startBit) + }); + InputSystem.Update(); + } + else + { + Assert.That(xboxGamepad, Is.AssignableTo()); + + InputSystem.QueueStateEvent(gamepad, + new XInputControllerWirelessOSXState + { + buttons = (uint)(1 << selectBit | + 1 << startBit) + }); + InputSystem.Update(); + } + + Assert.That(gamepad.selectButton.isPressed); + Assert.That(gamepad.startButton.isPressed); + } + // Disable tests in standalone builds from 2022.1+ see UUM-19622 #if !UNITY_STANDALONE_OSX || !TEMP_DISABLE_STANDALONE_OSX_XINPUT_TEST [Test] @@ -158,7 +217,12 @@ public void Devices_SupportXboxWirelessControllerOnOSX() { interfaceName = "HID", product = "Xbox One Wireless Controller", - manufacturer = "Microsoft" + manufacturer = "Microsoft", + capabilities = new HID.HIDDeviceDescriptor + { + vendorId = 0x045E, + productId = 0x02E0, + }.ToJson() }); Assert.That(device, Is.AssignableTo()); diff --git a/Packages/com.unity.inputsystem/CHANGELOG.md b/Packages/com.unity.inputsystem/CHANGELOG.md index f43554ee84..9c7f08597f 100644 --- a/Packages/com.unity.inputsystem/CHANGELOG.md +++ b/Packages/com.unity.inputsystem/CHANGELOG.md @@ -46,6 +46,7 @@ however, it has to be formatted properly to pass verification tests. - Fixed multiple `OnScreenStick` Components that does not work together when using them simultaneously in isolation mode. [ISXB-813](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-813) - Fixed an issue in input actions editor window that caused certain fields in custom input composite bindings to require multiple clicks to action / focus. [ISXB-1171](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1171) - Fixed an editor/player hang in `InputSystemUIInputModule` due to an infinite loop. This was caused by the assumption that `RemovePointerAtIndex` would _always_ successfully remove the pointer, which is not the case with touch based pointers. [ISXB-1258](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1258) +- Fixed wrong Xbox Series S|X and Xbox One wireless controllers "View" button mapping on macOS by expanding device PID and VID matching. [ISXB-1264](https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1264) ### Changed - Changed location of the link xml file (code stripping rules), from a temporary directory to the project Library folder (ISX-2140). diff --git a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs index bda67adbf8..2658ec1922 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs @@ -132,6 +132,8 @@ internal void OnGUI() EditorGUILayout.LabelField("Product", m_Device.description.product); if (!string.IsNullOrEmpty(m_Device.description.manufacturer)) EditorGUILayout.LabelField("Manufacturer", m_Device.description.manufacturer); + if (!string.IsNullOrEmpty(m_Device.description.version)) + EditorGUILayout.LabelField("Version", m_Device.description.version); if (!string.IsNullOrEmpty(m_Device.description.serial)) EditorGUILayout.LabelField("Serial Number", m_Device.description.serial); EditorGUILayout.LabelField("Device ID", m_DeviceIdString); diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs index 6e760a2393..06a3e37d94 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs @@ -28,9 +28,36 @@ public static void Initialize() InputSystem.RegisterLayout( matches: new InputDeviceMatcher().WithInterface("HID") .WithProduct("Xbox.*Wired Controller")); - InputSystem.RegisterLayout( + + // Matching older Xbox One controllers that have different View and Share buttons than the newer Xbox Series + // controllers. + // Reported inhttps://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1264 + // Based on devices from this list + // https://github.com/mdqinc/SDL_GameControllerDB/blob/a453871de2e0e2484544514c6c080e1e916d620c/gamecontrollerdb.txt#L798C1-L806C1 + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02B0); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02D1); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02DD); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02E0); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02E3); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02EA); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02FD); + RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02FF); + + // This layout is for all the other Xbox One or Series controllers that have the same View and Share buttons. + // Reported in https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-385 + InputSystem.RegisterLayout( matches: new InputDeviceMatcher().WithInterface("HID") .WithProduct("Xbox.*Wireless Controller")); + + void RegisterXboxOneWirelessFromProductAndVendorID(int vendorId, int productId) + { + InputSystem.RegisterLayout( + matches: new InputDeviceMatcher().WithInterface("HID") + .WithProduct("Xbox.*Wireless Controller") + .WithCapability("vendorId", vendorId) + .WithCapability("productId", productId)); + } + #endif } } diff --git a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs index ab43ddbaec..c57bfe3e8b 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs @@ -97,7 +97,7 @@ public enum Button public XInputControllerOSXState WithButton(Button button) { - Debug.Assert((int)button < 16, $"Expected button < 16, so we fit into the 16 bit wide bitmask"); + Debug.Assert((int)button < 16, $"A maximum of 16 buttons is supported for this layout."); buttons |= (ushort)(1U << (int)button); return this; } @@ -111,7 +111,7 @@ internal struct XInputControllerWirelessOSXState : IInputStateTypeInfo public enum Button { Start = 11, - Select = 10, + Select = 16, LeftThumbstickPress = 13, RightThumbstickPress = 14, LeftShoulder = 6, @@ -175,7 +175,7 @@ public enum Button public XInputControllerWirelessOSXState WithButton(Button button) { - Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask"); + Debug.Assert((int)button < 32, $"A maximum of 32 buttons is supported for this layout."); buttons |= 1U << (int)button; return this; } @@ -194,6 +194,98 @@ public XInputControllerWirelessOSXState WithDpad(byte value) leftStickY = 32767 }; } + + [StructLayout(LayoutKind.Explicit)] + internal struct XInputControllerWirelessOSXStateV2 : IInputStateTypeInfo + { + public static FourCC kFormat => new FourCC('H', 'I', 'D'); + + public enum Button + { + Start = 11, + Select = 10, + LeftThumbstickPress = 13, + RightThumbstickPress = 14, + LeftShoulder = 6, + RightShoulder = 7, + A = 0, + B = 1, + X = 3, + Y = 4, + } + [FieldOffset(0)] + private byte padding; + + [InputControl(name = "leftStick", layout = "Stick", format = "VC2S")] + [InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")] + [InputControl(name = "leftStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] + [InputControl(name = "leftStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")] + [InputControl(name = "leftStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")] + [InputControl(name = "leftStick/up", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] + [InputControl(name = "leftStick/down", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1,invert=false")] + [FieldOffset(1)] public ushort leftStickX; + [FieldOffset(3)] public ushort leftStickY; + + [InputControl(name = "rightStick", layout = "Stick", format = "VC2S")] + [InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")] + [InputControl(name = "rightStick/left", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] + [InputControl(name = "rightStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")] + [InputControl(name = "rightStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")] + [InputControl(name = "rightStick/up", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0,clampMax=0.5,invert")] + [InputControl(name = "rightStick/down", offset = 2, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1,invert=false")] + [FieldOffset(5)] public ushort rightStickX; + [FieldOffset(7)] public ushort rightStickY; + + [InputControl(name = "leftTrigger", format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=0.01560998")] + [FieldOffset(9)] public ushort leftTrigger; + [InputControl(name = "rightTrigger", format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=0.01560998")] + [FieldOffset(11)] public ushort rightTrigger; + + [InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 8)] + [InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=8,maxValue=2,nullValue=0,wrapAtValue=9", bit = 0, sizeInBits = 4)] + [InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=2,maxValue=4", bit = 0, sizeInBits = 4)] + [InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=4,maxValue=6", bit = 0, sizeInBits = 4)] + [InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=6, maxValue=8", bit = 0, sizeInBits = 4)] + [FieldOffset(13)] + public byte dpad; + + [InputControl(name = "start", bit = (uint)Button.Start, displayName = "Start")] + [InputControl(name = "select", bit = (uint)Button.Select, displayName = "Select")] + [InputControl(name = "leftStickPress", bit = (uint)Button.LeftThumbstickPress)] + [InputControl(name = "rightStickPress", bit = (uint)Button.RightThumbstickPress)] + [InputControl(name = "leftShoulder", bit = (uint)Button.LeftShoulder)] + [InputControl(name = "rightShoulder", bit = (uint)Button.RightShoulder)] + [InputControl(name = "buttonSouth", bit = (uint)Button.A, displayName = "A")] + [InputControl(name = "buttonEast", bit = (uint)Button.B, displayName = "B")] + [InputControl(name = "buttonWest", bit = (uint)Button.X, displayName = "X")] + [InputControl(name = "buttonNorth", bit = (uint)Button.Y, displayName = "Y")] + + [FieldOffset(14)] + public uint buttons; + + public FourCC format => kFormat; + + public XInputControllerWirelessOSXStateV2 WithButton(Button button) + { + Debug.Assert((int)button < 32, $"A maximum of 32 buttons is supported for this layout."); + buttons |= 1U << (int)button; + return this; + } + + public XInputControllerWirelessOSXStateV2 WithDpad(byte value) + { + dpad = value; + return this; + } + + public static XInputControllerWirelessOSXStateV2 defaultState => new XInputControllerWirelessOSXStateV2 + { + rightStickX = 32767, + rightStickY = 32767, + leftStickX = 32767, + leftStickY = 32767 + }; + } } namespace UnityEngine.InputSystem.XInput { @@ -223,5 +315,22 @@ public class XboxGamepadMacOS : XInputController public class XboxOneGampadMacOSWireless : XInputController { } + + /// + /// A wireless Xbox One or Xbox Series Gamepad connected to a macOS computer. + /// + /// + /// An Xbox One/Series wireless gamepad connected to a mac using Bluetooth. + /// The reason this is different from is that some Xbox Controllers have + /// different View and Share button bit mapping. So we need to use a different layout for those controllers. It seems + /// that some Xbox One and Xbox Series controller share the same mappings so this combines them all. + /// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work + /// with a proprietary Xbox wireless protocol, and cannot be used on a Mac. + /// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS. + /// + [InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)] + public class XboxGamepadMacOSWireless : XInputController + { + } } #endif // UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX