From 249535a206fb2e146369786c3f315d90fdbd7f83 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Mon, 30 Jun 2025 19:50:58 -0700 Subject: [PATCH 1/2] joystick: Improve Xbox controller mapping with xpad quirks xpad has a series of questionable design choices when it comes to button mapping. Notably, BTN_TRIGGER_HAPPY1-4 are used for the D-pad on dance mats instead of the typical BTN_DPAD_*, and maps the paddles to BTN_TRIGGER_HAPPY5-8. This commit plumbs through driver detection for a udev device and adds special exceptions for xpad's quirks. Newer kernels (6.17+) add the BTN_GRIP* mappings for paddles on controllers. We should prefer that if available, as its meaning and mapping is unambiguous. However, since it's only in new kernels, we need to maintain the older mappings where we know they exist. It also exposes KEY_RECORD as the share button regardless of vendor, which had previously been gated behind being a Microsoft controller. All Xbox Series controllers can include this button, and many third party ones do. --- src/core/linux/SDL_udev.c | 19 +++- src/core/linux/SDL_udev.h | 4 +- src/joystick/linux/SDL_sysjoystick.c | 137 ++++++++++++++++++++++----- 3 files changed, 132 insertions(+), 28 deletions(-) diff --git a/src/core/linux/SDL_udev.c b/src/core/linux/SDL_udev.c index def0e6a8e32e8..f32a4fdf647f5 100644 --- a/src/core/linux/SDL_udev.c +++ b/src/core/linux/SDL_udev.c @@ -68,6 +68,7 @@ static bool SDL_UDEV_load_syms(void) SDL_UDEV_SYM(udev_device_get_action); SDL_UDEV_SYM(udev_device_get_devnode); + SDL_UDEV_SYM(udev_device_get_driver); SDL_UDEV_SYM(udev_device_get_syspath); SDL_UDEV_SYM(udev_device_get_subsystem); SDL_UDEV_SYM(udev_device_get_parent_with_subsystem_devtype); @@ -219,7 +220,7 @@ bool SDL_UDEV_Scan(void) return true; } -bool SDL_UDEV_GetProductInfo(const char *device_path, Uint16 *vendor, Uint16 *product, Uint16 *version, int *class) +bool SDL_UDEV_GetProductInfo(const char *device_path, struct input_id *inpid, int *class, char **driver) { struct stat statbuf; char type; @@ -253,17 +254,27 @@ bool SDL_UDEV_GetProductInfo(const char *device_path, Uint16 *vendor, Uint16 *pr val = _this->syms.udev_device_get_property_value(dev, "ID_VENDOR_ID"); if (val) { - *vendor = (Uint16)SDL_strtol(val, NULL, 16); + inpid->vendor = (Uint16)SDL_strtol(val, NULL, 16); } val = _this->syms.udev_device_get_property_value(dev, "ID_MODEL_ID"); if (val) { - *product = (Uint16)SDL_strtol(val, NULL, 16); + inpid->product = (Uint16)SDL_strtol(val, NULL, 16); } val = _this->syms.udev_device_get_property_value(dev, "ID_REVISION"); if (val) { - *version = (Uint16)SDL_strtol(val, NULL, 16); + inpid->version = (Uint16)SDL_strtol(val, NULL, 16); + } + + if (driver) { + val = _this->syms.udev_device_get_driver(dev); + if (!val) { + val = _this->syms.udev_device_get_property_value(dev, "ID_USB_DRIVER"); + } + if (val) { + *driver = SDL_strdup(val); + } } class_temp = device_class(dev); diff --git a/src/core/linux/SDL_udev.h b/src/core/linux/SDL_udev.h index 738f4bafe3f50..e99d06a5bee84 100644 --- a/src/core/linux/SDL_udev.h +++ b/src/core/linux/SDL_udev.h @@ -31,6 +31,7 @@ #endif #include +#include #include #include @@ -56,6 +57,7 @@ typedef struct SDL_UDEV_Symbols { const char *(*udev_device_get_action)(struct udev_device *); const char *(*udev_device_get_devnode)(struct udev_device *); + const char *(*udev_device_get_driver)(struct udev_device *); const char *(*udev_device_get_syspath)(struct udev_device *); const char *(*udev_device_get_subsystem)(struct udev_device *); struct udev_device *(*udev_device_get_parent_with_subsystem_devtype)(struct udev_device *udev_device, const char *subsystem, const char *devtype); @@ -102,7 +104,7 @@ extern void SDL_UDEV_UnloadLibrary(void); extern bool SDL_UDEV_LoadLibrary(void); extern void SDL_UDEV_Poll(void); extern bool SDL_UDEV_Scan(void); -extern bool SDL_UDEV_GetProductInfo(const char *device_path, Uint16 *vendor, Uint16 *product, Uint16 *version, int *class); +extern bool SDL_UDEV_GetProductInfo(const char *device_path, struct input_id *inpid, int *class, char **driver); extern bool SDL_UDEV_AddCallback(SDL_UDEV_Callback cb); extern void SDL_UDEV_DelCallback(SDL_UDEV_Callback cb); extern const SDL_UDEV_Symbols *SDL_UDEV_GetUdevSyms(void); diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c index ea73821c064d2..6990b4ad4d2fc 100644 --- a/src/joystick/linux/SDL_sysjoystick.c +++ b/src/joystick/linux/SDL_sysjoystick.c @@ -75,6 +75,18 @@ #ifndef BTN_DPAD_RIGHT #define BTN_DPAD_RIGHT 0x223 #endif +#ifndef BTN_GRIPL +#define BTN_GRIPL 0x224 +#endif +#ifndef BTN_GRIPR +#define BTN_GRIPR 0x225 +#endif +#ifndef BTN_GRIPL2 +#define BTN_GRIPL2 0x226 +#endif +#ifndef BTN_GRIPR2 +#define BTN_GRIPR2 0x227 +#endif #ifndef BTN_TRIGGER_HAPPY #define BTN_TRIGGER_HAPPY 0x2c0 @@ -151,6 +163,7 @@ typedef struct SDL_joylist_item SDL_JoystickID device_instance; char *path; // "/dev/input/event2" or whatever char *name; // "SideWinder 3D Pro" or whatever + char *driver; // "xpad" or whatever SDL_GUID guid; dev_t devnum; int steam_virtual_gamepad_slot; @@ -274,19 +287,20 @@ static bool GuessIsSensor(int fd) return false; } -static bool IsJoystick(const char *path, int *fd, char **name_return, Uint16 *vendor_return, Uint16 *product_return, SDL_GUID *guid) +static bool IsJoystick(const char *path, int *fd, char **name_return, Uint16 *vendor_return, Uint16 *product_return, SDL_GUID *guid, char **driver_return) { struct input_id inpid; - char *name; + char *name = NULL; + char *driver = NULL; char product_string[128]; int class = 0; SDL_zero(inpid); #ifdef SDL_USE_LIBUDEV // Opening input devices can generate synchronous device I/O, so avoid it if we can - if (SDL_UDEV_GetProductInfo(path, &inpid.vendor, &inpid.product, &inpid.version, &class) && + if (SDL_UDEV_GetProductInfo(path, &inpid, &class, &driver) && !(class & SDL_UDEV_DEVICE_JOYSTICK)) { - return false; + goto error; } #endif @@ -294,34 +308,33 @@ static bool IsJoystick(const char *path, int *fd, char **name_return, Uint16 *ve *fd = open(path, O_RDONLY | O_CLOEXEC, 0); } if (!fd || *fd < 0) { - return false; + goto error; } if (ioctl(*fd, JSIOCGNAME(sizeof(product_string)), product_string) <= 0) { // When udev enumeration or classification, we only got joysticks here, so no need to test if (enumeration_method != ENUMERATION_LIBUDEV && !class && !GuessIsJoystick(*fd)) { - return false; + goto error; } // Could have vendor and product already from udev, but should agree with evdev if (ioctl(*fd, EVIOCGID, &inpid) < 0) { - return false; + goto error; } if (ioctl(*fd, EVIOCGNAME(sizeof(product_string)), product_string) < 0) { - return false; + goto error; } } name = SDL_CreateJoystickName(inpid.vendor, inpid.product, NULL, product_string); if (!name) { - return false; + goto error; } if (!IsVirtualJoystick(inpid.vendor, inpid.product, inpid.version, name) && SDL_JoystickHandledByAnotherDriver(&SDL_LINUX_JoystickDriver, inpid.vendor, inpid.product, inpid.version, name)) { - SDL_free(name); - return false; + goto error; } FixupDeviceInfoForMapping(*fd, &inpid); @@ -331,14 +344,23 @@ static bool IsJoystick(const char *path, int *fd, char **name_return, Uint16 *ve #endif if (SDL_ShouldIgnoreJoystick(inpid.vendor, inpid.product, inpid.version, name)) { - SDL_free(name); - return false; + goto error; } *name_return = name; + *driver_return = driver; *vendor_return = inpid.vendor; *product_return = inpid.product; *guid = SDL_CreateJoystickGUID(inpid.bustype, inpid.vendor, inpid.product, inpid.version, NULL, product_string, 0, 0); return true; + +error: + if (driver) { + SDL_free(driver); + } + if (name) { + SDL_free(name); + } + return false; } static bool IsSensor(const char *path, int *fd) @@ -349,7 +371,7 @@ static bool IsSensor(const char *path, int *fd) SDL_zero(inpid); #ifdef SDL_USE_LIBUDEV // Opening input devices can generate synchronous device I/O, so avoid it if we can - if (SDL_UDEV_GetProductInfo(path, &inpid.vendor, &inpid.product, &inpid.version, &class) && + if (SDL_UDEV_GetProductInfo(path, &inpid, &class, NULL) && !(class & SDL_UDEV_DEVICE_ACCELEROMETER)) { return false; } @@ -422,6 +444,9 @@ static void FreeJoylistItem(SDL_joylist_item *item) SDL_free(item->mapping); SDL_free(item->path); SDL_free(item->name); + if (item->driver) { + SDL_free(item->driver); + } SDL_free(item); } @@ -436,6 +461,7 @@ static void MaybeAddDevice(const char *path) struct stat sb; int fd = -1; char *name = NULL; + char *driver = NULL; Uint16 vendor, product; SDL_GUID guid; SDL_joylist_item *item; @@ -473,7 +499,7 @@ static void MaybeAddDevice(const char *path) SDL_Log("Checking %s", path); #endif - if (IsJoystick(path, &fd, &name, &vendor, &product, &guid)) { + if (IsJoystick(path, &fd, &name, &vendor, &product, &guid, &driver)) { #ifdef DEBUG_INPUT_EVENTS SDL_Log("found joystick: %s", path); #endif @@ -488,6 +514,7 @@ static void MaybeAddDevice(const char *path) item->path = SDL_strdup(path); item->name = name; item->guid = guid; + item->driver = driver; if (vendor == USB_VENDOR_VALVE && product == USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD) { @@ -861,7 +888,7 @@ static void LINUX_ScanSteamVirtualGamepads(void) // Opening input devices can generate synchronous device I/O, so avoid it if we can class = 0; SDL_zero(inpid); - if (SDL_UDEV_GetProductInfo(path, &inpid.vendor, &inpid.product, &inpid.version, &class) && + if (SDL_UDEV_GetProductInfo(path, &inpid, &class, NULL) && (inpid.vendor != USB_VENDOR_VALVE || inpid.product != USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD)) { free(entries[i]); // This should NOT be SDL_free() continue; @@ -2244,6 +2271,12 @@ static bool LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping MAPPED_DPAD_LEFT = 0x4, MAPPED_DPAD_RIGHT = 0x8, MAPPED_DPAD_ALL = 0xF, + + MAPPED_LEFT_PADDLE1 = 0x1, + MAPPED_RIGHT_PADDLE1 = 0x2, + MAPPED_LEFT_PADDLE2 = 0x4, + MAPPED_RIGHT_PADDLE2 = 0x8, + MAPPED_PADDLE_ALL = 0xF, }; unsigned int mapped; bool result = false; @@ -2609,6 +2642,27 @@ static bool LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping SDL_Log("Mapped DPUP+DOWN to axis %d (ABS_HAT0Y)", out->dpup.target); SDL_Log("Mapped DPLEFT+RIGHT to axis %d (ABS_HAT0X)", out->dpleft.target); #endif + } else if (item->driver && SDL_strcmp(item->driver, "xpad") == 0) { + // xpad will sometimes map the D-Pad as BTN_TRIGGER_HAPPY1 - BTN_TRIGGER_HAPPY4 + if (joystick->hwdata->has_key[BTN_TRIGGER_HAPPY1] && + joystick->hwdata->has_key[BTN_TRIGGER_HAPPY2] && + joystick->hwdata->has_key[BTN_TRIGGER_HAPPY3] && + joystick->hwdata->has_key[BTN_TRIGGER_HAPPY4]) { + out->dpleft.kind = EMappingKind_Button; + out->dpright.kind = EMappingKind_Button; + out->dpup.kind = EMappingKind_Button; + out->dpdown.kind = EMappingKind_Button; + out->dpleft.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY1]; + out->dpright.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY2]; + out->dpup.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY3]; + out->dpdown.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY4]; +#ifdef DEBUG_GAMEPAD_MAPPING + SDL_Log("Mapped DPLEFT to button %d (BTN_TRIGGER_HAPPY1)", out->dpleft.target); + SDL_Log("Mapped DPRIGHT to button %d (BTN_TRIGGER_HAPPY2)", out->dpright.target); + SDL_Log("Mapped DPUP to button %d (BTN_TRIGGER_HAPPY3)", out->dpup.target); + SDL_Log("Mapped DPDOWN to button %d (BTN_TRIGGER_HAPPY4)", out->dpdown.target); +#endif + } } } @@ -2653,8 +2707,44 @@ static bool LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping #endif } - if (SDL_GetJoystickVendor(joystick) == USB_VENDOR_MICROSOFT) { + mapped = 0; + + if (joystick->hwdata->has_key[BTN_GRIPR]) { + out->right_paddle1.kind = EMappingKind_Button; + out->right_paddle1.target = joystick->hwdata->key_map[BTN_GRIPR]; + mapped |= MAPPED_RIGHT_PADDLE1; +#ifdef DEBUG_GAMEPAD_MAPPING + SDL_Log("Mapped RIGHT_PADDLE1 to button %d (BTN_GRIPR)", out->right_paddle1.target); +#endif + } + if (joystick->hwdata->has_key[BTN_GRIPL]) { + out->left_paddle1.kind = EMappingKind_Button; + out->left_paddle1.target = joystick->hwdata->key_map[BTN_GRIPL]; + mapped |= MAPPED_LEFT_PADDLE1; +#ifdef DEBUG_GAMEPAD_MAPPING + SDL_Log("Mapped LEFT_PADDLE1 to button %d (BTN_GRIPL)", out->left_paddle1.target); +#endif + } + if (joystick->hwdata->has_key[BTN_GRIPR2]) { + out->right_paddle2.kind = EMappingKind_Button; + out->right_paddle2.target = joystick->hwdata->key_map[BTN_GRIPR2]; + mapped |= MAPPED_RIGHT_PADDLE2; +#ifdef DEBUG_GAMEPAD_MAPPING + SDL_Log("Mapped RIGHT_PADDLE2 to button %d (BTN_GRIPR)", out->right_paddle2.target); +#endif + } + if (joystick->hwdata->has_key[BTN_GRIPL2]) { + out->left_paddle2.kind = EMappingKind_Button; + out->left_paddle2.target = joystick->hwdata->key_map[BTN_GRIPL2]; + mapped |= MAPPED_LEFT_PADDLE2; +#ifdef DEBUG_GAMEPAD_MAPPING + SDL_Log("Mapped LEFT_PADDLE2 to button %d (BTN_GRIPL2)", out->left_paddle2.target); +#endif + } + + if (mapped != MAPPED_PADDLE_ALL && SDL_GetJoystickVendor(joystick) == USB_VENDOR_MICROSOFT) { // The Xbox Elite controllers have the paddles as BTN_TRIGGER_HAPPY5 - BTN_TRIGGER_HAPPY8 + // in older drivers if (joystick->hwdata->has_key[BTN_TRIGGER_HAPPY5] && joystick->hwdata->has_key[BTN_TRIGGER_HAPPY6] && joystick->hwdata->has_key[BTN_TRIGGER_HAPPY7] && @@ -2667,6 +2757,7 @@ static bool LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping out->right_paddle2.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY6]; out->left_paddle2.kind = EMappingKind_Button; out->left_paddle2.target = joystick->hwdata->key_map[BTN_TRIGGER_HAPPY8]; + mapped = MAPPED_PADDLE_ALL; #ifdef DEBUG_GAMEPAD_MAPPING SDL_Log("Mapped RIGHT_PADDLE1 to button %d (BTN_TRIGGER_HAPPY5)", out->right_paddle1.target); SDL_Log("Mapped LEFT_PADDLE1 to button %d (BTN_TRIGGER_HAPPY7)", out->left_paddle1.target); @@ -2674,15 +2765,15 @@ static bool LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping SDL_Log("Mapped LEFT_PADDLE2 to button %d (BTN_TRIGGER_HAPPY8)", out->left_paddle2.target); #endif } + } - // The Xbox Series X controllers have the Share button as KEY_RECORD - if (joystick->hwdata->has_key[KEY_RECORD]) { - out->misc1.kind = EMappingKind_Button; - out->misc1.target = joystick->hwdata->key_map[KEY_RECORD]; + // Xbox Series controllers have the Share button as KEY_RECORD + if (joystick->hwdata->has_key[KEY_RECORD]) { + out->misc1.kind = EMappingKind_Button; + out->misc1.target = joystick->hwdata->key_map[KEY_RECORD]; #ifdef DEBUG_GAMEPAD_MAPPING - SDL_Log("Mapped MISC1 to button %d (KEY_RECORD)", out->misc1.target); + SDL_Log("Mapped MISC1 to button %d (KEY_RECORD)", out->misc1.target); #endif - } } // Cache the mapping for later From 1d4f3a3376b57be261a737e982e75be35966798b Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Mon, 28 Jul 2025 20:02:38 -0700 Subject: [PATCH 2/2] Update src/joystick/linux/SDL_sysjoystick.c --- src/joystick/linux/SDL_sysjoystick.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c index 6990b4ad4d2fc..224a3dce37f6b 100644 --- a/src/joystick/linux/SDL_sysjoystick.c +++ b/src/joystick/linux/SDL_sysjoystick.c @@ -444,9 +444,7 @@ static void FreeJoylistItem(SDL_joylist_item *item) SDL_free(item->mapping); SDL_free(item->path); SDL_free(item->name); - if (item->driver) { - SDL_free(item->driver); - } + SDL_free(item->driver); SDL_free(item); }