Skip to content

Commit 29ca1f3

Browse files
FIX: Set macOS Xbox wireless gamepad button mappings based on PID/VID (#2097)
1 parent 7b21b83 commit 29ca1f3

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

Assets/Tests/InputSystem/APIVerificationTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ internal static bool IgnoreTypeForDocsByName(string fullName)
183183
#if UNITY_EDITOR_OSX
184184
fullName == typeof(UnityEngine.InputSystem.XInput.XboxGamepadMacOS).FullName ||
185185
fullName == typeof(UnityEngine.InputSystem.XInput.XboxOneGampadMacOSWireless).FullName ||
186+
fullName == typeof(UnityEngine.InputSystem.XInput.XboxGamepadMacOSWireless).FullName ||
186187
#endif
187188
#if UNITY_EDITOR_WIN
188189
fullName == typeof(UnityEngine.InputSystem.XInput.XInputControllerWindows).FullName ||

Assets/Tests/InputSystem/Plugins/XInputTests.cs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using UnityEngine.InputSystem.Layouts;
66
using UnityEngine.InputSystem.Utilities;
77
using System.Runtime.InteropServices;
8+
using UnityEngine.InputSystem.HID;
89
using UnityEngine.InputSystem.Processors;
910

1011
#if UNITY_EDITOR_WIN || UNITY_EDITOR_OSX || UNITY_XBOXONE || UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN
@@ -23,7 +24,7 @@ internal class XInputTests : CoreTestsFixture
2324
[Category("Devices")]
2425
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
2526
[TestCase("Xbox One Wired Controller", "Microsoft", "HID", "XboxGamepadMacOS")]
26-
[TestCase("Xbox One Wireless Controller", "Microsoft", "HID", "XboxOneGampadMacOSWireless")]
27+
[TestCase("Xbox Series Wireless Controller", "Microsoft", "HID", "XboxGamepadMacOSWireless")]
2728
#endif
2829
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN || UNITY_WSA
2930
[TestCase(null, null, "XInput", "XInputControllerWindows")]
@@ -148,6 +149,64 @@ public void Devices_SupportXboxControllerOnOSX()
148149
AssertButtonPress(gamepad, new XInputControllerOSXState().WithButton(XInputControllerOSXState.Button.Select), gamepad.selectButton);
149150
}
150151

152+
[TestCase(0x045E, 0x02E0, 16, 11)] // Xbox One Wireless Controller
153+
[TestCase(0x045E, 0x0B20, 10, 11)] // Xbox Series X|S Wireless Controller
154+
// This test is used to establish the correct button map layout based on the PID and VIDs. The usual difference
155+
// is around the select and start button bits.
156+
// If the layout is changed this test will fail and will need to be adapted either with a new device/layout or
157+
// a new button map.
158+
public void Devices_SupportWirelessXboxOneAndSeriesControllerOnOSX(int vendorId, int productId, int selectBit, int startBit)
159+
{
160+
// Fake a real Xbox Wireless Controller
161+
var xboxGamepad = InputSystem.AddDevice(new InputDeviceDescription
162+
{
163+
interfaceName = "HID",
164+
product = "Xbox Wireless Controller",
165+
manufacturer = "Microsoft",
166+
capabilities = new HID.HIDDeviceDescriptor
167+
{
168+
vendorId = vendorId,
169+
productId = productId,
170+
}.ToJson()
171+
});
172+
173+
174+
Assert.That(xboxGamepad, Is.AssignableTo<XInputController>());
175+
176+
var gamepad = (XInputController)xboxGamepad;
177+
Assert.That(gamepad.selectButton.isPressed, Is.False);
178+
179+
// Check if the controller is an Xbox One from a particular type where we know the select and start buttons are
180+
// different
181+
if (productId == 0x02e0)
182+
{
183+
Assert.That(xboxGamepad, Is.AssignableTo<XboxOneGampadMacOSWireless>());
184+
185+
InputSystem.QueueStateEvent(gamepad,
186+
new XInputControllerWirelessOSXState
187+
{
188+
buttons = (uint)(1 << selectBit |
189+
1 << startBit)
190+
});
191+
InputSystem.Update();
192+
}
193+
else
194+
{
195+
Assert.That(xboxGamepad, Is.AssignableTo<XboxGamepadMacOSWireless>());
196+
197+
InputSystem.QueueStateEvent(gamepad,
198+
new XInputControllerWirelessOSXState
199+
{
200+
buttons = (uint)(1 << selectBit |
201+
1 << startBit)
202+
});
203+
InputSystem.Update();
204+
}
205+
206+
Assert.That(gamepad.selectButton.isPressed);
207+
Assert.That(gamepad.startButton.isPressed);
208+
}
209+
151210
// Disable tests in standalone builds from 2022.1+ see UUM-19622
152211
#if !UNITY_STANDALONE_OSX || !TEMP_DISABLE_STANDALONE_OSX_XINPUT_TEST
153212
[Test]
@@ -158,7 +217,12 @@ public void Devices_SupportXboxWirelessControllerOnOSX()
158217
{
159218
interfaceName = "HID",
160219
product = "Xbox One Wireless Controller",
161-
manufacturer = "Microsoft"
220+
manufacturer = "Microsoft",
221+
capabilities = new HID.HIDDeviceDescriptor
222+
{
223+
vendorId = 0x045E,
224+
productId = 0x02E0,
225+
}.ToJson()
162226
});
163227

