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.
This commit is contained in:
Sam Lantinga
2026-06-09 12:04:18 -07:00
parent 36f621842b
commit 18fc4d931a
10 changed files with 49 additions and 37 deletions

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)) {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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.