SDL: The names of joysticks and gamepads are valid after they've been removed

From 6109fa6794e741780ada636dbe65e7de1c4212a8 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 15 Oct 2025 21:43:02 -0700
Subject: [PATCH] The names of joysticks and gamepads are valid after they've
 been removed

---
 src/joystick/SDL_gamepad.c  | 68 ++++++++++++++++++++++++++++++-------
 src/joystick/SDL_joystick.c | 52 +++++++++++++++++++++++++---
 src/test/SDL_test_common.c  | 16 ++++-----
 3 files changed, 111 insertions(+), 25 deletions(-)

diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index d2e2ca3274db7..4f56cf752d182 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -74,6 +74,7 @@
 
 static bool SDL_gamepads_initialized;
 static SDL_Gamepad *SDL_gamepads SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
+static SDL_HashTable *SDL_gamepad_names SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
 
 // The face button style of a gamepad
 typedef enum
@@ -372,6 +373,45 @@ static void RecenterGamepad(SDL_Gamepad *gamepad)
     }
 }
 
+static const char *SDL_UpdateGamepadNameForID(SDL_JoystickID instance_id)
+{
+    const char *current_name = NULL;
+
+    GamepadMapping_t *mapping = SDL_PrivateGetGamepadMapping(instance_id, true);
+    if (mapping) {
+        if (SDL_strcmp(mapping->name, "*") == 0) {
+            current_name = SDL_GetJoystickNameForID(instance_id);
+        } else {
+            current_name = mapping->name;
+        }
+    }
+
+    if (!SDL_gamepad_names) {
+        return SDL_GetPersistentString(current_name);
+    }
+
+    char *name = NULL;
+    bool found = SDL_FindInHashTable(SDL_gamepad_names, (const void *)(uintptr_t)instance_id, (const void **)&name);
+    if (!current_name) {
+        if (!found) {
+            SDL_SetError("Gamepad %" SDL_PRIu32 " not found", instance_id);
+            return NULL;
+        }
+        if (!name) {
+            // SDL_strdup() failed during insert
+            SDL_OutOfMemory();
+            return NULL;
+        }
+        return name;
+    }
+
+    if (!name || SDL_strcmp(name, current_name) != 0) {
+        name = SDL_strdup(current_name);
+        SDL_InsertIntoHashTable(SDL_gamepad_names, (const void *)(uintptr_t)instance_id, name, true);
+    }
+    return name;
+}
+
 void SDL_PrivateGamepadAdded(SDL_JoystickID instance_id)
 {
     SDL_Event event;
@@ -380,6 +420,8 @@ void SDL_PrivateGamepadAdded(SDL_JoystickID instance_id)
         return;
     }
 
+    SDL_UpdateGamepadNameForID(instance_id);
+
     event.type = SDL_EVENT_GAMEPAD_ADDED;
     event.common.timestamp = 0;
     event.gdevice.which = instance_id;
@@ -2969,6 +3011,10 @@ bool SDL_InitGamepads(void)
 
     SDL_gamepads_initialized = true;
 
+    SDL_LockJoysticks();
+
+    SDL_gamepad_names = SDL_CreateHashTable(0, false, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL);
+
     // Watch for joystick events and fire gamepad ones if needed
     SDL_AddEventWatch(SDL_GamepadEventWatcher, NULL);
 
@@ -2983,6 +3029,8 @@ bool SDL_InitGamepads(void)
         SDL_free(joysticks);
     }
 
+    SDL_UnlockJoysticks();
+
     return true;
 }
 
@@ -3029,19 +3077,10 @@ SDL_JoystickID *SDL_GetGamepads(int *count)
 
 const char *SDL_GetGamepadNameForID(SDL_JoystickID instance_id)
 {
-    const char *result = NULL;
+    const char *result;
 
     SDL_LockJoysticks();
-    {
-        GamepadMapping_t *mapping = SDL_PrivateGetGamepadMapping(instance_id, true);
-        if (mapping) {
-            if (SDL_strcmp(mapping->name, "*") == 0) {
-                result = SDL_GetJoystickNameForID(instance_id);
-            } else {
-                result = SDL_GetPersistentString(mapping->name);
-            }
-        }
-    }
+    result = SDL_UpdateGamepadNameForID(instance_id);
     SDL_UnlockJoysticks();
 
     return result;
@@ -3212,7 +3251,7 @@ bool SDL_ShouldIgnoreGamepad(Uint16 vendor_id, Uint16 product_id, Uint16 version
                     return true;
                 }
                 break;
-            
+
             case GAMEPAD_BLACKLIST_END:
                 if (SDL_endswith(name, blacklist_word->str)) {
                     return true;
@@ -4298,6 +4337,11 @@ void SDL_QuitGamepads(void)
         SDL_CloseGamepad(SDL_gamepads);
     }
 
+    if (SDL_gamepad_names) {
+        SDL_DestroyHashTable(SDL_gamepad_names);
+        SDL_gamepad_names = NULL;
+    }
+
     SDL_UnlockJoysticks();
 }
 
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index 7e8c726dad740..9bbbd848f9a13 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -123,6 +123,7 @@ static bool SDL_joystick_being_added;
 static SDL_Joystick *SDL_joysticks SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
 static int SDL_joystick_player_count SDL_GUARDED_BY(SDL_joystick_lock) = 0;
 static SDL_JoystickID *SDL_joystick_players SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