164228
Assert.That(device, Is.AssignableTo<XInputController>());

Packages/com.unity.inputsystem/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ however, it has to be formatted properly to pass verification tests.
4646
- 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)
4747
- 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)
4848
- 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)
49+
- 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)
4950

5051
### Changed
5152
- Changed location of the link xml file (code stripping rules), from a temporary directory to the project Library folder (ISX-2140).

Packages/com.unity.inputsystem/InputSystem/Editor/Debugger/InputDeviceDebuggerWindow.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ internal void OnGUI()
132132
EditorGUILayout.LabelField("Product", m_Device.description.product);
133133
if (!string.IsNullOrEmpty(m_Device.description.manufacturer))
134134
EditorGUILayout.LabelField("Manufacturer", m_Device.description.manufacturer);
135+
if (!string.IsNullOrEmpty(m_Device.description.version))
136+
EditorGUILayout.LabelField("Version", m_Device.description.version);
135137
if (!string.IsNullOrEmpty(m_Device.description.serial))
136138
EditorGUILayout.LabelField("Serial Number", m_Device.description.serial);
137139
EditorGUILayout.LabelField("Device ID", m_DeviceIdString);

Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XInputSupport.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,36 @@ public static void Initialize()
2828
InputSystem.RegisterLayout<XboxGamepadMacOS>(
2929
matches: new InputDeviceMatcher().WithInterface("HID")
3030
.WithProduct("Xbox.*Wired Controller"));
31-
InputSystem.RegisterLayout<XboxOneGampadMacOSWireless>(
31+
32+
// Matching older Xbox One controllers that have different View and Share buttons than the newer Xbox Series
33+
// controllers.
34+
// Reported inhttps://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-1264
35+
// Based on devices from this list
36+
// https://github.com/mdqinc/SDL_GameControllerDB/blob/a453871de2e0e2484544514c6c080e1e916d620c/gamecontrollerdb.txt#L798C1-L806C1
37+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02B0);
38+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02D1);
39+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02DD);
40+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02E0);
41+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02E3);
42+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02EA);
43+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02FD);
44+
RegisterXboxOneWirelessFromProductAndVendorID(0x045E, 0x02FF);
45+
46+
// This layout is for all the other Xbox One or Series controllers that have the same View and Share buttons.
47+
// Reported in https://issuetracker.unity3d.com/product/unity/issues/guid/ISXB-385
48+
InputSystem.RegisterLayout<XboxGamepadMacOSWireless>(
3249
matches: new InputDeviceMatcher().WithInterface("HID")
3350
.WithProduct("Xbox.*Wireless Controller"));
51+
52+
void RegisterXboxOneWirelessFromProductAndVendorID(int vendorId, int productId)
53+
{
54+
InputSystem.RegisterLayout<XboxOneGampadMacOSWireless>(
55+
matches: new InputDeviceMatcher().WithInterface("HID")
56+
.WithProduct("Xbox.*Wireless Controller")
57+
.WithCapability("vendorId", vendorId)
58+
.WithCapability("productId", productId));
59+
}
60+
3461
#endif
3562
}
3663
}

