SDL: wayland: Make sure the primary display is always first under GNOME

From c202893fbb0a115a72b684a542961f9a55132dea Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Thu, 25 Jan 2024 15:25:03 -0500
Subject: [PATCH] wayland: Make sure the primary display is always first under
 GNOME

GNOME can deliver displays in arbitrary order, which can even change across sleep/wake cycles, so use a DBus method to find the primary display, and try to make sure it is the first exposed by SDL.

On other window managers, continue to assume that the order in which displays are exposed is the preferred order.

This can eventually be replaced by a Wayland protocol that serves the same purpose, if the pending ones are ever finalized.
---
 src/video/wayland/SDL_waylandvideo.c | 149 ++++++++++++++++++++++++++-
 1 file changed, 144 insertions(+), 5 deletions(-)

diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 3bb4c8ba96b0..0a09eba1a1e5 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -85,6 +85,114 @@
 #define SDL_WL_OUTPUT_VERSION 3
 #endif
 
+#ifdef SDL_USE_LIBDBUS
+#include "../../core/linux/SDL_dbus.h"
+
+#define DISPLAY_INFO_NODE   "org.gnome.Mutter.DisplayConfig"
+#define DISPLAY_INFO_PATH   "/org/gnome/Mutter/DisplayConfig"
+#define DISPLAY_INFO_METHOD "GetCurrentState"
+#endif
+
+/* GNOME doesn't expose displays in any particular order, but we can find the
+ * primary display and its logical coordinates via a DBus method.
+ */
+static SDL_bool Wayland_GetPrimaryDisplayCoordinates(int *x, int *y)
+{
+#ifdef SDL_USE_LIBDBUS
+    SDL_DBusContext *dbus = SDL_DBus_GetContext();
+    DBusMessage *reply = NULL;
+    DBusMessageIter iter[3];
+    DBusMessage *msg = dbus->message_new_method_call(DISPLAY_INFO_NODE,
+                                                     DISPLAY_INFO_PATH,
+                                                     DISPLAY_INFO_NODE,
+                                                     DISPLAY_INFO_METHOD);
+
+    if (msg) {
+        reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL);
+        dbus->message_unref(msg);
+    }
+
+    if (reply) {
+        /* Serial (don't care) */
+        dbus->message_iter_init(reply, &iter[0]);
+        if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_UINT32) {
+            goto error;
+        }
+
+        /* Physical monitor array (don't care) */
+        dbus->message_iter_next(&iter[0]);
+        if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_ARRAY) {
+            goto error;
+        }
+
+        /* Logical monitor array of structs */
+        dbus->message_iter_next(&iter[0]);
+        if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_ARRAY) {
+            goto error;
+        }
+
+        /* First logical monitor struct */
+        dbus->message_iter_recurse(&iter[0], &iter[1]);
+        if (dbus->message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_STRUCT) {
+            goto error;
+        }
+
+        do {
+            int logical_x, logical_y;
+            dbus_bool_t primary;
+
+            /* Logical X */
+            dbus->message_iter_recurse(&iter[1], &iter[2]);
+            if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_INT32) {
+                goto error;
+            }
+            dbus->message_iter_get_basic(&iter[2], &logical_x);
+
+            /* Logical Y */
+            dbus->message_iter_next(&iter[2]);
+            if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_INT32) {
+                goto error;
+            }
+            dbus->message_iter_get_basic(&iter[2], &logical_y);
+
+            /* Scale (don't care) */
+            dbus->message_iter_next(&iter[2]);
+            if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_DOUBLE) {
+                goto error;
+            }
+
+            /* Transform (don't care) */
+            dbus->message_iter_next(&iter[2]);
+            if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_UINT32) {
+                goto error;
+            }
+
+            /* Primary display boolean */
+            dbus->message_iter_next(&iter[2]);
+            if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_BOOLEAN) {
+                goto error;
+            }
+            dbus->message_iter_get_basic(&iter[2], &primary);
+
+            if (primary) {
+                *x = logical_x;
+                *y = logical_y;
+
+                /* We found the primary display: success. */
+                dbus->message_unref(reply);
+                return SDL_TRUE;
+            }
+        } while (dbus->message_iter_next(&iter[1]));
+    }
+
+error:
+    if (reply) {
+        dbus->message_unref(reply);
+    }
+#endif
+    return SDL_FALSE;
+}
+
 static void display_handle_done(void *data, struct wl_output *output);
 
 /* Initialization/Query functions */
@@ -665,8 +773,7 @@ static void display_handle_done(void *data,
     }
 
     if (driverdata->display == 0) {
-        /* First time getting display info, create the VideoDisplay */
-        SDL_bool send_event = !driverdata->videodata->initializing;
+        /* First time getting display info, initialize the VideoDisplay */
         if (driverdata->physical_width >= driverdata->physical_height) {
             driverdata->placeholder.natural_orientation = SDL_ORIENTATION_LANDSCAPE;
         } else {
@@ -674,9 +781,13 @@ static void display_handle_done(void *data,
         }
         driverdata->placeholder.current_orientation = driverdata->orientation;
         driverdata->placeholder.driverdata = driverdata;
-        driverdata->display = SDL_AddVideoDisplay(&driverdata->placeholder, send_event);
-        SDL_free(driverdata->placeholder.name);
-        SDL_zero(driverdata->placeholder);
+
+        /* During initialization, the displays will be added after enumeration is complete. */
+        if (!video->initializing) {
+            driverdata->display = SDL_AddVideoDisplay(&driverdata->placeholder, SDL_TRUE);
+            SDL_free(driverdata->placeholder.name);
+            SDL_zero(driverdata->placeholder);
+        }
     } else {
         SDL_SendDisplayEvent(dpy, SDL_EVENT_DISPLAY_ORIENTATION, driverdata->orientation);
     }
@@ -770,6 +881,32 @@ static void Wayland_free_display(SDL_VideoDisplay *display)
     }
 }
 
+static void Wayland_FinalizeDisplays(SDL_VideoData *vid)
+{
+    SDL_DisplayData *d;
+    int p_x, p_y;
+
+    /* GNOME doesn't expose the displays in any preferential order, so find the primary display coordinates and use them
+     * to manually sort the primary display to the front of the list so that it is always the first exposed by SDL.
+     * Otherwise, assume that the displays were already exposed in preferential order.
+     * */
+    if (Wayland_GetPrimaryDisplayCoordinates(&p_x, &p_y)) {
+        wl_list_for_each(d, &vid->output_list, link) {
+            if (d->x == p_x && d->y == p_y) {
+                WAYLAND_wl_list_remove(&d->link);
+                WAYLAND_wl_list_insert(&vid->output_list, &d->link);
+                break;
+            }
+        }
+    }
+
+    wl_list_for_each(d, &vid->output_list, link) {
+        d->display = SDL_AddVideoDisplay(&d->placeholder, SDL_FALSE);
+        SDL_free(d->placeholder.name);
+        SDL_zero(d->placeholder);
+    }
+}
+
 static void Wayland_init_xdg_output(SDL_VideoData *d)
 {
     SDL_DisplayData *node;
@@ -945,6 +1082,8 @@ int Wayland_VideoInit(SDL_VideoDevice *_this)
     // Second roundtrip to receive all output events.
     WAYLAND_wl_display_roundtrip(data->display);
 
+    Wayland_FinalizeDisplays(data);
+
     Wayland_InitMouse();
 
     WAYLAND_wl_display_flush(data->display);