+static SDL_HashTable *SDL_joystick_names SDL_GUARDED_BY(SDL_joystick_lock) = NULL;
 static bool SDL_joystick_allows_background_events = false;
 
 static Uint32 initial_old_xboxone_controllers[] = {
@@ -833,6 +834,8 @@ bool SDL_InitJoysticks(void)
 
     SDL_joysticks_initialized = true;
 
+    SDL_joystick_names = SDL_CreateHashTable(0, false, SDL_HashID, SDL_KeyMatchID, SDL_DestroyHashValue, NULL);
+
     SDL_LoadVIDPIDList(&old_xboxone_controllers);
     SDL_LoadVIDPIDList(&arcadestick_devices);
     SDL_LoadVIDPIDList(&blacklist_devices);
@@ -980,20 +983,52 @@ const SDL_SteamVirtualGamepadInfo *SDL_GetJoystickVirtualGamepadInfoForID(SDL_Jo
 /*
  * Get the implementation dependent name of a joystick
  */
-const char *SDL_GetJoystickNameForID(SDL_JoystickID instance_id)
+static const char *SDL_UpdateJoystickNameForID(SDL_JoystickID instance_id)
 {
     SDL_JoystickDriver *driver;
     int device_index;
-    const char *name = NULL;
+    const char *current_name = NULL;
     const SDL_SteamVirtualGamepadInfo *info;
 
-    SDL_LockJoysticks();
     info = SDL_GetJoystickVirtualGamepadInfoForID(instance_id);
     if (info) {
-        name = SDL_GetPersistentString(info->name);
+        current_name = info->name;
     } else if (SDL_GetDriverAndJoystickIndex(instance_id, &driver, &device_index)) {
-        name = SDL_GetPersistentString(driver->GetDeviceName(device_index));
+        current_name = driver->GetDeviceName(device_index);
+    }
+
+    if (!SDL_joystick_names) {
+        return SDL_GetPersistentString(current_name);
+    }
+
+    char *name = NULL;
+    bool found = SDL_FindInHashTable(SDL_joystick_names, (const void *)(uintptr_t)instance_id, (const void **)&name);
+    if (!current_name) {
+        if (!found) {
+            SDL_SetError("Joystick %" SDL_PRIu32 " not found", instance_id);
+            return NULL;
+        }
+        if (!name) {
+            // SDL_strdup() failed during insert
+            SDL_OutOfMemory();
+            return NULL;
+        }
+        return name;
+    }
+
+    if (!name || SDL_strcmp(name, current_name) != 0) {
+        name = SDL_strdup(current_name);
+        SDL_InsertIntoHashTable(SDL_joystick_names, (const void *)(uintptr_t)instance_id, name, true);
     }
+    return name;
+}
+
+const char *SDL_GetJoystickNameForID(SDL_JoystickID instance_id)
+{
+    const char *name;
+
+    SDL_LockJoysticks();
+    name = SDL_UpdateJoystickNameForID(instance_id);
     SDL_UnlockJoysticks();
 
     return name;
@@ -2234,6 +2269,11 @@ void SDL_QuitJoysticks(void)
 
     SDL_QuitGamepadMappings();
 
+    if (SDL_joystick_names) {
+        SDL_DestroyHashTable(SDL_joystick_names);
+        SDL_joystick_names = NULL;
+    }
+
     SDL_joysticks_quitting = false;
     SDL_joysticks_initialized = false;
 
@@ -2343,6 +2383,8 @@ void SDL_PrivateJoystickAdded(SDL_JoystickID instance_id)
         SDL_SetJoystickIDForPlayerIndex(player_index, instance_id);
     }
 
+    SDL_UpdateJoystickNameForID(instance_id);
+
     {
         SDL_Event event;
 
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index c1158df7002f0..d900f27a7f859 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -1811,12 +1811,12 @@ void SDLTest_PrintEvent(const SDL_Event *event)
                 event->wheel.x, event->wheel.y, event->wheel.direction, event->wheel.windowID);
         break;
     case SDL_EVENT_JOYSTICK_ADDED:
-        SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " attached",
-                event->jdevice.which);
+        SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " (%s) attached",
+                event->jdevice.which, SDL_GetJoystickNameForID(event->jdevice.which));
         break;
     case SDL_EVENT_JOYSTICK_REMOVED:
-        SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " removed",
-                event->jdevice.which);
+        SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " (%s) removed",
+                event->jdevice.which, SDL_GetJoystickNameForID(event->jdevice.which));
         break;
     case SDL_EVENT_JOYSTICK_AXIS_MOTION:
         SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " axis %d value: %d",
@@ -1877,12 +1877,12 @@ void SDLTest_PrintEvent(const SDL_Event *event)
                 event->jbattery.which, event->jbattery.percent);
         break;
     case SDL_EVENT_GAMEPAD_ADDED:
-        SDL_Log("SDL EVENT: Gamepad %" SDL_PRIu32 " attached",
-                event->gdevice.which);
+        SDL_Log("SDL EVENT: Gamepad %" SDL_PRIu32 " (%s) attached",
+                event->gdevice.which, SDL_GetGamepadNameForID(event->gdevice.which));
         break;
     case SDL_EVENT_GAMEPAD_REMOVED:
-        SDL_Log("SDL EVENT: Gamepad %" SDL_PRIu32 " removed",
-                event->gdevice.which);
+        SDL_Log("SDL EVENT: Gamepad %" SDL_PRIu32 " (%s) removed",
+                event->gdevice.which, SDL_GetGamepadNameForID(event->gdevice.which));
         break;
     case SDL_EVENT_GAMEPAD_REMAPPED:
         SDL_Log("SDL EVENT: Gamepad %" SDL_PRIu32 " mapping changed",