SDL: Sort Steam virtual gamepads by Steam controller slot

From 17723381da9bc3eadb129beaeaed2164c64b5155 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 9 Dec 2023 20:31:28 -0800
Subject: [PATCH] Sort Steam virtual gamepads by Steam controller slot

This makes sure that games launched by Steam see the first controller first, and the controllers in the game match up with the controllers in the Steam UI.

Fixes https://github.com/libsdl-org/SDL/issues/8672
---
 src/joystick/linux/SDL_sysjoystick.c | 115 +++++++++++++++++++++++----
 1 file changed, 101 insertions(+), 14 deletions(-)

diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c
index 9b8a85f26678..50a4e9989176 100644
--- a/src/joystick/linux/SDL_sysjoystick.c
+++ b/src/joystick/linux/SDL_sysjoystick.c
@@ -832,6 +832,104 @@ static int SDLCALL sort_entries(const void *_a, const void *_b)
     return numA - numB;
 }
 
+typedef struct
+{
+    char *path;
+    int slot;
+} VirtualGamepadEntry;
+
+static int SDLCALL sort_virtual_gamepads(const void *_a, const void *_b)
+{
+    const VirtualGamepadEntry *a = (const VirtualGamepadEntry *)_a;
+    const VirtualGamepadEntry *b = (const VirtualGamepadEntry *)_b;
+    return a->slot - b->slot;
+}
+
+static SDL_bool get_virtual_gamepad_slot(const char *name, int *slot)
+{
+    const char *digits = SDL_strstr(name, "pad ");
+    if (digits) {
+        digits += 4;
+        if (SDL_isdigit(*digits)) {
+            *slot = SDL_atoi(digits);
+            return SDL_TRUE;
+        }
+    }
+    return SDL_FALSE;
+}
+
+static void LINUX_ScanSteamVirtualGamepads(void)
+{
+    int i, count;
+    int fd;
+    struct dirent **entries = NULL;
+    char path[PATH_MAX];
+    char name[128];
+    struct input_id inpid;
+    int num_virtual_gamepads = 0;
+    int virtual_gamepad_slot;
+    VirtualGamepadEntry *virtual_gamepads = NULL;
+
+    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);
+
+        fd = open(path, O_RDONLY | O_CLOEXEC, 0);
+        if (fd >= 0) {
+            if (ioctl(fd, EVIOCGID, &inpid) == 0 &&
+                inpid.vendor == USB_VENDOR_VALVE &&
+                inpid.product == USB_PRODUCT_STEAM_VIRTUAL_GAMEPAD &&
+                ioctl(fd, EVIOCGNAME(sizeof(name)), name) > 0 &&
+                get_virtual_gamepad_slot(name, &virtual_gamepad_slot)) {
+                VirtualGamepadEntry *new_virtual_gamepads = (VirtualGamepadEntry *)SDL_realloc(virtual_gamepads, (num_virtual_gamepads + 1) * sizeof(*virtual_gamepads));
+                if (new_virtual_gamepads) {
+                    VirtualGamepadEntry *entry = &new_virtual_gamepads[num_virtual_gamepads];
+                    entry->path = SDL_strdup(path);
+                    entry->slot = virtual_gamepad_slot;
+                    if (entry->path) {
+                        virtual_gamepads = new_virtual_gamepads;
+                        ++num_virtual_gamepads;
+                    } else {
+                        SDL_free(entry->path);
+                        SDL_free(new_virtual_gamepads);
+                    }
+                }
+            }
+            close(fd);
+        }
+        free(entries[i]); /* This should NOT be SDL_free() */
+    }
+    free(entries); /* This should NOT be SDL_free() */
+
+    if (num_virtual_gamepads > 1) {
+        SDL_qsort(virtual_gamepads, num_virtual_gamepads, sizeof(*virtual_gamepads), sort_virtual_gamepads);
+    }
+    for (i = 0; i < num_virtual_gamepads; ++i) {
+        MaybeAddDevice(virtual_gamepads[i].path);
+        SDL_free(virtual_gamepads[i].path);
+    }
+    SDL_free(virtual_gamepads);
+}
+
+static void LINUX_ScanInputDevices(void)
+{
+    int i, count;
+    struct dirent **entries = NULL;
+    char path[PATH_MAX];
+
+    count = scandir("/dev/input", &entries, filter_entries, NULL);
+    if (count > 1) {
+        SDL_qsort(entries, count, sizeof(*entries), sort_entries);
+    }
+    for (i = 0; i < count; ++i) {
+        (void)SDL_snprintf(path, SDL_arraysize(path), "/dev/input/%s", entries[i]->d_name);
+        MaybeAddDevice(path);
+
+        free(entries[i]); /* This should NOT be SDL_free() */
+    }
+    free(entries); /* This should NOT be SDL_free() */
+}
+
 static void LINUX_FallbackJoystickDetect(void)
 {
     const Uint32 SDL_JOY_DETECT_INTERVAL_MS = 3000; /* Update every 3 seconds */
@@ -842,21 +940,10 @@ static void LINUX_FallbackJoystickDetect(void)
 
         /* Opening input devices can generate synchronous device I/O, so avoid it if we can */
         if (stat("/dev/input", &sb) == 0 && sb.st_mtime != last_input_dir_mtime) {
-            int i, count;
-            struct dirent **entries = NULL;
-            char path[PATH_MAX];
+            /* Look for Steam virtual gamepads first, and sort by Steam controller slot */
+            LINUX_ScanSteamVirtualGamepads();
 
-            count = scandir("/dev/input", &entries, filter_entries, NULL);
-            if (count > 1) {
-                SDL_qsort(entries, count, sizeof(*entries), sort_entries);
-            }
-            for (i = 0; i < count; ++i) {
-                (void)SDL_snprintf(path, SDL_arraysize(path), "/dev/input/%s", entries[i]->d_name);
-                MaybeAddDevice(path);
-
-                free(entries[i]); /* This should NOT be SDL_free() */
-            }
-            free(entries); /* This should NOT be SDL_free() */
+            LINUX_ScanInputDevices();
 
             last_input_dir_mtime = sb.st_mtime;
         }