From 101273f429a336615218b7790ea677c5222e9d81 Mon Sep 17 00:00:00 2001
From: Sanjay Govind <[EMAIL REDACTED]>
Date: Mon, 9 Mar 2026 13:29:56 +1300
Subject: [PATCH] extract capabilities for 360 controllers over libusb (#15183)
Read capabilities when using xinput controllers via the libusb backend
This gives us access to the subtype on linux and macOS, and gives us a lot of data we can use for handling more detailed device types when I look into a unified api for exposing instrument data later.
---
src/joystick/hidapi/SDL_hidapi_xbox360.c | 104 +++++++++++++++++++++-
src/joystick/hidapi/SDL_hidapi_xbox360.h | 27 ++++++
src/joystick/hidapi/SDL_hidapi_xbox360w.c | 78 ++++++++++++++++
3 files changed, 208 insertions(+), 1 deletion(-)
create mode 100644 src/joystick/hidapi/SDL_hidapi_xbox360.h
diff --git a/src/joystick/hidapi/SDL_hidapi_xbox360.c b/src/joystick/hidapi/SDL_hidapi_xbox360.c
index 208771e5f9d32..b3393c4cf89fc 100644
--- a/src/joystick/hidapi/SDL_hidapi_xbox360.c
+++ b/src/joystick/hidapi/SDL_hidapi_xbox360.c
@@ -23,9 +23,11 @@
#ifdef SDL_JOYSTICK_HIDAPI
#include "../../SDL_hints_c.h"
+#include "../../misc/SDL_libusb.h"
#include "../SDL_sysjoystick.h"
#include "SDL_hidapijoystick_c.h"
#include "SDL_hidapi_rumble.h"
+#include "SDL_hidapi_xbox360.h"
#ifdef SDL_JOYSTICK_HIDAPI_XBOX360
@@ -42,6 +44,7 @@ typedef struct
SDL_Joystick *joystick;
int player_index;
bool player_lights;
+ SDL_xinput_capabilities capabilities;
Uint8 last_state[USB_PACKET_LENGTH];
#ifdef SDL_PLATFORM_MACOS
bool controlled_by_360controller;
@@ -82,6 +85,103 @@ static bool IsControlledBy360ControllerDriverMacOS(SDL_HIDAPI_Device *device)
}
#endif
+#ifdef HAVE_LIBUSB
+static void FetchXInputCapabilities(SDL_HIDAPI_Device *device)
+{
+ SDL_DriverXbox360_Context *ctx = (SDL_DriverXbox360_Context *)device->context;
+ SDL_LibUSBContext *libusb_ctx;
+ if (SDL_InitLibUSB(&libusb_ctx)) {
+ libusb_device_handle *handle = (libusb_device_handle *)SDL_GetPointerProperty(SDL_hid_get_properties(device->dev), SDL_PROP_HIDAPI_LIBUSB_DEVICE_HANDLE_POINTER, NULL);
+ if (handle == NULL) {
+ SDL_QuitLibUSB();
+ return;
+ }
+ libusb_device *dev = libusb_ctx->get_device(handle);
+ if (dev == NULL) {
+ SDL_QuitLibUSB();
+ return;
+ }
+ struct libusb_config_descriptor *conf_desc = NULL;
+ const struct libusb_interface_descriptor *intf_desc;
+ libusb_ctx->get_active_config_descriptor(dev, &conf_desc);
+ if (conf_desc == NULL || conf_desc->bNumInterfaces < device->interface_number) {
+ SDL_QuitLibUSB();
+ return;
+ }
+ 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];
+ switch (ctx->capabilities.subType) {
+ case 0x01: // XINPUT_DEVSUBTYPE_GAMEPAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_GAMEPAD;
+ break;
+ case 0x02: // XINPUT_DEVSUBTYPE_WHEEL
+ device->joystick_type = SDL_JOYSTICK_TYPE_WHEEL;
+ break;
+ case 0x03: // XINPUT_DEVSUBTYPE_ARCADE_STICK
+ device->joystick_type = SDL_JOYSTICK_TYPE_ARCADE_STICK;
+ break;
+ case 0x04: // XINPUT_DEVSUBTYPE_FLIGHT_STICK
+ device->joystick_type = SDL_JOYSTICK_TYPE_FLIGHT_STICK;
+ break;
+ case 0x05: // XINPUT_DEVSUBTYPE_DANCE_PAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_DANCE_PAD;
+ break;
+ case 0x06: // XINPUT_DEVSUBTYPE_GUITAR
+ case 0x07: // XINPUT_DEVSUBTYPE_GUITAR_ALTERNATE
+ case 0x0B: // XINPUT_DEVSUBTYPE_GUITAR_BASS
+ device->joystick_type = SDL_JOYSTICK_TYPE_GUITAR;
+ break;
+ case 0x08: // XINPUT_DEVSUBTYPE_DRUM_KIT
+ device->joystick_type = SDL_JOYSTICK_TYPE_DRUM_KIT;
+ break;
+ case 0x13: // XINPUT_DEVSUBTYPE_ARCADE_PAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_ARCADE_PAD;
+ break;
+ default:
+ break;
+ }
+ device->guid.data[15] = ctx->capabilities.subType;
+ unsigned char buf[20];
+ int ret = libusb_ctx->control_transfer(handle, 0xC1, 0x01, 0x100, 0x0, buf, sizeof(buf), 100);
+ if (ret == sizeof(buf)) {
+ ctx->capabilities.flags = LOAD16(buf[18], buf[19]);
+ ctx->capabilities.gamepad.wButtons = LOAD16(buf[2], buf[3]);
+ ctx->capabilities.gamepad.bLeftTrigger = buf[4];
+ ctx->capabilities.gamepad.bRightTrigger = buf[5];
+ ctx->capabilities.gamepad.sThumbLX = LOAD16(buf[6], buf[7]);
+ ctx->capabilities.gamepad.sThumbLY = LOAD16(buf[8], buf[9]);
+ ctx->capabilities.gamepad.sThumbRX = LOAD16(buf[10], buf[11]);
+ ctx->capabilities.gamepad.sThumbRY = LOAD16(buf[12], buf[13]);
+ }
+ ret = libusb_ctx->control_transfer(handle, 0xC1, 0x01, 0x00, 0x0, buf, 8, 100);
+ if (ret == 8) {
+ ctx->capabilities.vibration.wLeftMotorSpeed = buf[3] << 8;
+ ctx->capabilities.vibration.wRightMotorSpeed = buf[4] << 8;
+ }
+#ifdef DEBUG_XBOX_PROTOCOL
+ SDL_Log("Xbox 360 capabilities:");
+ SDL_Log(" type: %02x", ctx->capabilities.type);
+ SDL_Log(" subType: %02x", ctx->capabilities.subType);
+ SDL_Log(" flags: %04x", ctx->capabilities.flags);
+ SDL_Log(" wButtons: %02x", ctx->capabilities.gamepad.wButtons);
+ SDL_Log(" bLeftTrigger: %02x", ctx->capabilities.gamepad.bLeftTrigger);
+ SDL_Log(" bRightTrigger: %02x", ctx->capabilities.gamepad.bRightTrigger);
+ SDL_Log(" sThumbLX: %02x", ctx->capabilities.gamepad.sThumbLX);
+ SDL_Log(" sThumbLY: %02x", ctx->capabilities.gamepad.sThumbLY);
+ SDL_Log(" sThumbRX: %02x", ctx->capabilities.gamepad.sThumbRX);
+ SDL_Log(" sThumbRY: %02x", ctx->capabilities.gamepad.sThumbRY);
+ SDL_Log(" wLeftMotorSpeed: %02x", ctx->capabilities.vibration.wLeftMotorSpeed);
+ SDL_Log(" wRightMotorSpeed: %02x", ctx->capabilities.vibration.wRightMotorSpeed);
+#endif
+ }
+ SDL_QuitLibUSB();
+ }
+}
+#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
@@ -227,7 +327,9 @@ static bool HIDAPI_DriverXbox360_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joy
joystick->nbuttons = 11;
joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
joystick->nhats = 1;
-
+#ifdef HAVE_LIBUSB
+ FetchXInputCapabilities(device);
+#endif
return true;
}
diff --git a/src/joystick/hidapi/SDL_hidapi_xbox360.h b/src/joystick/hidapi/SDL_hidapi_xbox360.h
new file mode 100644
index 0000000000000..df866b36d2a70
--- /dev/null
+++ b/src/joystick/hidapi/SDL_hidapi_xbox360.h
@@ -0,0 +1,27 @@
+#include "SDL_internal.h"
+
+#define FLAG_FORCE_FEEDBACK 0x01
+#define FLAG_WIRELESS 0x02
+#define FLAG_VOICE 0x04
+#define FLAG_PLUGIN_MODULES 0x08
+#define FLAG_NO_NAVIGATION 0x10
+
+typedef struct {
+ uint8_t type;
+ uint8_t subType;
+ uint16_t flags;
+ struct {
+ uint16_t wButtons;
+ uint8_t bLeftTrigger;
+ uint8_t bRightTrigger;
+ int16_t sThumbLX;
+ int16_t sThumbLY;
+ int16_t sThumbRX;
+ int16_t sThumbRY;
+ } gamepad;
+
+ struct {
+ uint16_t wLeftMotorSpeed;
+ uint16_t wRightMotorSpeed;
+ } vibration;
+} SDL_xinput_capabilities;
\ No newline at end of file
diff --git a/src/joystick/hidapi/SDL_hidapi_xbox360w.c b/src/joystick/hidapi/SDL_hidapi_xbox360w.c
index 57206bb8f88a9..81ea01e67cbaa 100644
--- a/src/joystick/hidapi/SDL_hidapi_xbox360w.c
+++ b/src/joystick/hidapi/SDL_hidapi_xbox360w.c
@@ -26,6 +26,7 @@
#include "../SDL_sysjoystick.h"
#include "SDL_hidapijoystick_c.h"
#include "SDL_hidapi_rumble.h"
+#include "SDL_hidapi_xbox360.h"
#ifdef SDL_JOYSTICK_HIDAPI_XBOX360
@@ -38,6 +39,7 @@ typedef struct
bool connected;
int player_index;
bool player_lights;
+ SDL_xinput_capabilities capabilities;
Uint8 last_state[USB_PACKET_LENGTH];
} SDL_DriverXbox360W_Context;
@@ -179,6 +181,18 @@ static bool HIDAPI_DriverXbox360W_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Jo
joystick->naxes = SDL_GAMEPAD_AXIS_COUNT;
joystick->nhats = 1;
joystick->connection_state = SDL_JOYSTICK_CONNECTION_WIRELESS;
+ ctx->capabilities.type = 1;
+ ctx->capabilities.flags = FLAG_WIRELESS;
+ ctx->capabilities.subType = SDL_JOYSTICK_TYPE_GAMEPAD;
+ ctx->capabilities.gamepad.wButtons = 0xFFFF;
+ ctx->capabilities.gamepad.bLeftTrigger = 0xFF;
+ ctx->capabilities.gamepad.bRightTrigger = 0xFF;
+ ctx->capabilities.gamepad.sThumbLX = 0xFFC0;
+ ctx->capabilities.gamepad.sThumbLY = 0xFFC0;
+ ctx->capabilities.gamepad.sThumbRX = 0xFFC0;
+ ctx->capabilities.gamepad.sThumbRY = 0xFFC0;
+ ctx->capabilities.vibration.wLeftMotorSpeed = 0xFFFF;
+ ctx->capabilities.vibration.wRightMotorSpeed = 0xFFFF;
return true;
}
@@ -328,6 +342,44 @@ static bool HIDAPI_DriverXbox360W_UpdateDevice(SDL_HIDAPI_Device *device)
if (joystick) {
UpdatePowerLevel(joystick, data[17]);
}
+ ctx->capabilities.type = 1;
+ ctx->capabilities.subType = data[25] & 0x7f;
+ if ((data[25] & 0x80) != 0) {
+ ctx->capabilities.flags |= FLAG_FORCE_FEEDBACK;
+ }
+ switch (data[25] & 0x7f) {
+ case 0x01: // XINPUT_DEVSUBTYPE_GAMEPAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_GAMEPAD;
+ break;
+ case 0x02: // XINPUT_DEVSUBTYPE_WHEEL
+ device->joystick_type = SDL_JOYSTICK_TYPE_WHEEL;
+ break;
+ case 0x03: // XINPUT_DEVSUBTYPE_ARCADE_STICK
+ device->joystick_type = SDL_JOYSTICK_TYPE_ARCADE_STICK;
+ break;
+ case 0x04: // XINPUT_DEVSUBTYPE_FLIGHT_STICK
+ device->joystick_type = SDL_JOYSTICK_TYPE_FLIGHT_STICK;
+ break;
+ case 0x05: // XINPUT_DEVSUBTYPE_DANCE_PAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_DANCE_PAD;
+ break;
+ case 0x06: // XINPUT_DEVSUBTYPE_GUITAR
+ case 0x07: // XINPUT_DEVSUBTYPE_GUITAR_ALTERNATE
+ case 0x0B: // XINPUT_DEVSUBTYPE_GUITAR_BASS
+ device->joystick_type = SDL_JOYSTICK_TYPE_GUITAR;
+ break;
+ case 0x08: // XINPUT_DEVSUBTYPE_DRUM_KIT
+ device->joystick_type = SDL_JOYSTICK_TYPE_DRUM_KIT;
+ break;
+ case 0x13: // XINPUT_DEVSUBTYPE_ARCADE_PAD
+ device->joystick_type = SDL_JOYSTICK_TYPE_ARCADE_PAD;
+ break;
+ }
+ device->guid.data[15] = ctx->capabilities.subType;
+ const Uint8 capabilities_packet[] = { 0x00, 0x00, 0x02, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+ if (SDL_hid_write(device->dev, capabilities_packet, sizeof(capabilities_packet)) != sizeof(capabilities_packet)) {
+ SDL_SetError("Couldn't write capabilities_packet packet");
+ }
} else if (size == 29 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x13) {
#ifdef DEBUG_JOYSTICK
SDL_Log("Battery status: %d", data[4]);
@@ -335,6 +387,32 @@ static bool HIDAPI_DriverXbox360W_UpdateDevice(SDL_HIDAPI_Device *device)
if (joystick) {
UpdatePowerLevel(joystick, data[4]);
}
+ } else if (data[0] == 0x00 && data[1] == 0x05 && data[5] == 0x12) {
+ ctx->capabilities.gamepad.wButtons = LOAD16(data[6], data[7]);
+ ctx->capabilities.gamepad.bLeftTrigger = data[8];
+ ctx->capabilities.gamepad.bRightTrigger = data[9];
+ ctx->capabilities.gamepad.sThumbLX = LOAD16(data[10], data[11]);
+ ctx->capabilities.gamepad.sThumbLY = LOAD16(data[12], data[13]);
+ ctx->capabilities.gamepad.sThumbRX = LOAD16(data[14], data[15]);
+ ctx->capabilities.gamepad.sThumbRY = LOAD16(data[16], data[17]);
+ ctx->capabilities.flags |= data[20];
+ ctx->capabilities.vibration.wLeftMotorSpeed = data[18] << 8;
+ ctx->capabilities.vibration.wRightMotorSpeed = data[19] << 8;
+#ifdef DEBUG_XBOX_PROTOCOL
+ SDL_Log("Xbox 360 capabilities:");
+ SDL_Log(" type: %02x", ctx->capabilities.type);
+ SDL_Log(" subType: %02x", ctx->capabilities.subType);
+ SDL_Log(" flags: %02x", ctx->capabilities.flags);
+ SDL_Log(" wButtons: %02x", ctx->capabilities.gamepad.wButtons);
+ SDL_Log(" bLeftTrigger: %02x", ctx->capabilities.gamepad.bLeftTrigger);
+ SDL_Log(" bRightTrigger: %02x", ctx->capabilities.gamepad.bRightTrigger);
+ SDL_Log(" sThumbLX: %02x", ctx->capabilities.gamepad.sThumbLX);
+ SDL_Log(" sThumbLY: %02x", ctx->capabilities.gamepad.sThumbLY);
+ SDL_Log(" sThumbRX: %02x", ctx->capabilities.gamepad.sThumbRX);
+ SDL_Log(" sThumbRY: %02x", ctx->capabilities.gamepad.sThumbRY);
+ SDL_Log(" wLeftMotorSpeed: %02x", ctx->capabilities.vibration.wLeftMotorSpeed);
+ SDL_Log(" wRightMotorSpeed: %02x", ctx->capabilities.vibration.wRightMotorSpeed);
+#endif
} else if (size == 29 && data[0] == 0x00 && (data[1] & 0x01) == 0x01) {
if (joystick) {
HIDAPI_DriverXbox360W_HandleStatePacket(joystick, device->dev, ctx, data + 4, size - 4);