SDL: Cleaned up udev support

From d6fbbbe14dddfa09d6168267a0fcd39dea5a398a Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 30 Oct 2026 07:46:23 -0700
Subject: [PATCH] Cleaned up udev support

You can call SDL_UDEV_GetUdevSyms() independently of SDL_UDEV_Init(). The first loads the udev library and symbols, and the second creates a udev context and registers for hotplug events.

This also makes sure the each call to SDL_UDEV_GetUdevSyms() has a matching SDL_UDEV_ReleaseUdevSyms() and each call to SDL_UDEV_Init() has a matching SDL_UDEV_Quit().
---
 src/camera/v4l2/SDL_camera_v4l2.c |  10 +-
 src/core/linux/SDL_udev.c         | 148 ++++++++++++++----------------
 src/core/linux/SDL_udev.h         |  15 ---
 src/hidapi/SDL_hidapi.c           |  21 ++++-
 4 files changed, 94 insertions(+), 100 deletions(-)

diff --git a/src/camera/v4l2/SDL_camera_v4l2.c b/src/camera/v4l2/SDL_camera_v4l2.c
index 55e5d675adcf6..e440618ecc009 100644
--- a/src/camera/v4l2/SDL_camera_v4l2.c
+++ b/src/camera/v4l2/SDL_camera_v4l2.c
@@ -899,6 +899,8 @@ static void V4L2_FreeDeviceHandle(SDL_Camera *device)
 }
 
 #ifdef SDL_USE_LIBUDEV
