SDL: Avoid opening non-joystick devices if possible to speedup scanning

From 45b804c158555356e9d533e3eddd321ac8a6ee6b Mon Sep 17 00:00:00 2001
From: Tyson Whitehead <[EMAIL REDACTED]>
Date: Thu, 4 Apr 2024 12:17:42 -0400
Subject: [PATCH] Avoid opening non-joystick devices if possible to speedup
 scanning

Closing a device file takes 0.01 to 0.5s, which can add up to
significant startup delays. The udev classification does not
require opening the actual device files, so, use it if possible,
and only fall back to opening the device and probing otherwise.
---
 src/joystick/linux/SDL_sysjoystick.c | 76 +++++++++++++++++++++-------
 1 file changed, 57 insertions(+), 19 deletions(-)

diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c
index 8a68cbdfd1d2b..cdaee0bf5b9b3 100644
--- a/src/joystick/linux/SDL_sysjoystick.c
+++ b/src/joystick/linux/SDL_sysjoystick.c
@@ -273,7 +273,7 @@ static int GuessIsSensor(int fd)
     return 0;
 }
 
-static int IsJoystick(const char *path, int fd, char **name_return, Uint16 *vendor_return, Uint16 *product_return, SDL_JoystickGUID *guid)
+static int IsJoystick(const char *path, int *fd, char **name_return, Uint16 *vendor_return, Uint16 *product_return, SDL_JoystickGUID *guid)
 {
     struct input_id inpid;
     char *name;
@@ -282,21 +282,32 @@ static int IsJoystick(const char *path, int fd, char **name_return, Uint16 *vend
 
     SDL_zero(inpid);
 #ifdef SDL_USE_LIBUDEV
-    SDL_UDEV_GetProductInfo(path, &inpid.vendor, &inpid.product, &inpid.version, &class);
+    /* 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) &&
+        !(class & SDL_UDEV_DEVICE_JOYSTICK)) {
+        return 0;
+    }
 #endif
-    if (ioctl(fd, JSIOCGNAME(sizeof(product_string)), product_string) <= 0) {
-        /* When udev is enabled we only get joystick devices here, so there's no need to test them */
-        if (enumeration_method != ENUMERATION_LIBUDEV &&
-            !(class & SDL_UDEV_DEVICE_JOYSTICK) && ( class || !GuessIsJoystick(fd))) {
+
+    if (fd && *fd < 0) {
+        *fd = open(path, O_RDONLY | O_CLOEXEC, 0);
+    }
+    if (!fd || *fd < 0) {
+        return 0;
+    }
+
+    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 0;
         }
 
         /* Could have vendor and product already from udev, but should agree with evdev */
-        if (ioctl(fd, EVIOCGID, &inpid) < 0) {
+        if (ioctl(*fd, EVIOCGID, &inpid) < 0) {
             return 0;
         }
 
-        if (ioctl(fd, EVIOCGNAME(sizeof(product_string)), product_string) < 0) {
+        if (ioctl(*fd, EVIOCGNAME(sizeof(product_string)), product_string) < 0) {
             return 0;
         }
     }
@@ -315,7 +326,7 @@ static int IsJoystick(const char *path, int fd, char **name_return, Uint16 *vend
     }
 #endif
 
-    FixupDeviceInfoForMapping(fd, &inpid);
+    FixupDeviceInfoForMapping(*fd, &inpid);
 
 #ifdef DEBUG_JOYSTICK
     SDL_Log("Joystick: %s, bustype = %d, vendor = 0x%.4x, product = 0x%.4x, version = %d\n", name, inpid.bustype, inpid.vendor, inpid.product, inpid.version);
@@ -333,11 +344,32 @@ static int IsJoystick(const char *path, int fd, char **name_return, Uint16 *vend
     return 1;
 }
 
-static int IsSensor(const char *path, int fd)
+static int IsSensor(const char *path, int *fd)
 {
     struct input_id inpid;
+    int class = 0;
 
-    if (ioctl(fd, EVIOCGID, &inpid) < 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) &&
+        !(class & SDL_UDEV_DEVICE_ACCELEROMETER)) {
+        return 0;
+    }
+#endif
+
+    if (fd && *fd < 0) {
+        *fd = open(path, O_RDONLY | O_CLOEXEC, 0);
+    }
+    if (!fd || *fd < 0) {
+        return 0;
+    }
+
+    if (!class && !GuessIsSensor(*fd)) {
+        return 0;
+    }
+
+    if (ioctl(*fd, EVIOCGID, &inpid) < 0) {
         return 0;
     }
 
@@ -347,7 +379,7 @@ static int IsSensor(const char *path, int fd)
         return 0;
     }
 
-    return GuessIsSensor(fd);
+    return 1;
 }
 
 #ifdef SDL_USE_LIBUDEV
@@ -434,16 +466,11 @@ static void MaybeAddDevice(const char *path)
         }
     }
 
-    fd = open(path, O_RDONLY | O_CLOEXEC, 0);
-    if (fd < 0) {
-        goto done;
-    }
-
 #ifdef DEBUG_INPUT_EVENTS
     SDL_Log("Checking %s\n", path);
 #endif
 
-    if (IsJoystick(path, fd, &name, &vendor, &product, &guid)) {
+    if (IsJoystick(path, &fd, &name, &vendor, &product, &guid)) {
 #ifdef DEBUG_INPUT_EVENTS
         SDL_Log("found joystick: %s\n", path);
 #endif
@@ -484,7 +511,7 @@ static void MaybeAddDevice(const char *path)
         goto done;
     }
 
-    if (IsSensor(path, fd)) {
+    if (IsSensor(path, &fd)) {
 #ifdef DEBUG_INPUT_EVENTS
         SDL_Log("found sensor: %s\n", path);
 #endif
@@ -880,11 +907,22 @@ static void LINUX_ScanSteamVirtualGamepads(void)
     int num_virtual_gamepads = 0;
     int virtual_gamepad_slot;
     VirtualGamepadEntry *virtual_gamepads = NULL;
+    int class;
 
     count = scandir("/dev/input", &entries, filter_entries, NULL);
     for (i = 0; i < count; ++i) {
         (void)SDL_snprintf(path, SDL_arraysize(path), "/dev/input/%s", entries[i]->d_name);
 
+#ifdef SDL_USE_LIBUDEV
+        /* 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) &&
+            (inpid.vendor != USB_VENDOR_VALVE || inpid.product != USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD)) {
+            free(entries[i]); /* This should NOT be SDL_free() */
+            continue;
+        }
+#endif
         fd = open(path, O_RDONLY | O_CLOEXEC, 0);
         if (fd >= 0) {
             if (ioctl(fd, EVIOCGID, &inpid) == 0 &&