From 50d0e2ede2a634a2f41705b85ae65380677ee306 Mon Sep 17 00:00:00 2001 From: tmkk Date: Sun, 22 Feb 2026 14:33:28 +0900 Subject: [PATCH] Bring back support for 360Controller driver on macOS --- src/hidapi/mac/hid.c | 7 ++++ src/joystick/darwin/SDL_iokitjoystick.c | 17 ++++++-- src/joystick/hidapi/SDL_hidapi_xbox360.c | 52 +++++++++++++++++++++++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/hidapi/mac/hid.c b/src/hidapi/mac/hid.c index 4936fe1262..8b9645ab90 100644 --- a/src/hidapi/mac/hid.c +++ b/src/hidapi/mac/hid.c @@ -1138,6 +1138,8 @@ return_error: static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char *data, size_t length) { + const char *pass_through_magic = "MAGIC0"; + size_t pass_through_magic_length = strlen(pass_through_magic); const unsigned char *data_to_send = data; CFIndex length_to_send = length; IOReturn res; @@ -1158,6 +1160,11 @@ static int set_report(hid_device *dev, IOHIDReportType type, const unsigned char data_to_send = data+1; length_to_send = length-1; } + else if (length > 6 && memcmp(data, pass_through_magic, pass_through_magic_length) == 0) { + report_id = data[pass_through_magic_length]; + data_to_send = data+pass_through_magic_length; + length_to_send = length-pass_through_magic_length; + } /* Avoid crash if the device has been unplugged. */ if (dev->disconnected) { diff --git a/src/joystick/darwin/SDL_iokitjoystick.c b/src/joystick/darwin/SDL_iokitjoystick.c index 209f7d8205..75e7cc5201 100644 --- a/src/joystick/darwin/SDL_iokitjoystick.c +++ b/src/joystick/darwin/SDL_iokitjoystick.c @@ -28,6 +28,7 @@ #include "../hidapi/SDL_hidapijoystick_c.h" #include "../../haptic/darwin/SDL_syshaptic_c.h" // For haptic hot plugging #include "../usb_ids.h" +#include #define SDL_JOYSTICK_RUNLOOP_MODE CFSTR("SDLJoystick") @@ -442,6 +443,16 @@ static int GetSteamVirtualGamepadSlot(Uint16 vendor_id, Uint16 product_id, const return slot; } +static bool IsControlledBy360ControllerDriver(IOHIDDeviceRef hidDevice) +{ + bool controlled_by_360controller = false; + io_service_t service = IOHIDDeviceGetService(hidDevice); + if (service != MACH_PORT_NULL) { + controlled_by_360controller = IOObjectConformsTo(service, "Xbox360ControllerClass"); + } + return controlled_by_360controller; +} + static bool GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice) { Sint32 vendor = 0; @@ -499,8 +510,8 @@ static bool GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice) CFNumberGetValue(refCF, kCFNumberSInt32Type, &version); } - if (SDL_IsJoystickXboxOne(vendor, product)) { - // We can't actually use this API for Xbox controllers + if (!IsControlledBy360ControllerDriver(hidDevice) && SDL_IsJoystickXboxOne(vendor, product)) { + // We can't actually use this API for Xbox controllers without the 360Controller driver return false; } @@ -550,7 +561,7 @@ static bool JoystickAlreadyKnown(IOHIDDeviceRef ioHIDDeviceObject) #ifdef SDL_JOYSTICK_MFI extern bool IOS_SupportedHIDDevice(IOHIDDeviceRef device); - if (IOS_SupportedHIDDevice(ioHIDDeviceObject)) { + if (!IsControlledBy360ControllerDriver(ioHIDDeviceObject) && IOS_SupportedHIDDevice(ioHIDDeviceObject)) { return true; } #endif diff --git a/src/joystick/hidapi/SDL_hidapi_xbox360.c b/src/joystick/hidapi/SDL_hidapi_xbox360.c index e140cef92d..208771e5f9 100644 --- a/src/joystick/hidapi/SDL_hidapi_xbox360.c +++ b/src/joystick/hidapi/SDL_hidapi_xbox360.c @@ -32,6 +32,10 @@ // Define this if you want to log all packets from the controller // #define DEBUG_XBOX_PROTOCOL +#ifdef SDL_PLATFORM_MACOS +#include +#endif + typedef struct { SDL_HIDAPI_Device *device; @@ -39,6 +43,9 @@ typedef struct int player_index; bool player_lights; Uint8 last_state[USB_PACKET_LENGTH]; +#ifdef SDL_PLATFORM_MACOS + bool controlled_by_360controller; +#endif } SDL_DriverXbox360_Context; static void HIDAPI_DriverXbox360_RegisterHints(SDL_HintCallback callback, void *userdata) @@ -59,6 +66,22 @@ static bool HIDAPI_DriverXbox360_IsEnabled(void) SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI_XBOX, SDL_GetHintBoolean(SDL_HINT_JOYSTICK_HIDAPI, SDL_HIDAPI_DEFAULT))); } +#ifdef SDL_PLATFORM_MACOS +static bool IsControlledBy360ControllerDriverMacOS(SDL_HIDAPI_Device *device) +{ + bool controlled_by_360controller = false; + if (device && device->path && SDL_strncmp("DevSrvsID:", device->path, 10) == 0) { + uint64_t entry_id = SDL_strtoull(device->path + 10, NULL, 10); + io_service_t service = IOServiceGetMatchingService(0, IORegistryEntryIDMatching(entry_id)); + if (service != MACH_PORT_NULL) { + controlled_by_360controller = IOObjectConformsTo(service, "Xbox360ControllerClass"); + IOObjectRelease(service); + } + } + return controlled_by_360controller; +} +#endif + static bool HIDAPI_DriverXbox360_IsSupportedDevice(SDL_HIDAPI_Device *device, const char *name, SDL_GamepadType type, Uint16 vendor_id, Uint16 product_id, Uint16 version, int interface_number, int interface_class, int interface_subclass, int interface_protocol) { const int XB360W_IFACE_PROTOCOL = 129; // Wireless @@ -80,13 +103,22 @@ static bool HIDAPI_DriverXbox360_IsSupportedDevice(SDL_HIDAPI_Device *device, co // This is the chatpad or other input interface, not the Xbox 360 interface return false; } +#ifdef SDL_PLATFORM_MACOS + if (IsControlledBy360ControllerDriverMacOS(device)) { + // Wired Xbox controllers are handled by this driver, when they are + // controlled by the 360Controller driver available from: + // https://github.com/360Controller/360Controller/releases + return true; + } +#endif #if defined(SDL_PLATFORM_MACOS) && defined(SDL_JOYSTICK_MFI) if (SDL_IsJoystickSteamVirtualGamepad(vendor_id, product_id, version)) { // GCController support doesn't work with the Steam Virtual Gamepad return true; } else { - // On macOS you can't write output reports to wired XBox controllers, - // so we'll just use the GCController support instead. + // 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. return false; } #else @@ -138,6 +170,9 @@ static bool HIDAPI_DriverXbox360_InitDevice(SDL_HIDAPI_Device *device) return false; } ctx->device = device; +#ifdef SDL_PLATFORM_MACOS + ctx->controlled_by_360controller = IsControlledBy360ControllerDriverMacOS(device); +#endif device->context = ctx; @@ -198,6 +233,19 @@ static bool HIDAPI_DriverXbox360_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joy static bool HIDAPI_DriverXbox360_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble) { +#ifdef SDL_PLATFORM_MACOS + if (((SDL_DriverXbox360_Context *)device->context)->controlled_by_360controller) { + // On macOS the 360Controller driver uses this short report, + // and we need to prefix it with a magic token so hidapi passes it through untouched + Uint8 rumble_packet[] = { 'M', 'A', 'G', 'I', 'C', '0', 0x00, 0x04, 0x00, 0x00 }; + rumble_packet[6 + 2] = (low_frequency_rumble >> 8); + rumble_packet[6 + 3] = (high_frequency_rumble >> 8); + if (SDL_HIDAPI_SendRumble(device, rumble_packet, sizeof(rumble_packet)) != sizeof(rumble_packet)) { + return SDL_SetError("Couldn't send rumble packet"); + } + return true; + } +#endif Uint8 rumble_packet[] = { 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; rumble_packet[3] = (low_frequency_rumble >> 8);