From 0b8e796e2c09a91637f3a455dbb477984ff99354 Mon Sep 17 00:00:00 2001
From: atfrase <[EMAIL REDACTED]>
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 | 285 ++++++++++++++++++-------
src/joystick/linux/SDL_sysjoystick_c.h | 5 +
2 files changed, 209 insertions(+), 81 deletions(-)
diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c
index e16ed688d59..28bec9db317 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;
+ }
+
+ 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 if missing. */
- if (joystick->hwdata->has_hat[2]) { /* Check if ABS_HAT2{X,Y} is available. */
+ /* 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];
- }
+ mapped |= 0x3;
+ }
- 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];
- }
+ 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, but settle for analog if missing. */
+ /* 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];
- }
-
- 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;
+ mapped |= 0x8;
+ }
+
+ 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 49b8686d054..f6211d55db1 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;