sdl2-compat: Fixed crash during controller hotplug in RetroArch

From 4dc33f975cb90fa39c418656f252b446c33cd18b Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 9 Mar 2025 09:53:54 -0700
Subject: [PATCH] Fixed crash during controller hotplug in RetroArch

RetroArch calls SDL_PumpEvents() followed by SDL_PeepEvents(), so we need to update the joystick list inside of Event3to2(). This event doesn't happen often, so this should be fine.
---
 src/sdl2_compat.c | 33 ++++++++++++++++++---------------
 1 file changed, 18 insertions(+), 15 deletions(-)

diff --git a/src/sdl2_compat.c b/src/sdl2_compat.c
index 0fe3aac..f381b48 100644
--- a/src/sdl2_compat.c
+++ b/src/sdl2_compat.c
@@ -2211,9 +2211,17 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
         event2->jbutton.which = JoystickID3to2(event3->jbutton.which);
         break;
     case SDL_EVENT_JOYSTICK_ADDED:
+        SDL_NumJoysticks(); /* Refresh */
+        SDL_NumHaptics(); /* Refresh */
         event2->jdevice.which = GetIndexFromJoystickInstance(event3->jdevice.which);
+        if (event2->jdevice.which < 0) {
+            /* Applications like RetroArch assume the index is always valid */
+            event2->jdevice.which = 0;
+        }
         break;
     case SDL_EVENT_JOYSTICK_REMOVED:
+        SDL_NumJoysticks(); /* Refresh */
+        SDL_NumHaptics(); /* Refresh */
         event2->jdevice.which = JoystickID3to2(event3->jdevice.which);
         break;
     case SDL_EVENT_JOYSTICK_BATTERY_UPDATED:
@@ -2250,9 +2258,17 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
         }
         break;
     case SDL_EVENT_GAMEPAD_ADDED:
+        /* Refresh the joystick list here in case the joystick added event is ignored */
+        SDL_NumJoysticks(); /* Refresh */
+        SDL_NumHaptics(); /* Refresh */
         event2->cdevice.which = GetIndexFromJoystickInstance(event3->gdevice.which);
         break;
     case SDL_EVENT_GAMEPAD_REMOVED:
+        /* Refresh the joystick list here in case the joystick removed event is ignored */
+        SDL_NumJoysticks(); /* Refresh */
+        SDL_NumHaptics(); /* Refresh */
+        event2->cdevice.which = JoystickID3to2(event3->gdevice.which);
+        break;
     case SDL_EVENT_GAMEPAD_REMAPPED:
     case SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED:
         event2->cdevice.which = JoystickID3to2(event3->gdevice.which);
@@ -2268,9 +2284,11 @@ static SDL2_Event *Event3to2(const SDL_Event *event3, SDL2_Event *event2)
         event2->csensor.timestamp_us = SDL_NS_TO_US(event3->gsensor.sensor_timestamp);
         break;
     case SDL_EVENT_AUDIO_DEVICE_ADDED:
+        SDL_GetNumAudioDevices(event3->adevice.recording ? SDL2_TRUE : SDL2_FALSE); /* Refresh */
         event2->adevice.which = GetIndexFromAudioDeviceInstance(event3->adevice.which, event3->adevice.recording);
         break;
     case SDL_EVENT_AUDIO_DEVICE_REMOVED:
+        SDL_GetNumAudioDevices(event3->adevice.recording ? SDL2_TRUE : SDL2_FALSE); /* Refresh */
         event2->adevice.which = AudioDeviceID3to2(event3->adevice.which);
         break;
     case SDL_EVENT_SENSOR_UPDATE:
@@ -2804,21 +2822,6 @@ SDL_WaitEventTimeout(SDL2_Event *event2, int timeout)
     SDL_Event event3;
     const int retval = SDL3_WaitEventTimeout(event2 ? &event3 : NULL, timeout);
     if ((retval == 1) && event2) {
-        /* Ensure joystick and haptic IDs are updated before calling Event3to2() */
-        switch (event3.type) {
-            case SDL_EVENT_JOYSTICK_ADDED:
-            case SDL_EVENT_GAMEPAD_ADDED:
-            case SDL_EVENT_GAMEPAD_REMOVED:
-            case SDL_EVENT_JOYSTICK_REMOVED:
-                SDL_NumJoysticks(); /* Refresh */
-                SDL_NumHaptics(); /* Refresh */
-                break;
-
-            case SDL_EVENT_AUDIO_DEVICE_ADDED:
-            case SDL_EVENT_AUDIO_DEVICE_REMOVED:
-                SDL_GetNumAudioDevices(event3.adevice.recording ? SDL2_TRUE : SDL2_FALSE); /* Refresh */
-                break;
-        }
         Event3to2(&event3, event2);
     }
     return retval;