Skip to content

Commit

Permalink
FIX: Set macOS Xbox wireless gamepad button mappings based on PID/VID (
Browse files Browse the repository at this point in the history
  • Loading branch information
jfreire-unity authored Jan 20, 2025
1 parent 7b21b83 commit 29ca1f3
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 6 deletions.
1 change: 1 addition & 0 deletions Assets/Tests/InputSystem/APIVerificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
68 changes: 66 additions & 2 deletions Assets/Tests/InputSystem/Plugins/XInputTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")]
Expand Down Expand Up @@ -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<XInputController>());

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<XboxOneGampadMacOSWireless>());

InputSystem.QueueStateEvent(gamepad,
new XInputControllerWirelessOSXState
{
buttons = (uint)(1 << selectBit |
1 << startBit)
});
InputSystem.Update();
}
else
{
Assert.That(xboxGamepad, Is.AssignableTo<XboxGamepadMacOSWireless>());

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]
Expand All @@ -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<XInputController>());
Expand Down
1 change: 1 addition & 0 deletions Packages/com.unity.inputsystem/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,36 @@ public static void Initialize()
InputSystem.RegisterLayout<XboxGamepadMacOS>(
matches: new InputDeviceMatcher().WithInterface("HID")
.WithProduct("Xbox.*Wired Controller"));
InputSystem.RegisterLayout<XboxOneGampadMacOSWireless>(

// 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<XboxGamepadMacOSWireless>(
matches: new InputDeviceMatcher().WithInterface("HID")
.WithProduct("Xbox.*Wireless Controller"));

void RegisterXboxOneWirelessFromProductAndVendorID(int vendorId, int productId)
{
InputSystem.RegisterLayout<XboxOneGampadMacOSWireless>(
matches: new InputDeviceMatcher().WithInterface("HID")
.WithProduct("Xbox.*Wireless Controller")
.WithCapability("vendorId", vendorId)
.WithCapability("productId", productId));
}

#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -111,7 +111,7 @@ internal struct XInputControllerWirelessOSXState : IInputStateTypeInfo
public enum Button
{
Start = 11,
Select = 10,
Select = 16,
LeftThumbstickPress = 13,
RightThumbstickPress = 14,
LeftShoulder = 6,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
{
Expand Down Expand Up @@ -223,5 +315,22 @@ public class XboxGamepadMacOS : XInputController
public class XboxOneGampadMacOSWireless : XInputController
{
}

/// <summary>
/// A wireless Xbox One or Xbox Series Gamepad connected to a macOS computer.
/// </summary>
/// <remarks>
/// An Xbox One/Series wireless gamepad connected to a mac using Bluetooth.
/// The reason this is different from <see cref="XboxOneGampadMacOSWireless"/> 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.
/// </remarks>
[InputControlLayout(displayName = "Wireless Xbox Controller", stateType = typeof(XInputControllerWirelessOSXStateV2), hideInUI = true)]
public class XboxGamepadMacOSWireless : XInputController
{
}
}
#endif // UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX

0 comments on commit 29ca1f3

Please sign in to comment.