From 0b8e796e2c09a91637f3a455dbb477984ff99354 Mon Sep 17 00:00:00 2001 From: atfrase Date: Wed, 12 Jan 2022 22:00:46 -0600 Subject: [PATCH] added hueristic to differentiate digital vs analog 'hat' input axes and expose the latter as regular axes; added automatic deadzones to hat outputs, in case analog axes are still mapped to digital hats; updated automatic gamepad control mapping to more completely follow the spec --- src/joystick/linux/SDL_sysjoystick.c | 283 ++++++++++++++++++------- src/joystick/linux/SDL_sysjoystick_c.h | 5 + 2 files changed, 208 insertions(+), 80 deletions(-) diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c index e16ed688d..28bec9db3 100644 --- a/src/joystick/linux/SDL_sysjoystick.c +++ b/src/joystick/linux/SDL_sysjoystick.c @@ -78,7 +78,7 @@ #include "../../core/linux/SDL_evdev_capabilities.h" #include "../../core/linux/SDL_udev.h" -#if 0 +#if 1 #define DEBUG_INPUT_EVENTS 1 #endif @@ -926,6 +926,33 @@ allocate_balldata(SDL_Joystick *joystick) return (0); } +static SDL_bool +GuessIfAxesAreDigitalHat(struct input_absinfo *absinfo_x, struct input_absinfo *absinfo_y) +{ + /* A "hat" is assumed to be a digital input with at most 9 possible states + * (3 per axis: negative/zero/positive), as opposed to a true "axis" which + * can report a continuous range of possible values. Unfortunately the Linux + * joystick interface makes no distinction between digital hat axes and any + * other continuous analog axis, so we have to guess. */ + + /* If both axes are missing, they're not anything. */ + if (!absinfo_x && !absinfo_y) + return SDL_FALSE; + + /* If both axes have ranges constrained between -1 and 1, they're definitely digital. */ + if ((!absinfo_x || (absinfo_x->minimum == -1 && absinfo_x->maximum == 1)) && + (!absinfo_y || (absinfo_y->minimum == -1 && absinfo_y->maximum == 1))) + return SDL_TRUE; + + /* If both axes lack fuzz, flat, and resolution values, they're probably digital. */ + if ((!absinfo_x || (!absinfo_x->fuzz && !absinfo_x->flat && !absinfo_x->resolution)) && + (!absinfo_y || (!absinfo_y->fuzz && !absinfo_y->flat && !absinfo_y->resolution))) + return SDL_TRUE; + + /* Otherwise, treat them as analog. */ + return SDL_FALSE; +} + static void ConfigJoystick(SDL_Joystick *joystick, int fd) { @@ -963,10 +990,43 @@ ConfigJoystick(SDL_Joystick *joystick, int fd) ++joystick->nbuttons; } } + for (i = ABS_HAT0X; i <= ABS_HAT3Y; i += 2) { + int hat_x = -1; + int hat_y = -1; + struct input_absinfo absinfo_x; + struct input_absinfo absinfo_y; + if (test_bit(i, absbit)) + hat_x = ioctl(fd, EVIOCGABS(i), &absinfo_x); + if (test_bit(i + 1, absbit)) + hat_y = ioctl(fd, EVIOCGABS(i + 1), &absinfo_y); + if (GuessIfAxesAreDigitalHat((hat_x < 0 ? (void*)0 : &absinfo_x), + (hat_y < 0 ? (void*)0 : &absinfo_y))) { + const int hat_index = (i - ABS_HAT0X) / 2; +#ifdef DEBUG_INPUT_EVENTS + SDL_Log("Joystick has digital hat: %d\n", hat_index); + if (hat_x >= 0) { + SDL_Log("X Values = { %d, %d, %d, %d, %d, %d }\n", + absinfo_x.value, absinfo_x.minimum, absinfo_x.maximum, + absinfo_x.fuzz, absinfo_x.flat, absinfo_x.resolution); + } + if (hat_y >= 0) { + SDL_Log("Y Values = { %d, %d, %d, %d, %d, %d }\n", + absinfo_y.value, absinfo_y.minimum, absinfo_y.maximum, + absinfo_y.fuzz, absinfo_y.flat, absinfo_y.resolution); + } +#endif /* DEBUG_INPUT_EVENTS */ + joystick->hwdata->hats_indices[hat_index] = joystick->nhats; + joystick->hwdata->has_hat[hat_index] = SDL_TRUE; + joystick->hwdata->hat_correct[hat_index].minimum[0] = (hat_x < 0) ? -1 : absinfo_x.minimum; + joystick->hwdata->hat_correct[hat_index].maximum[0] = (hat_x < 0) ? 1 : absinfo_x.maximum; + joystick->hwdata->hat_correct[hat_index].minimum[1] = (hat_y < 0) ? -1 : absinfo_y.minimum; + joystick->hwdata->hat_correct[hat_index].maximum[1] = (hat_y < 0) ? 1 : absinfo_y.maximum; + ++joystick->nhats; + } + } for (i = 0; i < ABS_MAX; ++i) { - /* Skip hats */ - if (i == ABS_HAT0X) { - i = ABS_HAT3Y; + /* Skip digital hats */ + if (joystick->hwdata->has_hat[(i - ABS_HAT0X) / 2]) { continue; } if (test_bit(i, absbit)) { @@ -978,9 +1038,9 @@ ConfigJoystick(SDL_Joystick *joystick, int fd) } #ifdef DEBUG_INPUT_EVENTS SDL_Log("Joystick has absolute axis: 0x%.2x\n", i); - SDL_Log("Values = { %d, %d, %d, %d, %d }\n", + SDL_Log("Values = { %d, %d, %d, %d, %d, %d }\n", absinfo.value, absinfo.minimum, absinfo.maximum, - absinfo.fuzz, absinfo.flat); + absinfo.fuzz, absinfo.flat, absinfo.resolution); #endif /* DEBUG_INPUT_EVENTS */ joystick->hwdata->abs_map[i] = joystick->naxes; joystick->hwdata->has_abs[i] = SDL_TRUE; @@ -1008,24 +1068,6 @@ ConfigJoystick(SDL_Joystick *joystick, int fd) ++joystick->naxes; } } - for (i = ABS_HAT0X; i <= ABS_HAT3Y; i += 2) { - if (test_bit(i, absbit) || test_bit(i + 1, absbit)) { - struct input_absinfo absinfo; - int hat_index = (i - ABS_HAT0X) / 2; - - if (ioctl(fd, EVIOCGABS(i), &absinfo) < 0) { - continue; - } -#ifdef DEBUG_INPUT_EVENTS - SDL_Log("Joystick has hat %d\n", hat_index); - SDL_Log("Values = { %d, %d, %d, %d, %d }\n", - absinfo.value, absinfo.minimum, absinfo.maximum, - absinfo.fuzz, absinfo.flat); -#endif /* DEBUG_INPUT_EVENTS */ - joystick->hwdata->hats_indices[hat_index] = joystick->nhats++; - joystick->hwdata->has_hat[hat_index] = SDL_TRUE; - } - } if (test_bit(REL_X, relbit) || test_bit(REL_Y, relbit)) { ++joystick->nballs; } @@ -1071,14 +1113,19 @@ ConfigJoystick(SDL_Joystick *joystick, int fd) for (i = 0; i < abs_pam_size; ++i) { Uint8 code = joystick->hwdata->abs_pam[i]; + // TODO: is there any way to detect analog hats in advance via this API? if (code >= ABS_HAT0X && code <= ABS_HAT3Y) { int hat_index = (code - ABS_HAT0X) / 2; if (!joystick->hwdata->has_hat[hat_index]) { #ifdef DEBUG_INPUT_EVENTS - SDL_Log("Joystick has hat %d\n", hat_index); + SDL_Log("Joystick has hat: %d\n", hat_index); #endif joystick->hwdata->hats_indices[hat_index] = joystick->nhats++; joystick->hwdata->has_hat[hat_index] = SDL_TRUE; + joystick->hwdata->hat_correct[hat_index].minimum[0] = -1; + joystick->hwdata->hat_correct[hat_index].maximum[0] = 1; + joystick->hwdata->hat_correct[hat_index].minimum[1] = -1; + joystick->hwdata->hat_correct[hat_index].maximum[1] = 1; } } else { #ifdef DEBUG_INPUT_EVENTS @@ -1277,26 +1324,47 @@ LINUX_JoystickSetSensorsEnabled(SDL_Joystick *joystick, SDL_bool enabled) } static void -HandleHat(SDL_Joystick *stick, Uint8 hat, int axis, int value) +HandleHat(SDL_Joystick *stick, int hatidx, int axis, int value) { + const int hatnum = stick->hwdata->hats_indices[hatidx]; struct hwdata_hat *the_hat; + struct hat_axis_correct *correct; const Uint8 position_map[3][3] = { {SDL_HAT_LEFTUP, SDL_HAT_UP, SDL_HAT_RIGHTUP}, {SDL_HAT_LEFT, SDL_HAT_CENTERED, SDL_HAT_RIGHT}, {SDL_HAT_LEFTDOWN, SDL_HAT_DOWN, SDL_HAT_RIGHTDOWN} }; - the_hat = &stick->hwdata->hats[hat]; + the_hat = &stick->hwdata->hats[hatnum]; + correct = &stick->hwdata->hat_correct[hatidx]; + /* If we failed to detect that this hat's axes are analog, + * we can at least make the hat output usable by applying + * a deadzone of half of the observed min/max values. If + * the axes really are digital, this won't hurt. */ if (value < 0) { - value = 0; - } else if (value == 0) { - value = 1; + if (value <= correct->minimum[axis]) { + correct->minimum[axis] = value; + value = 0; + } else if (value < correct->minimum[axis] / 2) { + value = 0; + } else { + value = 1; + } } else if (value > 0) { - value = 2; + if (value >= correct->maximum[axis]) { + correct->maximum[axis] = value; + value = 2; + } else if (value > correct->maximum[axis] / 2) { + value = 2; + } else { + value = 1; + } + } else { // value == 0 + value = 1; } if (value != the_hat->axis[axis]) { the_hat->axis[axis] = value; - SDL_PrivateJoystickHat(stick, hat, + SDL_PrivateJoystickHat(stick, hatnum, position_map[the_hat->axis[1]][the_hat->axis[0]]); } } @@ -1351,10 +1419,7 @@ PollAllValues(SDL_Joystick *joystick) /* Poll all axis */ for (i = ABS_X; i < ABS_MAX; i++) { - if (i == ABS_HAT0X) { /* we handle hats in the next loop, skip them for now. */ - i = ABS_HAT3Y; - continue; - } + /* We don't need to test for digital hats here, they won't have has_abs[] set */ if (joystick->hwdata->has_abs[i]) { if (ioctl(joystick->hwdata->fd, EVIOCGABS(i), &absinfo) >= 0) { absinfo.value = AxisCorrect(joystick, i, absinfo.value); @@ -1370,15 +1435,16 @@ PollAllValues(SDL_Joystick *joystick) } } - /* Poll all hats */ + /* Poll all digital hats */ for (i = ABS_HAT0X; i <= ABS_HAT3Y; i++) { const int baseaxis = i - ABS_HAT0X; const int hatidx = baseaxis / 2; SDL_assert(hatidx < SDL_arraysize(joystick->hwdata->has_hat)); + /* We don't need to test for analog axes here, they won't have has_hat[] set */ if (joystick->hwdata->has_hat[hatidx]) { if (ioctl(joystick->hwdata->fd, EVIOCGABS(i), &absinfo) >= 0) { const int hataxis = baseaxis % 2; - HandleHat(joystick, joystick->hwdata->hats_indices[hatidx], hataxis, absinfo.value); + HandleHat(joystick, hatidx, hataxis, absinfo.value); } } } @@ -1406,7 +1472,7 @@ static void HandleInputEvents(SDL_Joystick *joystick) { struct input_event events[32]; - int i, len, code; + int i, len, code, hat_index; if (joystick->hwdata->fresh) { PollAllValues(joystick); @@ -1441,9 +1507,11 @@ HandleInputEvents(SDL_Joystick *joystick) case ABS_HAT2Y: case ABS_HAT3X: case ABS_HAT3Y: - code -= ABS_HAT0X; - HandleHat(joystick, joystick->hwdata->hats_indices[code / 2], code % 2, events[i].value); - break; + hat_index = (code - ABS_HAT0X) / 2; + if (joystick->hwdata->has_hat[hat_index]) { + HandleHat(joystick, hat_index, code % 2, events[i].value); + break; + } default: events[i].value = AxisCorrect(joystick, code, events[i].value); SDL_PrivateJoystickAxis(joystick, @@ -1496,7 +1564,7 @@ static void HandleClassicEvents(SDL_Joystick *joystick) { struct js_event events[32]; - int i, len, code; + int i, len, code, hat_index; joystick->hwdata->fresh = SDL_FALSE; while ((len = read(joystick->hwdata->fd, events, (sizeof events))) > 0) { @@ -1520,9 +1588,11 @@ HandleClassicEvents(SDL_Joystick *joystick) case ABS_HAT2Y: case ABS_HAT3X: case ABS_HAT3Y: - code -= ABS_HAT0X; - HandleHat(joystick, joystick->hwdata->hats_indices[code / 2], code % 2, events[i].value); - break; + hat_index = (code - ABS_HAT0X) / 2; + if (joystick->hwdata->has_hat[hat_index]) { + HandleHat(joystick, hat_index, code % 2, events[i].value); + break; + } default: SDL_PrivateJoystickAxis(joystick, joystick->hwdata->abs_map[code], @@ -1628,6 +1698,7 @@ LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) { SDL_Joystick *joystick; SDL_joylist_item *item = JoystickByDevIndex(device_index); + unsigned int mapped; if (item->checked_mapping) { if (item->mapping) { @@ -1739,84 +1810,136 @@ LINUX_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out) can be digital, or analog, or both at the same time. */ - /* Prefer digital shoulder buttons, but settle for analog if missing. */ + /* Prefer digital shoulder buttons, but settle for digital or analog hat. */ + mapped = 0; + if (joystick->hwdata->has_key[BTN_TL]) { out->leftshoulder.kind = EMappingKind_Button; out->leftshoulder.target = joystick->hwdata->key_map[BTN_TL]; + mapped |= 0x1; } if (joystick->hwdata->has_key[BTN_TR]) { out->rightshoulder.kind = EMappingKind_Button; out->rightshoulder.target = joystick->hwdata->key_map[BTN_TR]; + mapped |= 0x2; } - if (joystick->hwdata->has_hat[1] && /* Check if ABS_HAT1{X, Y} is available. */ - (!joystick->hwdata->has_key[BTN_TL] || !joystick->hwdata->has_key[BTN_TR])) { + if (mapped != 0x3 && joystick->hwdata->has_hat[1]) { int hat = joystick->hwdata->hats_indices[1] << 4; out->leftshoulder.kind = EMappingKind_Hat; out->rightshoulder.kind = EMappingKind_Hat; out->leftshoulder.target = hat | 0x4; out->rightshoulder.target = hat | 0x2; + mapped |= 0x3; } - /* Prefer analog triggers, but settle for digital if missing. */ - if (joystick->hwdata->has_hat[2]) { /* Check if ABS_HAT2{X,Y} is available. */ + if (!(mapped & 0x1) && joystick->hwdata->has_abs[ABS_HAT1Y]) { + out->leftshoulder.kind = EMappingKind_Axis; + out->leftshoulder.target = joystick->hwdata->abs_map[ABS_HAT1Y]; + mapped |= 0x1; + } + + if (!(mapped & 0x2) && joystick->hwdata->has_abs[ABS_HAT1X]) { + out->rightshoulder.kind = EMappingKind_Axis; + out->rightshoulder.target = joystick->hwdata->abs_map[ABS_HAT1X]; + mapped |= 0x2; + } + + /* Prefer analog triggers, but settle for digital hat or buttons. */ + mapped = 0; + + if (joystick->hwdata->has_abs[ABS_HAT2Y]) { + out->lefttrigger.kind = EMappingKind_Axis; + out->lefttrigger.target = joystick->hwdata->abs_map[ABS_HAT2Y]; + mapped |= 0x1; + } else if (joystick->hwdata->has_abs[ABS_Z]) { + out->lefttrigger.kind = EMappingKind_Axis; + out->lefttrigger.target = joystick->hwdata->abs_map[ABS_Z]; + mapped |= 0x1; + } + + if (joystick->hwdata->has_abs[ABS_HAT2X]) { + out->righttrigger.kind = EMappingKind_Axis; + out->righttrigger.target = joystick->hwdata->abs_map[ABS_HAT2X]; + mapped |= 0x2; + } else if (joystick->hwdata->has_abs[ABS_RZ]) { + out->righttrigger.kind = EMappingKind_Axis; + out->righttrigger.target = joystick->hwdata->abs_map[ABS_RZ]; + mapped |= 0x2; + } + + if (mapped != 0x3 && joystick->hwdata->has_hat[2]) { int hat = joystick->hwdata->hats_indices[2] << 4; out->lefttrigger.kind = EMappingKind_Hat; out->righttrigger.kind = EMappingKind_Hat; out->lefttrigger.target = hat | 0x4; out->righttrigger.target = hat | 0x2; - } else { - if (joystick->hwdata->has_abs[ABS_Z]) { - out->lefttrigger.kind = EMappingKind_Axis; - out->lefttrigger.target = joystick->hwdata->abs_map[ABS_Z]; - } else if (joystick->hwdata->has_key[BTN_TL2]) { - out->lefttrigger.kind = EMappingKind_Button; - out->lefttrigger.target = joystick->hwdata->key_map[BTN_TL2]; - } - - if (joystick->hwdata->has_abs[ABS_RZ]) { - out->righttrigger.kind = EMappingKind_Axis; - out->righttrigger.target = joystick->hwdata->abs_map[ABS_RZ]; - } else if (joystick->hwdata->has_key[BTN_TR2]) { - out->righttrigger.kind = EMappingKind_Button; - out->righttrigger.target = joystick->hwdata->key_map[BTN_TR2]; - } + mapped |= 0x3; } - /* Prefer digital D-Pad, but settle for analog if missing. */ + if (!(mapped & 0x1) && joystick->hwdata->has_key[BTN_TL2]) { + out->lefttrigger.kind = EMappingKind_Button; + out->lefttrigger.target = joystick->hwdata->key_map[BTN_TL2]; + mapped |= 0x1; + } + + if (!(mapped & 0x2) && joystick->hwdata->has_key[BTN_TR2]) { + out->righttrigger.kind = EMappingKind_Button; + out->righttrigger.target = joystick->hwdata->key_map[BTN_TR2]; + mapped |= 0x2; + } + + /* Prefer digital D-Pad buttons, but settle for digital or analog hat. */ + mapped = 0; + if (joystick->hwdata->has_key[BTN_DPAD_UP]) { out->dpup.kind = EMappingKind_Button; out->dpup.target = joystick->hwdata->key_map[BTN_DPAD_UP]; + mapped |= 0x1; } if (joystick->hwdata->has_key[BTN_DPAD_DOWN]) { out->dpdown.kind = EMappingKind_Button; out->dpdown.target = joystick->hwdata->key_map[BTN_DPAD_DOWN]; + mapped |= 0x2; } if (joystick->hwdata->has_key[BTN_DPAD_LEFT]) { out->dpleft.kind = EMappingKind_Button; out->dpleft.target = joystick->hwdata->key_map[BTN_DPAD_LEFT]; + mapped |= 0x4; } if (joystick->hwdata->has_key[BTN_DPAD_RIGHT]) { out->dpright.kind = EMappingKind_Button; out->dpright.target = joystick->hwdata->key_map[BTN_DPAD_RIGHT]; + mapped |= 0x8; } - if (joystick->hwdata->has_hat[0] && /* Check if ABS_HAT0{X,Y} is available. */ - (!joystick->hwdata->has_key[BTN_DPAD_LEFT] || !joystick->hwdata->has_key[BTN_DPAD_RIGHT] || - !joystick->hwdata->has_key[BTN_DPAD_UP] || !joystick->hwdata->has_key[BTN_DPAD_DOWN])) { - int hat = joystick->hwdata->hats_indices[0] << 4; - out->dpleft.kind = EMappingKind_Hat; - out->dpright.kind = EMappingKind_Hat; - out->dpup.kind = EMappingKind_Hat; - out->dpdown.kind = EMappingKind_Hat; - out->dpleft.target = hat | 0x8; - out->dpright.target = hat | 0x2; - out->dpup.target = hat | 0x1; - out->dpdown.target = hat | 0x4; + if (mapped != 0xF) { + if (joystick->hwdata->has_hat[0]) { + int hat = joystick->hwdata->hats_indices[0] << 4; + out->dpleft.kind = EMappingKind_Hat; + out->dpright.kind = EMappingKind_Hat; + out->dpup.kind = EMappingKind_Hat; + out->dpdown.kind = EMappingKind_Hat; + out->dpleft.target = hat | 0x8; + out->dpright.target = hat | 0x2; + out->dpup.target = hat | 0x1; + out->dpdown.target = hat | 0x4; + mapped |= 0xF; + } else if (joystick->hwdata->has_abs[ABS_HAT0X] && joystick->hwdata->has_abs[ABS_HAT0Y]) { + out->dpleft.kind = EMappingKind_Axis; + out->dpright.kind = EMappingKind_Axis; + out->dpup.kind = EMappingKind_Axis; + out->dpdown.kind = EMappingKind_Axis; + out->dpleft.target = joystick->hwdata->abs_map[ABS_HAT0X]; + out->dpright.target = joystick->hwdata->abs_map[ABS_HAT0X]; + out->dpup.target = joystick->hwdata->abs_map[ABS_HAT0Y]; + out->dpdown.target = joystick->hwdata->abs_map[ABS_HAT0Y]; + mapped |= 0xF; + } } if (joystick->hwdata->has_abs[ABS_X] && joystick->hwdata->has_abs[ABS_Y]) { diff --git a/src/joystick/linux/SDL_sysjoystick_c.h b/src/joystick/linux/SDL_sysjoystick_c.h index 49b8686d0..f6211d55d 100644 --- a/src/joystick/linux/SDL_sysjoystick_c.h +++ b/src/joystick/linux/SDL_sysjoystick_c.h @@ -83,6 +83,11 @@ struct joystick_hwdata /* 4 = (ABS_HAT3X-ABS_HAT0X)/2 (see input-event-codes.h in kernel) */ int hats_indices[4]; SDL_bool has_hat[4]; + struct hat_axis_correct + { + int minimum[2]; + int maximum[2]; + } hat_correct[4]; /* Set when gamepad is pending removal due to ENODEV read error */ SDL_bool gone;