+static bool V4L2_udev_initialized;
+
 static bool FindV4L2CameraByPathCallback(void *userdata, SDL_Camera *device)
 {
     const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
@@ -927,8 +929,11 @@ static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, c
 static void V4L2_Deinitialize(void)
 {
 #ifdef SDL_USE_LIBUDEV
-    SDL_UDEV_DelCallback(CameraUdevCallback);
-    SDL_UDEV_Quit();
+    if (V4L2_udev_initialized) {
+        SDL_UDEV_DelCallback(CameraUdevCallback);
+        SDL_UDEV_Quit();
+        V4L2_udev_initialized = false;
+    }
 #endif // SDL_USE_LIBUDEV
 }
 
@@ -936,6 +941,7 @@ static void V4L2_DetectDevices(void)
 {
 #ifdef SDL_USE_LIBUDEV
     if (SDL_UDEV_Init()) {
+        V4L2_udev_initialized = true;
         if (SDL_UDEV_AddCallback(CameraUdevCallback)) {
             SDL_UDEV_Scan();  // Force a scan to build the initial device list
         }
diff --git a/src/core/linux/SDL_udev.c b/src/core/linux/SDL_udev.c
index 941c93b73ac38..0fa321b0b651c 100644
--- a/src/core/linux/SDL_udev.c
+++ b/src/core/linux/SDL_udev.c
@@ -52,11 +52,25 @@ SDL_ELF_NOTE_DLOPEN(
 )
 #endif
 
-static SDL_UDEV_PrivateData *_this = NULL;
+typedef struct SDL_UDEV_Context
+{
+    SDL_SpinLock syms_lock;
+    int syms_ref_count;
+    SDL_SharedObject *udev_handle;
+    SDL_UDEV_Symbols syms;
+
+    // The udev context for the main thread
+    int udev_ref_count;
+    struct udev *udev;
+    struct udev_monitor *udev_mon;
+    SDL_UDEV_CallbackList *first, *last;
+
+} SDL_UDEV_Context;
+
+static SDL_UDEV_Context SDL_udev_context, *_this = &SDL_udev_context;
 
 #ifdef SDL_UDEV_DYNAMIC
 static bool SDL_UDEV_load_sym(const char *fn, void **addr);
-static bool SDL_UDEV_load_syms(void);
 #endif
 static bool SDL_UDEV_hotplug_update_available(void);
 static void get_caps(struct udev_device *dev, struct udev_device *pdev, const char *attr, unsigned long *bitmask, size_t bitmask_len);
@@ -79,7 +93,6 @@ static bool SDL_UDEV_load_sym(const char *fn, void **addr)
 
 static bool SDL_UDEV_load_syms(void)
 {
-
 #ifdef SDL_UDEV_DYNAMIC
 /* cast funcs to char* first, to please GCC's strict aliasing rules. */
 #define SDL_UDEV_SYM(x)                                          \
@@ -148,17 +161,14 @@ static bool SDL_UDEV_hotplug_update_available(void)
 
 bool SDL_UDEV_Init(void)
 {
-    if (!_this) {
-        _this = (SDL_UDEV_PrivateData *)SDL_calloc(1, sizeof(*_this));
-        if (!_this) {
-            return false;
-        }
-
-        if (!SDL_UDEV_LoadLibrary()) {
-            SDL_UDEV_Quit();
+    if (_this->udev_ref_count == 0) {
+        if (!SDL_UDEV_GetUdevSyms()) {
             return false;
         }
+    }
 
+    ++_this->udev_ref_count;
+    if (_this->udev_ref_count == 1) {
         /* Set up udev monitoring
          * Listen for input devices (mouse, keyboard, joystick, etc) and sound devices
          */
@@ -179,26 +189,16 @@ bool SDL_UDEV_Init(void)
         _this->syms.udev_monitor_filter_add_match_subsystem_devtype(_this->udev_mon, "sound", NULL);
         _this->syms.udev_monitor_filter_add_match_subsystem_devtype(_this->udev_mon, "video4linux", NULL);
         _this->syms.udev_monitor_enable_receiving(_this->udev_mon);
-
-        // Do an initial scan of existing devices
-        SDL_UDEV_Scan();
     }
-
-    _this->ref_count += 1;
-
     return true;
 }
 
 void SDL_UDEV_Quit(void)
 {
-    if (!_this) {
-        return;
-    }
-
-    _this->ref_count -= 1;
-
-    if (_this->ref_count < 1) {
+    SDL_assert(_this->udev_ref_count > 0);
 
+    --_this->udev_ref_count;
+    if (_this->udev_ref_count == 0) {
         if (_this->udev_mon) {
             _this->syms.udev_monitor_unref(_this->udev_mon);
             _this->udev_mon = NULL;
@@ -208,16 +208,15 @@ void SDL_UDEV_Quit(void)
             _this->udev = NULL;
         }
 
-        // Remove existing devices
+        // Remove existing callbacks
         while (_this->first) {
             SDL_UDEV_CallbackList *item = _this->first;
             _this->first = _this->first->next;
             SDL_free(item);
         }
+        _this->last = NULL;
 
-        SDL_UDEV_UnloadLibrary();
-        SDL_free(_this);
-        _this = NULL;
+        SDL_UDEV_ReleaseUdevSyms();
     }
 }
 
@@ -227,13 +226,12 @@ bool SDL_UDEV_Scan(void)
     struct udev_list_entry *devs = NULL;
     struct udev_list_entry *item = NULL;
 
-    if (!_this) {
+    if (!_this->udev) {
         return true;
     }
 
     enumerate = _this->syms.udev_enumerate_new(_this->udev);
     if (!enumerate) {
-        SDL_UDEV_Quit();
         return SDL_SetError("udev_enumerate_new() failed");
     }
 
@@ -264,7 +262,7 @@ bool SDL_UDEV_GetProductInfo(const char *device_path, struct input_id *inpid, in
     const char *val;
     int class_temp;
 
-    if (!_this) {
+    if (!_this->udev) {
         return false;
     }
 
@@ -328,7 +326,7 @@ char *SDL_UDEV_GetProductSerial(const char *device_path)
     const char *val;
     char *result = NULL;
 
-    if (!_this) {
+    if (!_this->udev) {
         return NULL;
     }
 
@@ -359,64 +357,50 @@ char *SDL_UDEV_GetProductSerial(const char *device_path)
     return result;
 }
 
-void SDL_UDEV_UnloadLibrary(void)
+static void SDL_UDEV_UnloadLibrary(void)
 {
-    if (!_this) {
-        return;
-    }
-
     if (_this->udev_handle) {
         SDL_UnloadObject(_this->udev_handle);
         _this->udev_handle = NULL;
     }
+    SDL_zero(_this->syms);
 }
 
-bool SDL_UDEV_LoadLibrary(void)
+static bool SDL_UDEV_LoadLibrary(void)
 {
-    bool result = true;
-
-    if (!_this) {
-        return SDL_SetError("UDEV not initialized");
+    if (_this->syms.udev_new) {
+        return true;
     }
 
     // See if there is a udev library already loaded
     if (SDL_UDEV_load_syms()) {
         return true;
     }
+    SDL_UDEV_UnloadLibrary();
 
 #ifdef SDL_UDEV_DYNAMIC
     // Check for the build environment's libudev first
-    if (!_this->udev_handle) {
-        _this->udev_handle = SDL_LoadObject(SDL_UDEV_DYNAMIC);
-        if (_this->udev_handle) {
-            result = SDL_UDEV_load_syms();
-            if (!result) {
-                SDL_UDEV_UnloadLibrary();
-            }
+    _this->udev_handle = SDL_LoadObject(SDL_UDEV_DYNAMIC);
+    if (_this->udev_handle) {
+        if (SDL_UDEV_load_syms()) {
+            return true;
         }
+        SDL_UDEV_UnloadLibrary();
     }
 #endif
 
-    if (!_this->udev_handle) {
-        for (int i = 0; i < SDL_arraysize(SDL_UDEV_LIBS); i++) {
-            _this->udev_handle = SDL_LoadObject(SDL_UDEV_LIBS[i]);
-            if (_this->udev_handle) {
-                result = SDL_UDEV_load_syms();
-                if (!result) {
-                    SDL_UDEV_UnloadLibrary();
-                } else {
-                    break;
-                }
+    for (int i = 0; i < SDL_arraysize(SDL_UDEV_LIBS); i++) {
+        _this->udev_handle = SDL_LoadObject(SDL_UDEV_LIBS[i]);
+        if (_this->udev_handle) {
+            if (SDL_UDEV_load_syms()) {
+                return true;
             }
-        }
-
-        if (!_this->udev_handle) {
-            result = false;
-            // Don't call SDL_SetError(): SDL_LoadObject already did.
+            SDL_UDEV_UnloadLibrary();
         }
     }
 
-    return result;
+    // Don't call SDL_SetError(): SDL_LoadObject or SDL_LoadFunction already did.
+    return false;
 }
 
 static void get_caps(struct udev_device *dev, struct udev_device *pdev, const char *attr, unsigned long *bitmask, size_t bitmask_len)
@@ -601,10 +585,6 @@ void SDL_UDEV_Poll(void)
     struct udev_device *dev = NULL;
     const char *action = NULL;
 
-    if (!_this) {
-        return;
-    }
-
     while (SDL_UDEV_hotplug_update_available()) {
         dev = _this->syms.udev_monitor_receive_device(_this->udev_mon);
         if (!dev) {
@@ -627,6 +607,11 @@ void SDL_UDEV_Poll(void)
 bool SDL_UDEV_AddCallback(SDL_UDEV_Callback cb)
 {
     SDL_UDEV_CallbackList *item;
+
+    if (_this->udev_ref_count < 1) {
+        return SDL_SetError("UDEV not initialized");
+    }
+
     item = (SDL_UDEV_CallbackList *)SDL_calloc(1, sizeof(SDL_UDEV_CallbackList));
     if (!item) {
         return false;
@@ -649,10 +634,6 @@ void SDL_UDEV_DelCallback(SDL_UDEV_Callback cb)
     SDL_UDEV_CallbackList *item;
     SDL_UDEV_CallbackList *prev = NULL;
 
-    if (!_this) {
-        return;
-    }
-
     for (item = _this->first; item; item = item->next) {
         // found it, remove it.
         if (item->callback == cb) {
@@ -674,17 +655,28 @@ void SDL_UDEV_DelCallback(SDL_UDEV_Callback cb)
 
 const SDL_UDEV_Symbols *SDL_UDEV_GetUdevSyms(void)
 {
-    if (!SDL_UDEV_Init()) {
-        SDL_SetError("Could not initialize UDEV");
-        return NULL;
+    const SDL_UDEV_Symbols *syms = NULL;
+
+    SDL_LockSpinlock(&_this->syms_lock);
+    if (SDL_UDEV_LoadLibrary()) {
+        ++_this->syms_ref_count;
+        syms = &_this->syms;
     }
+    SDL_UnlockSpinlock(&_this->syms_lock);
 
-    return &_this->syms;
+    return syms;
 }
 
 void SDL_UDEV_ReleaseUdevSyms(void)
 {
-    SDL_UDEV_Quit();
+    SDL_LockSpinlock(&_this->syms_lock);
+    SDL_assert(_this->syms_ref_count > 0);
+
+    --_this->syms_ref_count;
+    if (_this->syms_ref_count == 0) {
+        SDL_UDEV_UnloadLibrary();
+    }
+    SDL_UnlockSpinlock(&_this->syms_lock);
 }
 
 #endif // SDL_USE_LIBUDEV
diff --git a/src/core/linux/SDL_udev.h b/src/core/linux/SDL_udev.h
index 8fe273d15d428..b62f2a5709396 100644
--- a/src/core/linux/SDL_udev.h
+++ b/src/core/linux/SDL_udev.h
@@ -91,23 +91,8 @@ typedef struct SDL_UDEV_Symbols
 
 } SDL_UDEV_Symbols;
 
-typedef struct SDL_UDEV_PrivateData
-{
-    const char *udev_library;
-    SDL_SharedObject *udev_handle;
-    struct udev *udev;
-    struct udev_monitor *udev_mon;
-    int ref_count;
-    SDL_UDEV_CallbackList *first, *last;
-
-    // Function pointers
-    SDL_UDEV_Symbols syms;
-} SDL_UDEV_PrivateData;
-
 extern bool SDL_UDEV_Init(void);
 extern void SDL_UDEV_Quit(void);
-extern void SDL_UDEV_UnloadLibrary(void);
-extern bool SDL_UDEV_LoadLibrary(void);
 extern void SDL_UDEV_Poll(void);
 extern bool SDL_UDEV_Scan(void);
 extern bool SDL_UDEV_GetProductInfo(const char *device_path, struct input_id *inpid, int *class, char **driver);
diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c
index 5d4291f901e62..deeab48857846 100644
--- a/src/hidapi/SDL_hidapi.c
+++ b/src/hidapi/SDL_hidapi.c
@@ -281,6 +281,8 @@ static void HIDAPI_InitializeDiscovery(void)
             if (SDL_HIDAPI_discovery.m_pUdev != NULL) {
                 SDL_HIDAPI_discovery.m_pUdevMonitor = usyms->udev_monitor_new_from_netlink(SDL_HIDAPI_discovery.m_pUdev, "udev");
                 if (SDL_HIDAPI_discovery.m_pUdevMonitor != NULL) {
+                    usyms->udev_monitor_filter_add_match_subsystem_devtype(SDL_HIDAPI_discovery.m_pUdevMonitor, "hidraw", NULL);
+                    usyms->udev_monitor_filter_add_match_subsystem_devtype(SDL_HIDAPI_discovery.m_pUdevMonitor, "usb", NULL);
                     usyms->udev_monitor_enable_receiving(SDL_HIDAPI_discovery.m_pUdevMonitor);
                     SDL_HIDAPI_discovery.m_nUdevFd = usyms->udev_monitor_get_fd(SDL_HIDAPI_discovery.m_pUdevMonitor);
                     SDL_HIDAPI_discovery.m_bCanGetNotifications = true;
@@ -1178,10 +1180,18 @@ int SDL_hid_init(void)
     ++attempts;
 #ifdef SDL_PLATFORM_LINUX
     udev_ctx = SDL_UDEV_GetUdevSyms();
-#endif // __LINUX __
-    if (udev_ctx && PLATFORM_hid_init() == 0) {
+    if (udev_ctx && PLATFORM_hid_init() != 0) {
+        SDL_UDEV_ReleaseUdevSyms();
+        udev_ctx = NULL;
+    }
+    if (udev_ctx) {
+        ++success;
+    }
+#else
+    if (PLATFORM_hid_init() == 0) {
         ++success;
     }
+#endif
 #endif // HAVE_PLATFORM_BACKEND
 
     if (attempts > 0 && success == 0) {
@@ -1216,10 +1226,11 @@ int SDL_hid_exit(void)
 #ifdef HAVE_PLATFORM_BACKEND
     if (udev_ctx) {
         result |= PLATFORM_hid_exit();
-    }
 #ifdef SDL_PLATFORM_LINUX
-    SDL_UDEV_ReleaseUdevSyms();
-#endif // __LINUX __
+        SDL_UDEV_ReleaseUdevSyms();
+        udev_ctx = NULL;
+#endif
+    }
 #endif // HAVE_PLATFORM_BACKEND
 
 #ifdef HAVE_LIBUSB