From fcb29e4fb462042b1dbb055986351f5669b9e4aa Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 9 Jun 2026 12:04:18 -0700
Subject: [PATCH] Added support for Xbox controllers via libusb on macOS
A number of third party Xbox controllers are not supported by macOS, but work with libusb and the SDL HIDAPI driver.
(cherry picked from commit 18fc4d931a942e36f372218902b6319d113d0edd)
---
src/hidapi/SDL_hidapi.c | 23 +++++++++++++-----
src/hidapi/SDL_hidapi_c.h | 2 +-
src/hidapi/android/hid.cpp | 2 +-
src/hidapi/ios/hid.m | 2 +-
src/hidapi/libusb/hid.c | 31 ++++++++++++------------
src/hidapi/linux/hid.c | 2 +-
src/hidapi/mac/hid.c | 2 +-
src/hidapi/windows/hid.c | 2 +-
src/joystick/hidapi/SDL_hidapi_xbox360.c | 7 +++---
src/joystick/hidapi/SDL_hidapi_xboxone.c | 3 ++-
10 files changed, 44 insertions(+), 32 deletions(-)
diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c
index 0dda4d4ba18a7..3f42b3da2c40f 100644
--- a/src/hidapi/SDL_hidapi.c
+++ b/src/hidapi/SDL_hidapi.c
@@ -543,8 +543,8 @@ static void HIDAPI_ShutdownDiscovery(void)
// Platform HIDAPI Implementation
#define HIDAPI_USING_SDL_RUNTIME
-#define HIDAPI_IGNORE_DEVICE(BUS, VID, PID, USAGE_PAGE, USAGE, LIBUSB) \
- SDL_HIDAPI_ShouldIgnoreDevice(BUS, VID, PID, USAGE_PAGE, USAGE, LIBUSB)
+#define HIDAPI_IGNORE_DEVICE(BUS, VID, PID, USAGE_PAGE, USAGE, LIBUSB, LIBUSB_XBOX) \
+ SDL_HIDAPI_ShouldIgnoreDevice(BUS, VID, PID, USAGE_PAGE, USAGE, LIBUSB, LIBUSB_XBOX)
struct PLATFORM_hid_device_;
typedef struct PLATFORM_hid_device_ PLATFORM_hid_device;
@@ -859,7 +859,7 @@ static const struct {
{ USB_VENDOR_NINTENDO, USB_PRODUCT_NINTENDO_SWITCH2_PRO },
};
-static bool RequiresLibUSB(Uint16 vendor, Uint16 product)
+static bool RequiresLibUSB(Uint16 vendor, Uint16 product, bool libusb_xbox)
{
for (int i = 0; i < SDL_arraysize(SDL_libusb_required); ++i) {
if (vendor == SDL_libusb_required[i].vendor &&
@@ -867,6 +867,17 @@ static bool RequiresLibUSB(Uint16 vendor, Uint16 product)
return true;
}
}
+
+#ifdef SDL_PLATFORM_MACOS
+ // On macOS we want to use libusb if possible for Xbox controllers
+ // that are not supported by the OS. Opening the device via libusb
+ // will fail if the device is supported (and opened) by the OS, so
+ // any devices we return here and can open are fair game.
+ if (libusb_xbox) {
+ return true;
+ }
+#endif // SDL_PLATFORM_MACOS
+
return false;
}
@@ -1054,10 +1065,10 @@ static void SDLCALL IgnoredDevicesChanged(void *userdata, const char *name, cons
}
}
-bool SDL_HIDAPI_ShouldIgnoreDevice(int bus, Uint16 vendor_id, Uint16 product_id, Uint16 usage_page, Uint16 usage, bool libusb)
+bool SDL_HIDAPI_ShouldIgnoreDevice(int bus, Uint16 vendor_id, Uint16 product_id, Uint16 usage_page, Uint16 usage, bool libusb, bool libusb_xbox)
{
if (libusb) {
- if (use_libusb_whitelist && !RequiresLibUSB(vendor_id, product_id)) {
+ if (use_libusb_whitelist && !RequiresLibUSB(vendor_id, product_id, libusb_xbox)) {
return true;
}
if (!use_libusb_gamecube &&
@@ -1065,7 +1076,7 @@ bool SDL_HIDAPI_ShouldIgnoreDevice(int bus, Uint16 vendor_id, Uint16 product_id,
return true;
}
} else {
- if (RequiresLibUSB(vendor_id, product_id)) {
+ if (RequiresLibUSB(vendor_id, product_id, libusb_xbox)) {
return true;
}
}
diff --git a/src/hidapi/SDL_hidapi_c.h b/src/hidapi/SDL_hidapi_c.h
index a4be1c74e12df..706d96a30c7a0 100644
--- a/src/hidapi/SDL_hidapi_c.h
+++ b/src/hidapi/SDL_hidapi_c.h
@@ -22,5 +22,5 @@
/* Return true if the HIDAPI should ignore a device during enumeration */
-extern bool SDL_HIDAPI_ShouldIgnoreDevice(int bus_type, Uint16 vendor_id, Uint16 product_id, Uint16 usage_page, Uint16 usage, bool libusb);
+extern bool SDL_HIDAPI_ShouldIgnoreDevice(int bus_type, Uint16 vendor_id, Uint16 product_id, Uint16 usage_page, Uint16 usage, bool libusb, bool is_xbox);
diff --git a/src/hidapi/android/hid.cpp b/src/hidapi/android/hid.cpp
index 8b6abd97aeca6..f1cb937fa4996 100644
--- a/src/hidapi/android/hid.cpp
+++ b/src/hidapi/android/hid.cpp
@@ -1114,7 +1114,7 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor
const hid_device_info *info = pDevice->GetDeviceInfo();
/* See if there are any devices we should skip in enumeration */
- if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_UNKNOWN, info->vendor_id, info->product_id, 0, 0, false)) {
+ if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_UNKNOWN, info->vendor_id, info->product_id, 0, 0, false, false)) {
continue;
}
diff --git a/src/hidapi/ios/hid.m b/src/hidapi/ios/hid.m
index fe7b2e38dba3f..47dc4f76909a8 100644
--- a/src/hidapi/ios/hid.m
+++ b/src/hidapi/ios/hid.m
@@ -995,7 +995,7 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
}
/* See if there are any devices we should skip in enumeration */
- if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, device.pid, 0, 0, false)) {
+ if (SDL_HIDAPI_ShouldIgnoreDevice(HID_API_BUS_BLUETOOTH, VALVE_USB_VID, device.pid, 0, 0, false, false)) {
continue;
}
diff --git a/src/hidapi/libusb/hid.c b/src/hidapi/libusb/hid.c
index c5e68d7e19324..6c5126c7f3855 100644
--- a/src/hidapi/libusb/hid.c
+++ b/src/hidapi/libusb/hid.c
@@ -911,21 +911,27 @@ static int is_xboxone(unsigned short vendor_id, const struct libusb_interface_de
return 0;
}
-static int should_enumerate_interface(unsigned short vendor_id, const struct libusb_interface_descriptor *intf_desc)
+static int should_enumerate_interface(unsigned short vendor_id, unsigned short product_id, const struct libusb_interface_descriptor *intf_desc)
{
+ int is_xbox = (is_xbox360(vendor_id, intf_desc) ||
+ is_xboxone(vendor_id, intf_desc));
+
#if 0
printf("Checking interface 0x%x %d/%d/%d/%d\n", vendor_id, intf_desc->bInterfaceNumber, intf_desc->bInterfaceClass, intf_desc->bInterfaceSubClass, intf_desc->bInterfaceProtocol);
#endif
- if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID)
- return 1;
+#ifdef HIDAPI_IGNORE_DEVICE
+ /* See if there are any devices we should skip in enumeration */
+ if (HIDAPI_IGNORE_DEVICE(HID_API_BUS_USB, vendor_id, product_id, 0, 0, true, is_xbox)) {
+ return 0;
+ }
+#endif
- /* Also enumerate Xbox 360 controllers */
- if (is_xbox360(vendor_id, intf_desc))
+ /* Enumerate Xbox 360 and Xbox One controllers */
+ if (is_xbox)
return 1;
- /* Also enumerate Xbox One controllers */
- if (is_xboxone(vendor_id, intf_desc))
+ if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID)
return 1;
return 0;
@@ -982,13 +988,6 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
continue;
}
-#ifdef HIDAPI_IGNORE_DEVICE
- /* See if there are any devices we should skip in enumeration */
- if (HIDAPI_IGNORE_DEVICE(HID_API_BUS_USB, dev_vid, dev_pid, 0, 0, true)) {
- continue;
- }
-#endif
-
res = libusb_get_active_config_descriptor(dev, &conf_desc);
if (res < 0)
libusb_get_config_descriptor(dev, 0, &conf_desc);
@@ -998,7 +997,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id,
for (k = 0; k < intf->num_altsetting; k++) {
const struct libusb_interface_descriptor *intf_desc;
intf_desc = &intf->altsetting[k];
- if (should_enumerate_interface(dev_vid, intf_desc)) {
+ if (should_enumerate_interface(dev_vid, dev_pid, intf_desc)) {
struct hid_device_info *tmp;
res = libusb_open(dev, &handle);
@@ -1485,7 +1484,7 @@ HID_API_EXPORT hid_device *hid_open_path(const char *path)
const struct libusb_interface *intf = &conf_desc->interface[j];
for (k = 0; k < intf->num_altsetting && !good_open; k++) {
const struct libusb_interface_descriptor *intf_desc = &intf->altsetting[k];
- if (should_enumerate_interface(desc.idVendor, intf_desc)) {
+ if (should_enumerate_interface(desc.idVendor, desc.idProduct, intf_desc)) {
char dev_path[64];
get_path(&dev_path, usb_dev, conf_desc->bConfigurationValue, intf_desc->bInterfaceNumber);
if (!strcmp(dev_path, path)) {
diff --git a/src/hidapi/linux/hid.c b/src/hidapi/linux/hid.c
index c0cb6a7b0e255..a66f62e4e6cf0 100644
--- a/src/hidapi/linux/hid.c
+++ b/src/hidapi/linux/hid.c
@@ -906,7 +906,7 @@ static struct hid_device_info * create_device_info_for_device(struct udev_device
cur_dev = root;
while (cur_dev) {
- if (HIDAPI_IGNORE_DEVICE(cur_dev->bus_type, cur_dev->vendor_id, cur_dev->product_id, cur_dev->usage_page, cur_dev->usage, false)) {
+ if (HIDAPI_IGNORE_DEVICE(cur_dev->bus_type, cur_dev->vendor_id, cur_dev->product_id, cur_dev->usage_page, cur_dev->usage, false, false)) {
struct hid_device_info *tmp = cur_dev;
cur_dev = tmp->next;
diff --git a/src/hidapi/mac/hid.c b/src/hidapi/mac/hid.c
index 79d86bee19c81..95c387ae435fc 100644
--- a/src/hidapi/mac/hid.c
+++ b/src/hidapi/mac/hid.c
@@ -598,7 +598,7 @@ static struct hid_device_info *create_device_info_with_usage(IOHIDDeviceRef dev,
#ifdef HIDAPI_IGNORE_DEVICE
/* See if there are any devices we should skip in enumeration */
- if (HIDAPI_IGNORE_DEVICE(get_bus_type(dev), dev_vid, dev_pid, usage_page, usage, false)) {
+ if (HIDAPI_IGNORE_DEVICE(get_bus_type(dev), dev_vid, dev_pid, usage_page, usage, false, false)) {
free(cur_dev);
return NULL;
}
diff --git a/src/hidapi/windows/hid.c b/src/hidapi/windows/hid.c
index 7579978232a08..7e02c635a1968 100644
--- a/src/hidapi/windows/hid.c
+++ b/src/hidapi/windows/hid.c
@@ -1046,7 +1046,7 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor
HidP_GetCaps(pp_data, &caps);
HidD_FreePreparsedData(pp_data);
}
- if (HIDAPI_IGNORE_DEVICE(bus_type, attrib.VendorID, attrib.ProductID, caps.UsagePage, caps.Usage, false)) {
+ if (HIDAPI_IGNORE_DEVICE(bus_type, attrib.VendorID, attrib.ProductID, caps.UsagePage, caps.Usage, false, false)) {
goto cont_close;
}
#endif
diff --git a/src/joystick/hidapi/SDL_hidapi_xbox360.c b/src/joystick/hidapi/SDL_hidapi_xbox360.c
index 09767e412732f..8ada0f9843ab8 100644
--- a/src/joystick/hidapi/SDL_hidapi_xbox360.c
+++ b/src/joystick/hidapi/SDL_hidapi_xbox360.c
@@ -117,10 +117,11 @@ static bool HIDAPI_DriverXbox360_IsSupportedDevice(SDL_HIDAPI_Device *device, co
if (SDL_IsJoystickSteamVirtualGamepad(vendor_id, product_id, version)) {
// GCController support doesn't work with the Steam Virtual Gamepad
return true;
- } else {
+ }
+ if (device && SDL_strncmp(device->path, "DevSrvsID", 9) == 0) {
// On macOS when it isn't controlled by the 360Controller driver and
- // it doesn't look like a Steam virtual gamepad we should rely on
- // GCController support instead.
+ // it doesn't look like a Steam virtual gamepad and it's not
+ // available via libusb we should rely on GCController support.
return false;
}
}
diff --git a/src/joystick/hidapi/SDL_hidapi_xboxone.c b/src/joystick/hidapi/SDL_hidapi_xboxone.c
index 2f6cf14cf2550..5d97876f7461b 100644
--- a/src/joystick/hidapi/SDL_hidapi_xboxone.c
+++ b/src/joystick/hidapi/SDL_hidapi_xboxone.c
@@ -369,7 +369,8 @@ static bool HIDAPI_DriverXboxOne_IsSupportedDevice(SDL_HIDAPI_Device *device, co
#if defined(SDL_PLATFORM_MACOS) && defined(SDL_JOYSTICK_MFI)
if (SDL_GetHintBoolean(SDL_HINT_JOYSTICK_MFI, true) &&
- !SDL_IsJoystickBluetoothXboxOne(vendor_id, product_id)) {
+ !SDL_IsJoystickBluetoothXboxOne(vendor_id, product_id) &&
+ (device && SDL_strncmp(device->path, "DevSrvsID", 9) == 0)) {
// On macOS we get a shortened version of the real report and
// you can't write output reports for wired controllers, so
// we'll just use the GCController support instead, if available.