SDL: x11: Hook up display hotplug notifications.

From a236bf4f252468f4f5d22fec7d9c11ceb23943cb Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 6 Jun 2022 02:13:37 -0400
Subject: [PATCH] x11: Hook up display hotplug notifications.

Obviously this needs XRandR support.

Fixes #4977.
---
 src/video/x11/SDL_x11events.c |   6 +
 src/video/x11/SDL_x11modes.c  | 313 ++++++++++++++++++++++------------
 src/video/x11/SDL_x11modes.h  |   4 +
 src/video/x11/SDL_x11sym.h    |   2 +
 src/video/x11/SDL_x11video.h  |   2 +
 5 files changed, 214 insertions(+), 113 deletions(-)

diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index 63091fe9800..6e3c0679b63 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -785,6 +785,12 @@ X11_DispatchEvent(_THIS, XEvent *xevent)
     }
 #endif
 
+#if SDL_VIDEO_DRIVER_X11_XRANDR
+    if (videodata->xrandr_event_base && (xevent->type == (videodata->xrandr_event_base + RRNotify))) {
+        X11_HandleXRandREvent(_this, xevent);
+    }
+#endif
+
     /* Send a SDL_SYSWMEVENT if the application wants them */
     if (SDL_GetEventState(SDL_SYSWMEVENT) == SDL_ENABLE) {
         SDL_SysWMmsg wmmsg;
diff --git a/src/video/x11/SDL_x11modes.c b/src/video/x11/SDL_x11modes.c
index d740f66ae26..c322b4ddadf 100644
--- a/src/video/x11/SDL_x11modes.c
+++ b/src/video/x11/SDL_x11modes.c
@@ -313,6 +313,194 @@ SetXRandRDisplayName(Display *dpy, Atom EDID, char *name, const size_t namelen,
 #endif
 }
 
+
+static int
+X11_AddXRandRDisplay(_THIS, Display *dpy, int screen, RROutput outputid, XRRScreenResources *res)
+{
+    Atom EDID = X11_XInternAtom(dpy, "EDID", False);
+    XRROutputInfo *output_info;
+    int display_x, display_y;
+    unsigned long display_mm_width, display_mm_height;
+    SDL_DisplayData *displaydata;
+    char display_name[128];
+    SDL_DisplayMode mode;
+    SDL_DisplayModeData *modedata;
+    SDL_VideoDisplay display;
+    RRMode modeID;
+    RRCrtc output_crtc;
+    XRRCrtcInfo *crtc;
+    XVisualInfo vinfo;
+    Uint32 pixelformat;
+    XPixmapFormatValues *pixmapformats;
+    int scanline_pad;
+    int xft_dpi = 0;
+    int i, n;
+
+    if (get_visualinfo(dpy, screen, &vinfo) < 0) {
+        return 0;  /* uh, skip this screen? */
+    }
+
+    pixelformat = X11_GetPixelFormatFromVisualInfo(dpy, &vinfo);
+    if (SDL_ISPIXELFORMAT_INDEXED(pixelformat)) {
+        return 0;  /* Palettized video modes are no longer supported, ignore this one. */
+    }
+
+    scanline_pad = SDL_BYTESPERPIXEL(pixelformat) * 8;
+    pixmapformats = X11_XListPixmapFormats(dpy, &n);
+    if (pixmapformats) {
+        for (i = 0; i < n; i++) {
+            if (pixmapformats[i].depth == vinfo.depth) {
+                scanline_pad = pixmapformats[i].scanline_pad;
+                break;
+            }
+        }
+        X11_XFree(pixmapformats);
+    }
+
+    output_info = X11_XRRGetOutputInfo(dpy, res, outputid);
+    if (!output_info || !output_info->crtc || output_info->connection == RR_Disconnected) {
+        X11_XRRFreeOutputInfo(output_info);
+        return 0;  /* ignore this one. */
+    }
+
+    SDL_strlcpy(display_name, output_info->name, sizeof(display_name));
+    display_mm_width = output_info->mm_width;
+    display_mm_height = output_info->mm_height;
+    output_crtc = output_info->crtc;
+    X11_XRRFreeOutputInfo(output_info);
+
+    crtc = X11_XRRGetCrtcInfo(dpy, res, output_crtc);
+    if (!crtc) {
+        return 0;  /* oh well, ignore it. */
+    }
+
+    SDL_zero(mode);
+    modeID = crtc->mode;
+    mode.w = crtc->width;
+    mode.h = crtc->height;
+    mode.format = pixelformat;
+
+    display_x = crtc->x;
+    display_y = crtc->y;
+
+    X11_XRRFreeCrtcInfo(crtc);
+
+    displaydata = (SDL_DisplayData *) SDL_calloc(1, sizeof(*displaydata));
+    if (!displaydata) {
+        return SDL_OutOfMemory();
+    }
+
+    modedata = (SDL_DisplayModeData *) SDL_calloc(1, sizeof(SDL_DisplayModeData));
+    if (!modedata) {
+        SDL_free(displaydata);
+        return SDL_OutOfMemory();
+    }
+
+    modedata->xrandr_mode = modeID;
+    mode.driverdata = modedata;
+
+    displaydata->screen = screen;
+    displaydata->visual = vinfo.visual;
+    displaydata->depth = vinfo.depth;
+    displaydata->hdpi = display_mm_width ? (((float) mode.w) * 25.4f / display_mm_width) : 0.0f;
+    displaydata->vdpi = display_mm_height ? (((float) mode.h) * 25.4f / display_mm_height) : 0.0f;
+    displaydata->ddpi = SDL_ComputeDiagonalDPI(mode.w, mode.h, ((float) display_mm_width) / 25.4f,((float) display_mm_height) / 25.4f);
+
+    /* if xft dpi is available we will use this over xrandr (!!! FIXME: ...yeah?) */
+    xft_dpi = GetXftDPI(dpy);
+    if (xft_dpi > 0) {
+        displaydata->hdpi = (float) xft_dpi;
+        displaydata->vdpi = (float) xft_dpi;
+    }
+
+    displaydata->scanline_pad = scanline_pad;
+    displaydata->x = display_x;
+    displaydata->y = display_y;
+    displaydata->use_xrandr = SDL_TRUE;
+    displaydata->xrandr_output = outputid;
+
+    SetXRandRModeInfo(dpy, res, output_crtc, modeID, &mode);
+    SetXRandRDisplayName(dpy, EDID, display_name, sizeof (display_name), outputid, display_mm_width, display_mm_height);
+
+    SDL_zero(display);
+    if (*display_name) {
+        display.name = display_name;
+    }
+    display.desktop_mode = mode;
+    display.current_mode = mode;
+    display.driverdata = displaydata;
+    return SDL_AddVideoDisplay(&display, SDL_TRUE);
+}
+
+static void
+X11_HandleXRandROutputChange(_THIS, const XRROutputChangeNotifyEvent *ev)
+{
+    const int num_displays = SDL_GetNumVideoDisplays();
+    SDL_VideoDisplay *display = NULL;
+    int displayidx = -1;
+    int i;
+
+    #if 0
+    printf("XRROutputChangeNotifyEvent! [output=%u, crtc=%u, mode=%u, rotation=%u, connection=%u]", (unsigned int) ev->output, (unsigned int) ev->crtc, (unsigned int) ev->mode, (unsigned int) ev->rotation, (unsigned int) ev->connection);
+    #endif
+
+    for (i = 0; i < num_displays; i++) {
+        SDL_VideoDisplay *thisdisplay = SDL_GetDisplay(i);
+        const SDL_DisplayData *displaydata = (const SDL_DisplayData *) thisdisplay->driverdata;
+        if (displaydata->xrandr_output == ev->output) {
+            display = thisdisplay;
+            displayidx = i;
+            break;
+        }
+    }
+
+    SDL_assert((displayidx == -1) == (display == NULL));
+
+    if (ev->connection == RR_Disconnected) {  /* output is going away */
+        if (display != NULL) {
+            SDL_DelVideoDisplay(displayidx);
+        }
+    } else if (ev->connection == RR_Connected) {  /* output is coming online */
+        if (display != NULL) {
+            /* !!! FIXME: update rotation or current mode of existing display? */
+        } else {
+            Display *dpy = ev->display;
+            const int screen = DefaultScreen(dpy);
+            XVisualInfo vinfo;
+            if (get_visualinfo(dpy, screen, &vinfo) == 0) {
+                XRRScreenResources *res = X11_XRRGetScreenResourcesCurrent(dpy, RootWindow(dpy, screen));
+                if (!res || res->noutput == 0) {
+                    if (res) {
+                        X11_XRRFreeScreenResources(res);
+                    }
+                    res = X11_XRRGetScreenResources(dpy, RootWindow(dpy, screen));
+                }
+
+                if (res) {
+                    X11_AddXRandRDisplay(_this, dpy, screen, ev->output, res);
+                    X11_XRRFreeScreenResources(res);
+                }
+            }
+        }
+    }
+}
+
+void
+X11_HandleXRandREvent(_THIS, const XEvent *xevent)
+{
+    SDL_VideoData *videodata = (SDL_VideoData *) _this->driverdata;
+    SDL_assert(xevent->type == (videodata->xrandr_event_base + RRNotify));
+
+    switch (((const XRRNotifyEvent *) xevent)->subtype) {
+        case RRNotify_OutputChange:
+            X11_HandleXRandROutputChange(_this, (const XRROutputChangeNotifyEvent *) xevent);
+            break;
+        default:
+            break;
+    }
+}
+
+
 static int
 X11_InitModes_XRandR(_THIS)
 {
@@ -321,15 +509,15 @@ X11_InitModes_XRandR(_THIS)
     const int screencount = ScreenCount(dpy);
     const int default_screen = DefaultScreen(dpy);
     RROutput primary = X11_XRRGetOutputPrimary(dpy, RootWindow(dpy, default_screen));
-    Atom EDID = X11_XInternAtom(dpy, "EDID", False);
     XRRScreenResources *res = NULL;
-    Uint32 pixelformat;
-    XVisualInfo vinfo;
-    XPixmapFormatValues *pixmapformats;
+    int xrandr_error_base = 0;
     int looking_for_primary;
-    int scanline_pad;
     int output;
-    int screen, i, n;
+    int screen;
+
+    if (!X11_XRRQueryExtension(dpy, &data->xrandr_event_base, &xrandr_error_base)) {
+        return SDL_SetError("XRRQueryExtension failed");
+    }
 
     for (looking_for_primary = 1; looking_for_primary >= 0; looking_for_primary--) {
         for (screen = 0; screen < screencount; screen++) {
@@ -339,27 +527,6 @@ X11_InitModes_XRandR(_THIS)
                 continue;
             }
 
-            if (get_visualinfo(dpy, screen, &vinfo) < 0) {
-                continue;  /* uh, skip this screen? */
-            }
-
-            pixelformat = X11_GetPixelFormatFromVisualInfo(dpy, &vinfo);
-            if (SDL_ISPIXELFORMAT_INDEXED(pixelformat)) {
-                continue;  /* Palettized video modes are no longer supported */
-            }
-
-            scanline_pad = SDL_BYTESPERPIXEL(pixelformat) * 8;
-            pixmapformats = X11_XListPixmapFormats(dpy, &n);
-            if (pixmapformats) {
-                for (i = 0; i < n; ++i) {
-                    if (pixmapformats[i].depth == vinfo.depth) {
-                        scanline_pad = pixmapformats[i].scanline_pad;
-                        break;
-                    }
-                }
-                X11_XFree(pixmapformats);
-            }
-
             res = X11_XRRGetScreenResourcesCurrent(dpy, RootWindow(dpy, screen));
             if (!res || res->noutput == 0) {
                 if (res) {
@@ -373,100 +540,20 @@ X11_InitModes_XRandR(_THIS)
             }
 
             for (output = 0; output < res->noutput; output++) {
-                XRROutputInfo *output_info;
-                int display_x, display_y;
-                unsigned long display_mm_width, display_mm_height;
-                SDL_DisplayData *displaydata;
-                char display_name[128];
-                SDL_DisplayMode mode;
-                SDL_DisplayModeData *modedata;
-                SDL_VideoDisplay display;
-                RRMode modeID;
-                RRCrtc output_crtc;
-                XRRCrtcInfo *crtc;
-                int xft_dpi = 0;
-
                 /* The primary output _should_ always be sorted first, but just in case... */
                 if ((looking_for_primary && (res->outputs[output] != primary)) ||
                     (!looking_for_primary && (screen == default_screen) && (res->outputs[output] == primary))) {
                     continue;
                 }
-
-                output_info = X11_XRRGetOutputInfo(dpy, res, res->outputs[output]);
-                if (!output_info || !output_info->crtc || output_info->connection == RR_Disconnected) {
-                    X11_XRRFreeOutputInfo(output_info);
-                    continue;
+                if (X11_AddXRandRDisplay(_this, dpy, screen, res->outputs[output], res) == -1) {
+                    break;
                 }
-
-                SDL_strlcpy(display_name, output_info->name, sizeof(display_name));
-                display_mm_width = output_info->mm_width;
-                display_mm_height = output_info->mm_height;
-                output_crtc = output_info->crtc;
-                X11_XRRFreeOutputInfo(output_info);
-
-                crtc = X11_XRRGetCrtcInfo(dpy, res, output_crtc);
-                if (!crtc) {
-                    continue;
-                }
-
-                SDL_zero(mode);
-                modeID = crtc->mode;
-                mode.w = crtc->width;
-                mode.h = crtc->height;
-                mode.format = pixelformat;
-
-                display_x = crtc->x;
-                display_y = crtc->y;
-
-                X11_XRRFreeCrtcInfo(crtc);
-
-                displaydata = (SDL_DisplayData *) SDL_calloc(1, sizeof(*displaydata));
-                if (!displaydata) {
-                    return SDL_OutOfMemory();
-                }
-
-                modedata = (SDL_DisplayModeData *) SDL_calloc(1, sizeof(SDL_DisplayModeData));
-                if (!modedata) {
-                    SDL_free(displaydata);
-                    return SDL_OutOfMemory();
-                }
-                modedata->xrandr_mode = modeID;
-                mode.driverdata = modedata;
-
-                displaydata->screen = screen;
-                displaydata->visual = vinfo.visual;
-                displaydata->depth = vinfo.depth;
-                displaydata->hdpi = display_mm_width ? (((float) mode.w) * 25.4f / display_mm_width) : 0.0f;
-                displaydata->vdpi = display_mm_height ? (((float) mode.h) * 25.4f / display_mm_height) : 0.0f;
-                displaydata->ddpi = SDL_ComputeDiagonalDPI(mode.w, mode.h, ((float) display_mm_width) / 25.4f,((float) display_mm_height) / 25.4f);
-
-                /* if xft dpi is available we will use this over xrandr */
-                xft_dpi = GetXftDPI(dpy);
-                if(xft_dpi > 0) {
-                    displaydata->hdpi = (float)xft_dpi;
-                    displaydata->vdpi = (float)xft_dpi;
-                }
-
-                displaydata->scanline_pad = scanline_pad;
-                displaydata->x = display_x;
-                displaydata->y = display_y;
-                displaydata->use_xrandr = SDL_TRUE;
-                displaydata->xrandr_output = res->outputs[output];
-
-                SetXRandRModeInfo(dpy, res, output_crtc, modeID, &mode);
-                SetXRandRDisplayName(dpy, EDID, display_name, sizeof (display_name), res->outputs[output], display_mm_width, display_mm_height);
-
-                SDL_zero(display);
-                if (*display_name) {
-                    display.name = display_name;
-                }
-                display.desktop_mode = mode;
-                display.current_mode = mode;
-                display.driverdata = displaydata;
-                SDL_AddVideoDisplay(&display, SDL_FALSE);
             }
 
             X11_XRRFreeScreenResources(res);
+
+            /* This will generate events for displays that come and go at runtime. */
+            X11_XRRSelectInput(dpy, RootWindow(dpy, screen), RROutputChangeNotifyMask);
         }
     }
 
@@ -564,7 +651,7 @@ static int X11_InitModes_StdXlib(_THIS)
     display.desktop_mode = mode;
     display.current_mode = mode;
     display.driverdata = displaydata;
-    SDL_AddVideoDisplay(&display, SDL_FALSE);
+    SDL_AddVideoDisplay(&display, SDL_TRUE);
 
     return 0;
 }
diff --git a/src/video/x11/SDL_x11modes.h b/src/video/x11/SDL_x11modes.h
index b599e3723ad..9a4257e430c 100644
--- a/src/video/x11/SDL_x11modes.h
+++ b/src/video/x11/SDL_x11modes.h
@@ -65,6 +65,10 @@ extern int X11_GetDisplayBounds(_THIS, SDL_VideoDisplay * sdl_display, SDL_Rect
 extern int X11_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * sdl_display, SDL_Rect * rect);
 extern int X11_GetDisplayDPI(_THIS, SDL_VideoDisplay * sdl_display, float * ddpi, float * hdpi, float * vdpi);
 
+#if SDL_VIDEO_DRIVER_X11_XRANDR
+extern void X11_HandleXRandREvent(_THIS, const XEvent *xevent);
+#endif
+
 #endif /* SDL_x11modes_h_ */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/video/x11/SDL_x11sym.h b/src/video/x11/SDL_x11sym.h
index 3d22d2f9f76..1a1b102acbc 100644
--- a/src/video/x11/SDL_x11sym.h
+++ b/src/video/x11/SDL_x11sym.h
@@ -276,6 +276,7 @@ SDL_X11_SYM(XIEventMask*,XIGetSelectedEvents,(Display *a,Window b,int *c),(a,b,c
 #if SDL_VIDEO_DRIVER_X11_XRANDR
 SDL_X11_MODULE(XRANDR)
 SDL_X11_SYM(Status,XRRQueryVersion,(Display *dpy,int *major_versionp,int *minor_versionp),(dpy,major_versionp,minor_versionp),return)
+SDL_X11_SYM(Bool,XRRQueryExtension,(Display *dpy,int *event_base_return,int *error_base_return),(dpy,event_base_return,error_base_return),return);
 SDL_X11_SYM(XRRScreenConfiguration *,XRRGetScreenInfo,(Display *dpy,Drawable draw),(dpy,draw),return)
 SDL_X11_SYM(SizeID,XRRConfigCurrentConfiguration,(XRRScreenConfiguration *config,Rotation *rotation),(config,rotation),return)
 SDL_X11_SYM(short,XRRConfigCurrentRate,(XRRScreenConfiguration *config),(config),return)
@@ -297,6 +298,7 @@ SDL_X11_SYM(Atom*,XRRListOutputProperties,(Display *dpy, RROutput output, int *n
 SDL_X11_SYM(XRRPropertyInfo*,XRRQueryOutputProperty,(Display *dpy,RROutput output, Atom property),(dpy,output,property),return)
 SDL_X11_SYM(int,XRRGetOutputProperty,(Display *dpy,RROutput output, Atom property, long offset, long length, Bool _delete, Bool pending, Atom req_type, Atom *actual_type, int *actual_format, unsigned long *nitems, unsigned long *bytes_after, unsigned char **prop),(dpy,output,property,offset,length, _delete, pending, req_type, actual_type, actual_format, nitems, bytes_after, prop),return)
 SDL_X11_SYM(RROutput,XRRGetOutputPrimary,(Display *dpy,Window window),(dpy,window),return)
+SDL_X11_SYM(void,XRRSelectInput,(Display *dpy, Window window, int mask),(dpy,window,mask),)
 #endif
 
 /* MIT-SCREEN-SAVER support */
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index 29014735c7a..5360d037fe0 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -134,6 +134,8 @@ typedef struct SDL_VideoData
     SDL_Point global_mouse_position;
     Uint32 global_mouse_buttons;
 
+    int xrandr_event_base;
+
 #if SDL_VIDEO_DRIVER_X11_HAS_XKBKEYCODETOKEYSYM
     XkbDescPtr xkb;
 #endif