Packages/com.unity.inputsystem/InputSystem/Plugins/XInput/XboxGamepadMacOS.cs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public enum Button
9797

9898
public XInputControllerOSXState WithButton(Button button)
9999
{
100-
Debug.Assert((int)button < 16, $"Expected button < 16, so we fit into the 16 bit wide bitmask");
100+
Debug.Assert((int)button < 16, $"A maximum of 16 buttons is supported for this layout.");
101101
buttons |= (ushort)(1U << (int)button);
102102
return this;
103103
}
@@ -111,7 +111,7 @@ internal struct XInputControllerWirelessOSXState : IInputStateTypeInfo
111111
public enum Button
112112
{
113113
Start = 11,
114-
Select = 10,
114+
Select = 16,
115115
LeftThumbstickPress = 13,
116116
RightThumbstickPress = 14,
117117
LeftShoulder = 6,
@@ -175,7 +175,7 @@ public enum Button
175175

176176
public XInputControllerWirelessOSXState WithButton(Button button)
177177
{
178-
Debug.Assert((int)button < 32, $"Expected button < 32, so we fit into the 32 bit wide bitmask");
178+
Debug.Assert((int)button < 32, $"A maximum of 32 buttons is supported for this layout.");
179179
buttons |= 1U << (int)button;
180180
return this;
181181
}
@@ -194,6 +194,98 @@ public XInputControllerWirelessOSXState WithDpad(byte value)
194194
leftStickY = 32767
195195
};
196196
}
197+
198+
[StructLayout(LayoutKind.Explicit)]
199+
internal struct XInputControllerWirelessOSXStateV2 : IInputStateTypeInfo
200+
{
201+
public static FourCC kFormat => new FourCC('H', 'I', 'D');
202+
203+
public enum Button
204+
{
205+
Start = 11,
206+
Select = 10,
207+
LeftThumbstickPress = 13,
208+
RightThumbstickPress = 14,
209+
LeftShoulder = 6,
210+
RightShoulder = 7,
211+
A = 0,
212+
B = 1,
213+
X = 3,
214+
Y = 4,
215+
}
216+
[FieldOffset(0)]
217+
private byte padding;
218+
219+
[InputControl(name = "leftStick", layout = "Stick", format = "VC2S")]
220+
[InputControl(name = "leftStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
221+
[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")]
222+
[InputControl(name = "leftStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")]
223+
[InputControl(name = "leftStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
224+
[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")]
225+
[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")]
226+
[FieldOffset(1)] public ushort leftStickX;
227+
[FieldOffset(3)] public ushort leftStickY;
228+
229+
[InputControl(name = "rightStick", layout = "Stick", format = "VC2S")]
230+
[InputControl(name = "rightStick/x", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
231+
[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")]
232+
[InputControl(name = "rightStick/right", offset = 0, format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5,clamp=1,clampMin=0.5,clampMax=1")]
233+
[InputControl(name = "rightStick/y", offset = 2, format = "USHT", parameters = "invert,normalize,normalizeMin=0,normalizeMax=1,normalizeZero=0.5")]
234+
[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")]
235+
[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")]
236+
[FieldOffset(5)] public ushort rightStickX;
237+
[FieldOffset(7)] public ushort rightStickY;
238+
239+
[InputControl(name = "leftTrigger", format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=0.01560998")]
240+
[FieldOffset(9)] public ushort leftTrigger;
241+
[InputControl(name = "rightTrigger", format = "USHT", parameters = "normalize,normalizeMin=0,normalizeMax=0.01560998")]
242+
[FieldOffset(11)] public ushort rightTrigger;
243+
244+
[InputControl(name = "dpad", format = "BIT", layout = "Dpad", sizeInBits = 4, defaultState = 8)]
245+
[InputControl(name = "dpad/up", format = "BIT", layout = "DiscreteButton", parameters = "minValue=8,maxValue=2,nullValue=0,wrapAtValue=9", bit = 0, sizeInBits = 4)]
246+
[InputControl(name = "dpad/right", format = "BIT", layout = "DiscreteButton", parameters = "minValue=2,maxValue=4", bit = 0, sizeInBits = 4)]
247+
[InputControl(name = "dpad/down", format = "BIT", layout = "DiscreteButton", parameters = "minValue=4,maxValue=6", bit = 0, sizeInBits = 4)]
248+
[InputControl(name = "dpad/left", format = "BIT", layout = "DiscreteButton", parameters = "minValue=6, maxValue=8", bit = 0, sizeInBits = 4)]
249+
[FieldOffset(13)]
250+
public byte dpad;
251+
252+
[InputControl(name = "start", bit = (uint)Button.Start, displayName = "Start")]
253+
[InputControl(name = "select", bit = (uint)Button.Select, displayName = "Select")]
254+
[InputControl(name = "leftStickPress", bit = (uint)Button.LeftThumbstickPress)]
255+
[InputControl(name = "rightStickPress", bit = (uint)Button.RightThumbstickPress)]
256+
[InputControl(name = "leftShoulder", bit = (uint)Button.LeftShoulder)]
257+
[InputControl(name = "rightShoulder", bit = (uint)Button.RightShoulder)]
258+
[InputControl(name = "buttonSouth", bit = (uint)Button.A, displayName = "A")]
259+
[InputControl(name = "buttonEast", bit = (uint)Button.B, displayName = "B")]
260+
[InputControl(name = "buttonWest", bit = (uint)Button.X, displayName = "X")]
261+
[InputControl(name = "buttonNorth", bit = (uint)Button.Y, displayName = "Y")]
262+
263+
[FieldOffset(14)]
264+
public uint buttons;
265+
266+
public FourCC format => kFormat;
267+
268+
public XInputControllerWirelessOSXStateV2 WithButton(Button button)
269+
{
270+
Debug.Assert((int)button < 32, $"A maximum of 32 buttons is supported for this layout.");
271+
buttons |= 1U << (int)button;
272+
return this;
273+
}
274+
275+
public XInputControllerWirelessOSXStateV2 WithDpad(byte value)
276+
{
277+
dpad = value;
278+
return this;
279+
}
280+
281+
public static XInputControllerWirelessOSXStateV2 defaultState => new XInputControllerWirelessOSXStateV2
282+
{
283+
rightStickX = 32767,
284+
rightStickY = 32767,
285+
leftStickX = 32767,
286+
leftStickY = 32767
287+
};
288+
}
197289
}
198290
namespace UnityEngine.InputSystem.XInput
199291
{
@@ -223,5 +315,22 @@ public class XboxGamepadMacOS : XInputController
223315
public class XboxOneGampadMacOSWireless : XInputController
224316
{
225317
}
318+
319+
/// <summary>
320+
/// A wireless Xbox One or Xbox Series Gamepad connected to a macOS computer.
321+
/// </summary>
322+
/// <remarks>
323+
/// An Xbox One/Series wireless gamepad connected to a mac using Bluetooth.
324+
/// The reason this is different from <see cref="XboxOneGampadMacOSWireless"/> is that some Xbox Controllers have
325+
/// different View and Share button bit mapping. So we need to use a different layout for those controllers. It seems
326+
/// that some Xbox One and Xbox Series controller share the same mappings so this combines them all.
327+
/// Note: only the latest version of Xbox One wireless gamepads support Bluetooth. Older models only work
328+
/// with a proprietary Xbox wireless protocol, and cannot be used on a Mac.
329+
/// Unlike wired controllers, bluetooth-cabable Xbox One controllers do not need a custom driver to work on macOS.
330+
/// </remarks>
331+
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)]
332+
public class XboxGamepadMacOSWireless : XInputController
333+
{
334+
}
226335
}
227336
#endif // UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX

0 commit comments

Comments
 (0)