From 18fc4d931a942e36f372218902b6319d113d0edd Mon Sep 17 00:00:00 2001 From: Sam Lantinga 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. --- 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 | 35 ++++++++++++------------ 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 | 13 +++++---- src/joystick/hidapi/SDL_hidapi_xboxone.c | 3 +- 10 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c index 0dda4d4ba1..3f42b3da2c 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 a4be1c74e1..706d96a30c 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 8b6abd97ae..f1cb937fa4 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 fe7b2e38db..47dc4f7690 100644 --- a/src/hidapi/ios/hid.m +++ b/src/hidapi/ios/hid.m @@ -995,7 +995,7 @@ struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, } /* 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 c5e68d7e19..6c5126c7f3 100644 --- a/src/hidapi/libusb/hid.c +++ b/src/hidapi/libusb/hid.c @@ -911,23 +911,29 @@ 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 +#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 + + /* Enumerate Xbox 360 and Xbox One controllers */ + if (is_xbox) + return 1; + if (intf_desc->bInterfaceClass == LIBUSB_CLASS_HID) return 1; - /* Also enumerate Xbox 360 controllers */ - if (is_xbox360(vendor_id, intf_desc)) - return 1; - - /* Also enumerate Xbox One controllers */ - if (is_xboxone(vendor_id, intf_desc)) - 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 47233d1f8f..5af8872c8c 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 79d86bee19..95c387ae43 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 3d0b4d41d7..948dffe14a 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 fb4fbd695e..bb28c79ca2 100644 --- a/src/joystick/hidapi/SDL_hidapi_xbox360.c +++ b/src/joystick/hidapi/SDL_hidapi_xbox360.c @@ -112,8 +112,8 @@ static void FetchXInputCapabilities(SDL_HIDAPI_Device *device) const struct libusb_interface *intf = &conf_desc->interface[device->interface_number]; intf_desc = &intf->altsetting[0]; if (intf_desc->extra_length == 17 && intf_desc->extra[1] == 0x21) { - ctx->capabilities.type = intf_desc->extra[3]; - ctx->capabilities.subType = intf_desc->extra[4]; + ctx->capabilities.type = intf_desc->extra[3]; + ctx->capabilities.subType = intf_desc->extra[4]; switch (ctx->capabilities.subType) { case 0x01: // XINPUT_DEVSUBTYPE_GAMEPAD device->joystick_type = SDL_JOYSTICK_TYPE_GAMEPAD; @@ -177,7 +177,7 @@ static void FetchXInputCapabilities(SDL_HIDAPI_Device *device) SDL_Log(" wLeftMotorSpeed: %02x", ctx->capabilities.vibration.wLeftMotorSpeed); SDL_Log(" wRightMotorSpeed: %02x", ctx->capabilities.vibration.wRightMotorSpeed); #endif - } + } SDL_QuitLibUSB(); } } @@ -217,10 +217,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 2f6cf14cf2..5d97876f74 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.