Screen Resolution under GNOME through Mutter's DisplayConfig

On GNOME's Wayland session RetroArch's X11 driver runs on XWayland.
XWayland lists every mode at the desktop's current rate - a 240 Hz
panel shows no 60 Hz mode - and a "switch" only scales the fullscreen
window; the real modes belong to the compositor. The Wayland driver
had no resolution list at all. Under Mutter no X11 or Wayland protocol
reaches the real modes; its org.gnome.Mutter.DisplayConfig D-Bus
interface (what GNOME Settings uses) does.

gfx/common/mutter_displayconfig.c (HAVE_DBUS only) reads
GetCurrentState and applies ApplyMonitorsConfig:

- The list is the target head's real modes, one entry per size and
  rate (a fixed and a variable rate twin are merged), sorted, the
  scanned-out mode current.
- A switch picks the listed mode of that size nearest the rate, the
  same rule as the XRandR path; a zero size keeps the current one (the
  refresh rate autoswitch). It is applied as a temporary configuration:
  not saved to monitors.xml and no "keep these settings?" prompt.
  Every other monitor keeps its mode, position, scale and transform;
  every monitor keeps its colour mode (HDR), RGB range and
  underscanning; a mirror switches all its monitors or none; heads to
  the right of or below the switched one move with its new size; the
  layout mode goes along only where Mutter allows choosing it.
- The target head is the connector (wl_output v4 name), else the head
  under the RetroArch window (XWayland root coordinates are Mutter's
  logical layout), else the monitor index, else the primary.
- Each call opens and closes its own private session bus connection,
  bounded by a 2 s timeout. With no bus address and no
  $XDG_RUNTIME_DIR/bus it does nothing, so no bus is ever autolaunched.

Backwards compatibility - every existing path behaves as before:

- Builds without HAVE_DBUS compile exactly the previous code.
- dispserv_x11 only asks Mutter when the output is XWayland's
  (XWAYLAND<n>); a real X server never talks to D-Bus. On XWayland
  without Mutter (KDE, sway, ...) XWayland's own list and switch are
  used as before. Only when Mutter answers is its answer final.
- dispserv_wl gets set_resolution, get_resolution_list and get_flags
  in HAVE_DBUS builds. A new display server flag,
  DISPSERV_CTX_NO_RESOLUTION_LIST, lets it report "nothing to list
  right now": set when Mutter was not on the bus at init, and
  video_display_server_has_resolution_list() then answers false, so
  the Screen Resolution entry and the refresh rate autoswitch stay off
  on non-GNOME Wayland exactly as before. No existing server sets the
  flag, and its bit is not DISPSERV_CTX_MODELINE's.

Checked against a real headless Mutter 46: GetCurrentState parses and
ApplyMonitorsConfig is accepted.

Regression test samples/gfx/mutter_displayconfig (ASan + UBSan, new
Linux-samples-gfx step): mock_mutter.py carries Mutter's signatures and
its ApplyMonitorsConfig checks (serial, connectors, modes, scales,
every lit monitor configured) on two monitors - a 240/120/60 Hz HDR
panel and an underscanned one beside it. Cases: no session bus and a
bus without Mutter (unavailable, nothing autolaunched); the list, a
60 Hz switch with every other setting carried over, rate only, whole
hertz, a smaller mode with the neighbour moved, another head by
connector, unlisted size and rate refused without an apply; dispserv_x11
on a real Xorg dummy server with Mutter on the bus (XRandR list, Mutter
never asked); dispserv_x11 on Xwayland under a headless Weston with
Mutter (Mutter's list and switch) and without (XWayland's list as
before); dispserv_wl with Mutter (list, switch, not flagged) and
without (flagged, no list, as before).
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-23 17:59:15 +02:00
1 parent 8cee4866b1
commit 17c7c6a393
13 files changed
+2006 -7

No files matched your search

+27
View File
@@ -766,6 +766,33 @@ jobs:
pkill openbox || true
sudo kill "$XORG_PID" || true
- name: Screen Resolution through Mutter's DisplayConfig (ASan + UBSan)
shell: bash
run: |
set -eu
# Under GNOME's Wayland session the X11 driver runs on
# XWayland, which lists every mode at the desktop's current
# rate and only scales the window when "switched", and the
# Wayland driver had no resolution list at all; Mutter's
# org.gnome.Mutter.DisplayConfig D-Bus interface is the only
# way to the monitor's real modes. mock_mutter.py carries
# Mutter's signatures and its ApplyMonitorsConfig checks; the
# test drives gfx/common/mutter_displayconfig.c directly and
# through dispserv_x11 (real Xorg dummy server, and Xwayland
# under a headless Weston) and dispserv_wl, with and without
# Mutter on the bus. Without it every path must behave
# exactly as before: no session bus, a bus without Mutter, a
# real X server (Mutter is never asked), XWayland's own list,
# and no Wayland resolution list.
sudo apt-get install -y -qq libdbus-1-dev dbus python3-dbus \
python3-gi weston xwayland > /dev/null
(cd samples/gfx/mutter_displayconfig \
&& make clean all SANITIZER=address,undefined \
&& ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 \
XORG="sudo Xorg" timeout 300 ./run.sh)
echo "[pass] mutter_displayconfig_test"
- name: D3D11 hardware ring under threaded video (mingw + Wine, headless)
shell: bash
run: |
+1
View File
@@ -1444,6 +1444,7 @@ ifeq ($(HAVE_DBUS), 1)
LIBS += $(DBUS_LIBS)
DEF_FLAGS += $(DBUS_CFLAGS)
OBJ += gfx/common/dbus_common.o
OBJ += gfx/common/mutter_displayconfig.o
endif
ifeq ($(HAVE_UDEV), 1)
+969
View File
@@ -0,0 +1,969 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2026 - The RetroArch team
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dbus/dbus.h>
#include <compat/strl.h>
#include "mutter_displayconfig.h"
#include "../../verbosity.h"
#define MDC_BUS_NAME "org.gnome.Mutter.DisplayConfig"
#define MDC_PATH "/org/gnome/Mutter/DisplayConfig"
#define MDC_IFACE "org.gnome.Mutter.DisplayConfig"
#define MDC_TIMEOUT_MS 2000
/* ApplyMonitorsConfig methods */
#define MDC_METHOD_TEMPORARY 1
/* Layout modes: logical monitors sized by mode / scale, or by mode */
#define MDC_LAYOUT_LOGICAL 1
#define MDC_LAYOUT_PHYSICAL 2
#define MDC_MAX_SCALES 16
#define MDC_MAX_LM_MON 8
typedef struct
{
char id[128];
int w, h;
double refresh;
double scales[MDC_MAX_SCALES];
int nscales;
bool current;
bool interlaced;
} mdc_mode_t;
typedef struct
{
char connector[64];
mdc_mode_t *modes;
int nmodes;
unsigned color_mode;
unsigned rgb_range;
bool underscanning;
bool has_underscanning;
bool has_color_mode;
bool has_rgb_range;
} mdc_monitor_t;
typedef struct
{
int x, y;
double scale;
unsigned transform;
int mon[MDC_MAX_LM_MON]; /* indices into state->mon */
int nmon;
bool primary;
} mdc_logical_t;
typedef struct
{
unsigned serial;
unsigned layout_mode;
mdc_monitor_t *mon;
int nmon;
mdc_logical_t *lm;
int nlm;
bool has_layout_mode;
bool supports_layout_change;
} mdc_state_t;
/* ------------------------------------------------------------------
* Connection
* ------------------------------------------------------------------ */
/* The session bus, never an autolaunched one: with no address in the
* environment and no $XDG_RUNTIME_DIR/bus socket, libdbus would spawn
* a bus of its own, which is nothing Mutter is on. */
static DBusConnection *mdc_connect(void)
{
DBusError err;
DBusConnection *conn;
const char *addr = getenv("DBUS_SESSION_BUS_ADDRESS");
if (!addr || !*addr)
{
char path[512];
const char *rt = getenv("XDG_RUNTIME_DIR");
if (!rt || !*rt)
return NULL;
snprintf(path, sizeof(path), "%s/bus", rt);
if (access(path, F_OK) != 0)
return NULL;
}
dbus_error_init(&err);
conn = dbus_bus_get_private(DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err))
dbus_error_free(&err);
if (conn)
dbus_connection_set_exit_on_disconnect(conn, false);
return conn;
}
static void mdc_disconnect(DBusConnection *conn)
{
if (!conn)
return;
dbus_connection_close(conn);
dbus_connection_unref(conn);
}
static bool mdc_has_owner(DBusConnection *conn)
{
DBusError err;
bool has;
dbus_error_init(&err);
has = dbus_bus_name_has_owner(conn, MDC_BUS_NAME, &err) ? true : false;
if (dbus_error_is_set(&err))
{
dbus_error_free(&err);
return false;
}
return has;
}
bool mutter_displayconfig_available(void)
{
bool has;
DBusConnection *conn = mdc_connect();
if (!conn)
return false;
has = mdc_has_owner(conn);
mdc_disconnect(conn);
return has;
}
/* ------------------------------------------------------------------
* GetCurrentState
* ------------------------------------------------------------------ */
static void mdc_state_free(mdc_state_t *st)
{
int i;
if (!st)
return;
for (i = 0; i < st->nmon; i++)
free(st->mon[i].modes);
free(st->mon);
free(st->lm);
free(st);
}
static int mdc_count(DBusMessageIter *array)
{
int n = 0;
DBusMessageIter it;
dbus_message_iter_recurse(array, &it);
while (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_INVALID)
{
n++;
dbus_message_iter_next(&it);
}
return n;
}
/* One a{sv} entry's variant, if it holds a basic value of type t */
static bool mdc_variant_basic(DBusMessageIter *variant, int t, void *out)
{
DBusMessageIter v;
dbus_message_iter_recurse(variant, &v);
if (dbus_message_iter_get_arg_type(&v) != t)
return false;
dbus_message_iter_get_basic(&v, out);
return true;
}
/* Walks an a{sv}; cb is handed each key and its variant */
typedef void (*mdc_prop_cb)(void *ud, const char *key,
DBusMessageIter *variant);
static bool mdc_props(DBusMessageIter *array, mdc_prop_cb cb, void *ud)
{
DBusMessageIter it;
if (dbus_message_iter_get_arg_type(array) != DBUS_TYPE_ARRAY)
return false;
dbus_message_iter_recurse(array, &it);
while (dbus_message_iter_get_arg_type(&it) == DBUS_TYPE_DICT_ENTRY)
{
DBusMessageIter e;
const char *key = NULL;
dbus_message_iter_recurse(&it, &e);
if (dbus_message_iter_get_arg_type(&e) == DBUS_TYPE_STRING)
{
dbus_message_iter_get_basic(&e, &key);
dbus_message_iter_next(&e);
if (dbus_message_iter_get_arg_type(&e) == DBUS_TYPE_VARIANT)
cb(ud, key, &e);
}
dbus_message_iter_next(&it);
}
return true;
}
static void mdc_mode_prop(void *ud, const char *key, DBusMessageIter *v)
{
mdc_mode_t *m = (mdc_mode_t*)ud;
dbus_bool_t b = FALSE;
if (!strcmp(key, "is-current") && mdc_variant_basic(v, DBUS_TYPE_BOOLEAN, &b))
m->current = b ? true : false;
else if (!strcmp(key, "is-interlaced") && mdc_variant_basic(v, DBUS_TYPE_BOOLEAN, &b))
m->interlaced = b ? true : false;
}
static void mdc_monitor_prop(void *ud, const char *key, DBusMessageIter *v)
{
mdc_monitor_t *mon = (mdc_monitor_t*)ud;
dbus_bool_t b = FALSE;
dbus_uint32_t u = 0;
if (!strcmp(key, "is-underscanning") && mdc_variant_basic(v, DBUS_TYPE_BOOLEAN, &b))
{
mon->underscanning = b ? true : false;
mon->has_underscanning = true;
}
else if (!strcmp(key, "color-mode") && mdc_variant_basic(v, DBUS_TYPE_UINT32, &u))
{
mon->color_mode = u;
mon->has_color_mode = true;
}
else if (!strcmp(key, "rgb-range") && mdc_variant_basic(v, DBUS_TYPE_UINT32, &u))
{
mon->rgb_range = u;
mon->has_rgb_range = true;
}
}
static void mdc_global_prop(void *ud, const char *key, DBusMessageIter *v)
{
mdc_state_t *st = (mdc_state_t*)ud;
dbus_bool_t b = FALSE;
dbus_uint32_t u = 0;
if (!strcmp(key, "layout-mode") && mdc_variant_basic(v, DBUS_TYPE_UINT32, &u))
{
st->layout_mode = u;
st->has_layout_mode = true;
}
else if (!strcmp(key, "supports-changing-layout-mode")
&& mdc_variant_basic(v, DBUS_TYPE_BOOLEAN, &b))
st->supports_layout_change = b ? true : false;
}
/* (ssss): the connector is the first string */
static bool mdc_spec_connector(DBusMessageIter *spec, char *s, size_t len)
{
DBusMessageIter f;
const char *c = NULL;
if (dbus_message_iter_get_arg_type(spec) != DBUS_TYPE_STRUCT)
return false;
dbus_message_iter_recurse(spec, &f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_STRING)
return false;
dbus_message_iter_get_basic(&f, &c);
strlcpy(s, c ? c : "", len);
return true;
}
/* (siiddada{sv}) */
static bool mdc_parse_mode(DBusMessageIter *s, mdc_mode_t *m)
{
DBusMessageIter f, sc;
const char *id = NULL;
dbus_int32_t w = 0, h = 0;
double refresh = 0.0, pref = 0.0;
dbus_message_iter_recurse(s, &f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_STRING)
return false;
dbus_message_iter_get_basic(&f, &id);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_INT32)
return false;
dbus_message_iter_get_basic(&f, &w);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_INT32)
return false;
dbus_message_iter_get_basic(&f, &h);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_DOUBLE)
return false;
dbus_message_iter_get_basic(&f, &refresh);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_DOUBLE)
return false;
dbus_message_iter_get_basic(&f, &pref);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_ARRAY)
return false;
memset(m, 0, sizeof(*m));
strlcpy(m->id, id ? id : "", sizeof(m->id));
m->w = (int)w;
m->h = (int)h;
m->refresh = refresh;
dbus_message_iter_recurse(&f, &sc);
while (dbus_message_iter_get_arg_type(&sc) == DBUS_TYPE_DOUBLE
&& m->nscales < MDC_MAX_SCALES)
{
dbus_message_iter_get_basic(&sc, &m->scales[m->nscales++]);
dbus_message_iter_next(&sc);
}
dbus_message_iter_next(&f);
mdc_props(&f, mdc_mode_prop, m);
return m->id[0] && m->w > 0 && m->h > 0;
}
/* ((ssss)a(siiddada{sv})a{sv}) */
static bool mdc_parse_monitor(DBusMessageIter *s, mdc_monitor_t *mon)
{
DBusMessageIter f, modes;
int n;
memset(mon, 0, sizeof(*mon));
dbus_message_iter_recurse(s, &f);
if (!mdc_spec_connector(&f, mon->connector, sizeof(mon->connector)))
return false;
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_ARRAY)
return false;
if ((n = mdc_count(&f)) > 0
&& !(mon->modes = (mdc_mode_t*)calloc(n, sizeof(*mon->modes))))
return false;
dbus_message_iter_recurse(&f, &modes);
while (dbus_message_iter_get_arg_type(&modes) == DBUS_TYPE_STRUCT
&& mon->nmodes < n)
{
if (mdc_parse_mode(&modes, &mon->modes[mon->nmodes]))
mon->nmodes++;
dbus_message_iter_next(&modes);
}
dbus_message_iter_next(&f);
mdc_props(&f, mdc_monitor_prop, mon);
return true;
}
/* (iiduba(ssss)a{sv}) */
static bool mdc_parse_logical(DBusMessageIter *s, const mdc_state_t *st,
mdc_logical_t *lm)
{
DBusMessageIter f, specs;
dbus_int32_t x = 0, y = 0;
dbus_uint32_t transform = 0;
dbus_bool_t primary = FALSE;
double scale = 1.0;
memset(lm, 0, sizeof(*lm));
dbus_message_iter_recurse(s, &f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_INT32)
return false;
dbus_message_iter_get_basic(&f, &x);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_INT32)
return false;
dbus_message_iter_get_basic(&f, &y);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_DOUBLE)
return false;
dbus_message_iter_get_basic(&f, &scale);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_UINT32)
return false;
dbus_message_iter_get_basic(&f, &transform);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_BOOLEAN)
return false;
dbus_message_iter_get_basic(&f, &primary);
dbus_message_iter_next(&f);
if (dbus_message_iter_get_arg_type(&f) != DBUS_TYPE_ARRAY)
return false;
lm->x = (int)x;
lm->y = (int)y;
lm->scale = scale > 0.0 ? scale : 1.0;
lm->transform = transform;
lm->primary = primary ? true : false;
dbus_message_iter_recurse(&f, &specs);
while (dbus_message_iter_get_arg_type(&specs) == DBUS_TYPE_STRUCT)
{
char c[64];
if (mdc_spec_connector(&specs, c, sizeof(c)))
{
int m;
for (m = 0; m < st->nmon && lm->nmon < MDC_MAX_LM_MON; m++)
if (!strcmp(st->mon[m].connector, c))
{
lm->mon[lm->nmon++] = m;
break;
}
}
dbus_message_iter_next(&specs);
}
return lm->nmon > 0;
}
static mdc_state_t *mdc_get_state(DBusConnection *conn)
{
DBusMessageIter it, arr;
DBusError err;
int n;
dbus_uint32_t serial = 0;
DBusMessage *reply = NULL;
mdc_state_t *st = NULL;
DBusMessage *msg = dbus_message_new_method_call(MDC_BUS_NAME,
MDC_PATH, MDC_IFACE, "GetCurrentState");
if (!msg)
return NULL;
dbus_error_init(&err);
reply = dbus_connection_send_with_reply_and_block(conn, msg,
MDC_TIMEOUT_MS, &err);
dbus_message_unref(msg);
if (dbus_error_is_set(&err))
{
RARCH_WARN("[Mutter] GetCurrentState: %s.\n", err.message);
dbus_error_free(&err);
return NULL;
}
if (!reply)
return NULL;
if ( !dbus_message_iter_init(reply, &it)
|| dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_UINT32
|| !(st = (mdc_state_t*)calloc(1, sizeof(*st))))
goto fail;
dbus_message_iter_get_basic(&it, &serial);
st->serial = serial;
st->layout_mode = MDC_LAYOUT_LOGICAL;
/* monitors */
dbus_message_iter_next(&it);
if (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_ARRAY)
goto fail;
if ((n = mdc_count(&it)) > 0
&& !(st->mon = (mdc_monitor_t*)calloc(n, sizeof(*st->mon))))
goto fail;
dbus_message_iter_recurse(&it, &arr);
while (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_STRUCT
&& st->nmon < n)
{
if (mdc_parse_monitor(&arr, &st->mon[st->nmon]))
st->nmon++;
else
free(st->mon[st->nmon].modes);
dbus_message_iter_next(&arr);
}
/* logical monitors */
dbus_message_iter_next(&it);
if (dbus_message_iter_get_arg_type(&it) != DBUS_TYPE_ARRAY)
goto fail;
if ((n = mdc_count(&it)) > 0
&& !(st->lm = (mdc_logical_t*)calloc(n, sizeof(*st->lm))))
goto fail;
dbus_message_iter_recurse(&it, &arr);
while (dbus_message_iter_get_arg_type(&arr) == DBUS_TYPE_STRUCT
&& st->nlm < n)
{
if (mdc_parse_logical(&arr, st, &st->lm[st->nlm]))
st->nlm++;
dbus_message_iter_next(&arr);
}
/* properties */
dbus_message_iter_next(&it);
mdc_props(&it, mdc_global_prop, st);
dbus_message_unref(reply);
if (!st->nlm)
{
mdc_state_free(st);
return NULL;
}
return st;
fail:
RARCH_WARN("[Mutter] GetCurrentState reply not understood.\n");
dbus_message_unref(reply);
mdc_state_free(st);
return NULL;
}
/* ------------------------------------------------------------------
* Head selection and geometry
* ------------------------------------------------------------------ */
static const mdc_mode_t *mdc_current_mode(const mdc_monitor_t *mon)
{
int i;
for (i = 0; i < mon->nmodes; i++)
if (mon->modes[i].current)
return &mon->modes[i];
return NULL;
}
static int mdc_round(double v)
{
return (int)(v + 0.5);
}
/* A logical monitor's extent in the layout on the given mode */
static void mdc_logical_size(const mdc_state_t *st, const mdc_logical_t *lm,
const mdc_mode_t *mode, double scale, int *w, int *h)
{
int mw = mode ? mode->w : 0;
int mh = mode ? mode->h : 0;
if (lm->transform & 1)
{
int t = mw;
mw = mh;
mh = t;
}
if (st->layout_mode != MDC_LAYOUT_PHYSICAL && scale > 0.0)
{
mw = mdc_round(mw / scale);
mh = mdc_round(mh / scale);
}
*w = mw;
*h = mh;
}
static int mdc_pick_logical(const mdc_state_t *st,
const mutter_dc_target_t *t)
{
int i, j;
if (t && t->connector && *t->connector)
for (i = 0; i < st->nlm; i++)
for (j = 0; j < st->lm[i].nmon; j++)
if (!strcmp(st->mon[st->lm[i].mon[j]].connector, t->connector))
return i;
if (t && t->have_point)
for (i = 0; i < st->nlm; i++)
{
int w, h;
const mdc_logical_t *lm = &st->lm[i];
mdc_logical_size(st, lm, mdc_current_mode(&st->mon[lm->mon[0]]),
lm->scale, &w, &h);
if ( t->x >= lm->x && t->x < lm->x + w
&& t->y >= lm->y && t->y < lm->y + h)
return i;
}
if (t && t->monitor_index > 0 && t->monitor_index <= st->nlm)
return t->monitor_index - 1;
for (i = 0; i < st->nlm; i++)
if (st->lm[i].primary)
return i;
return 0;
}
/* The listed mode of that size nearest the rate: within half a hertz
* or matching the whole-hertz label, progressive first, the current
* one on a tie */
static const mdc_mode_t *mdc_find_mode(const mdc_monitor_t *mon,
int w, int h, int int_hz, double hz)
{
int i;
double best_diff = 0.0;
const mdc_mode_t *best = NULL;
for (i = 0; i < mon->nmodes; i++)
{
double diff;
const mdc_mode_t *m = &mon->modes[i];
if (m->w != w || m->h != h)
continue;
diff = m->refresh - hz;
if (diff < 0.0)
diff = -diff;
if (diff >= 0.5 && !(int_hz > 0 && (int)(m->refresh + 0.001) == int_hz))
continue;
if (m->interlaced)
diff += 1000.0;
if (!best || diff < best_diff || (diff == best_diff && m->current))
{
best = m;
best_diff = diff;
}
}
return best;
}
static bool mdc_mode_has_scale(const mdc_mode_t *m, double scale)
{
int i;
if (!m->nscales)
return true;
for (i = 0; i < m->nscales; i++)
if (m->scales[i] - scale < 0.0001 && scale - m->scales[i] < 0.0001)
return true;
return false;
}
/* The supported scale nearest the one in use */
static double mdc_nearest_scale(const mdc_mode_t *m, double scale)
{
int i;
double best = scale, best_diff = -1.0;
for (i = 0; i < m->nscales; i++)
{
double d = m->scales[i] - scale;
if (d < 0.0)
d = -d;
if (best_diff < 0.0 || d < best_diff)
{
best = m->scales[i];
best_diff = d;
}
}
return best;
}
/* ------------------------------------------------------------------
* Resolution list
* ------------------------------------------------------------------ */
static int mdc_list_qsort(const void *pa, const void *pb)
{
const video_display_config_t *a = (const video_display_config_t*)pa;
const video_display_config_t *b = (const video_display_config_t*)pb;
if (a->dims != b->dims)
return a->dims < b->dims ? -1 : 1;
if (a->interlaced != b->interlaced)
return a->interlaced ? 1 : -1;
if (a->refreshrate_float != b->refreshrate_float)
return a->refreshrate_float < b->refreshrate_float ? -1 : 1;
return 0;
}
enum mutter_dc_result mutter_displayconfig_get_resolution_list(
const mutter_dc_target_t *target,
video_display_config_t **list, unsigned *len)
{
int i;
unsigned j, n = 0;
const mdc_monitor_t *mon = NULL;
video_display_config_t *conf = NULL;
mdc_state_t *st = NULL;
DBusConnection *conn = mdc_connect();
*list = NULL;
*len = 0;
if (!conn)
return MUTTER_DC_UNAVAILABLE;
if (!mdc_has_owner(conn) || !(st = mdc_get_state(conn)))
{
mdc_disconnect(conn);
return MUTTER_DC_UNAVAILABLE;
}
mdc_disconnect(conn);
mon = &st->mon[st->lm[mdc_pick_logical(st, target)].mon[0]];
if (mon->nmodes > 0
&& (conf = (video_display_config_t*)calloc(mon->nmodes, sizeof(*conf))))
{
for (i = 0; i < mon->nmodes; i++)
{
video_display_config_t e;
const mdc_mode_t *m = &mon->modes[i];
bool dup = false;
memset(&e, 0, sizeof(e));
e.dims = VIDEO_SCALE_PACK(m->w, m->h);
e.bpp = 32;
e.refreshrate_float = (float)m->refresh;
e.refreshrate = (unsigned)(m->refresh + 0.001);
e.interlaced = m->interlaced;
e.current = m->current;
/* The same size and rate twice (a fixed and a variable rate
* mode, say) is one choice in the menu */
for (j = 0; j < n; j++)
{
float d = conf[j].refreshrate_float - e.refreshrate_float;
if ( conf[j].dims == e.dims
&& conf[j].interlaced == e.interlaced
&& d < 0.005f && d > -0.005f)
{
conf[j].current = conf[j].current || e.current;
dup = true;
break;
}
}
if (!dup)
conf[n++] = e;
}
qsort(conf, n, sizeof(*conf), mdc_list_qsort);
for (j = 0; j < n; j++)
conf[j].idx = j;
}
mdc_state_free(st);
if (!n)
{
free(conf);
return MUTTER_DC_FAILED;
}
*list = conf;
*len = n;
return MUTTER_DC_OK;
}
/* ------------------------------------------------------------------
* ApplyMonitorsConfig
* ------------------------------------------------------------------ */
static bool mdc_append_dict_bool(DBusMessageIter *dict, const char *key,
bool value)
{
DBusMessageIter e, v;
dbus_bool_t b = value ? TRUE : FALSE;
return dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &e)
&& dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &key)
&& dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "b", &v)
&& dbus_message_iter_append_basic(&v, DBUS_TYPE_BOOLEAN, &b)
&& dbus_message_iter_close_container(&e, &v)
&& dbus_message_iter_close_container(dict, &e);
}
static bool mdc_append_dict_u32(DBusMessageIter *dict, const char *key,
unsigned value)
{
DBusMessageIter e, v;
dbus_uint32_t u = value;
return dbus_message_iter_open_container(dict, DBUS_TYPE_DICT_ENTRY, NULL, &e)
&& dbus_message_iter_append_basic(&e, DBUS_TYPE_STRING, &key)
&& dbus_message_iter_open_container(&e, DBUS_TYPE_VARIANT, "u", &v)
&& dbus_message_iter_append_basic(&v, DBUS_TYPE_UINT32, &u)
&& dbus_message_iter_close_container(&e, &v)
&& dbus_message_iter_close_container(dict, &e);
}
/* (ssa{sv}): connector, mode id, and the monitor's own settings as it
* reported them, so colour mode, RGB range and underscanning carry
* over instead of falling back to their defaults */
static bool mdc_append_monitor(DBusMessageIter *arr, const mdc_monitor_t *mon,
const char *mode_id)
{
DBusMessageIter s, props;
const char *c = mon->connector;
if ( !dbus_message_iter_open_container(arr, DBUS_TYPE_STRUCT, NULL, &s)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_STRING, &c)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_STRING, &mode_id)
|| !dbus_message_iter_open_container(&s, DBUS_TYPE_ARRAY, "{sv}", &props))
return false;
if (mon->has_underscanning
&& !mdc_append_dict_bool(&props, "enable_underscanning", mon->underscanning))
return false;
if (mon->has_color_mode
&& !mdc_append_dict_u32(&props, "color-mode", mon->color_mode))
return false;
if (mon->has_rgb_range
&& !mdc_append_dict_u32(&props, "rgb-range", mon->rgb_range))
return false;
return dbus_message_iter_close_container(&s, &props)
&& dbus_message_iter_close_container(arr, &s);
}
enum mutter_dc_result mutter_displayconfig_set_resolution(
const mutter_dc_target_t *target,
unsigned dims, int int_hz, float hz)
{
int i, j, t;
int want_w, want_h, old_w = 0, old_h = 0, new_w = 0, new_h = 0;
int *x = NULL, *y = NULL;
double new_scale;
double want_hz = hz;
dbus_uint32_t serial, method = MDC_METHOD_TEMPORARY;
enum mutter_dc_result result = MUTTER_DC_FAILED;
const mdc_mode_t **chosen = NULL;
const mdc_mode_t *cur, *best;
const mdc_monitor_t *mon;
mdc_logical_t *lm;
DBusMessageIter it, lms, props;
DBusMessage *msg = NULL;
DBusMessage *reply = NULL;
mdc_state_t *st = NULL;
DBusConnection *conn = mdc_connect();
DBusError err;
dbus_error_init(&err);
if (!conn)
return MUTTER_DC_UNAVAILABLE;
if (!mdc_has_owner(conn) || !(st = mdc_get_state(conn)))
{
mdc_disconnect(conn);
return MUTTER_DC_UNAVAILABLE;
}
t = mdc_pick_logical(st, target);
lm = &st->lm[t];
mon = &st->mon[lm->mon[0]];
cur = mdc_current_mode(mon);
want_w = (int)VIDEO_SCALE_W(dims);
want_h = (int)VIDEO_SCALE_H(dims);
if (!want_w)
want_w = cur ? cur->w : 0;
if (!want_h)
want_h = cur ? cur->h : 0;
if (want_hz <= 0.0 && int_hz > 0)
want_hz = int_hz;
if (want_hz <= 0.0 && cur)
want_hz = cur->refresh;
if (!(best = mdc_find_mode(mon, want_w, want_h, int_hz, want_hz)))
{
RARCH_WARN("[Mutter] No listed mode %dx%d at %.3f Hz on %s.\n",
want_w, want_h, want_hz, mon->connector);
goto end;
}
if (cur && best == cur && lm->nmon == 1)
{
result = MUTTER_DC_OK;
goto end;
}
/* Mode per monitor of the head: a mirror takes the same size and
* rate on each of its monitors, or nothing */
if (!(chosen = (const mdc_mode_t**)calloc(st->nmon, sizeof(*chosen)))
|| !(x = (int*)calloc(st->nlm, sizeof(*x)))
|| !(y = (int*)calloc(st->nlm, sizeof(*y))))
goto end;
for (i = 0; i < st->nmon; i++)
chosen[i] = mdc_current_mode(&st->mon[i]);
chosen[lm->mon[0]] = best;
for (j = 1; j < lm->nmon; j++)
{
const mdc_mode_t *mm = mdc_find_mode(&st->mon[lm->mon[j]],
best->w, best->h, (int)(best->refresh + 0.001), best->refresh);
if (!mm)
{
RARCH_WARN("[Mutter] Mirror %s has no %dx%d at %.3f Hz.\n",
st->mon[lm->mon[j]].connector, best->w, best->h, best->refresh);
goto end;
}
chosen[lm->mon[j]] = mm;
}
for (i = 0; i < st->nmon; i++)
{
bool lit = false;
int k;
for (j = 0; j < st->nlm && !lit; j++)
for (k = 0; k < st->lm[j].nmon; k++)
if (st->lm[j].mon[k] == i)
lit = true;
if (lit && !chosen[i])
goto end;
}
/* Keep the scale unless the new mode cannot take it */
new_scale = lm->scale;
if (!mdc_mode_has_scale(best, new_scale))
new_scale = mdc_nearest_scale(best, new_scale);
/* Heads to the right of and below this one move with its new
* extent, so the layout stays adjacent without overlaps */
mdc_logical_size(st, lm, cur, lm->scale, &old_w, &old_h);
mdc_logical_size(st, lm, best, new_scale, &new_w, &new_h);
for (i = 0; i < st->nlm; i++)
{
x[i] = st->lm[i].x;
y[i] = st->lm[i].y;
if (i == t)
continue;
if (st->lm[i].x >= lm->x + old_w)
x[i] += new_w - old_w;
if (st->lm[i].y >= lm->y + old_h)
y[i] += new_h - old_h;
}
if (!(msg = dbus_message_new_method_call(MDC_BUS_NAME, MDC_PATH,
MDC_IFACE, "ApplyMonitorsConfig")))
goto end;
serial = st->serial;
dbus_message_iter_init_append(msg, &it);
if ( !dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &serial)
|| !dbus_message_iter_append_basic(&it, DBUS_TYPE_UINT32, &method)
|| !dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY,
"(iiduba(ssa{sv}))", &lms))
goto end;
for (i = 0; i < st->nlm; i++)
{
DBusMessageIter s, mons;
const mdc_logical_t *l = &st->lm[i];
dbus_int32_t lx = x[i];
dbus_int32_t ly = y[i];
double sc = (i == t) ? new_scale : l->scale;
dbus_uint32_t tr = l->transform;
dbus_bool_t pr = l->primary ? TRUE : FALSE;
if ( !dbus_message_iter_open_container(&lms, DBUS_TYPE_STRUCT, NULL, &s)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_INT32, &lx)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_INT32, &ly)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_DOUBLE, &sc)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_UINT32, &tr)
|| !dbus_message_iter_append_basic(&s, DBUS_TYPE_BOOLEAN, &pr)
|| !dbus_message_iter_open_container(&s, DBUS_TYPE_ARRAY,
"(ssa{sv})", &mons))
goto end;
for (j = 0; j < l->nmon; j++)
if (!mdc_append_monitor(&mons, &st->mon[l->mon[j]],
chosen[l->mon[j]]->id))
goto end;
if ( !dbus_message_iter_close_container(&s, &mons)
|| !dbus_message_iter_close_container(&lms, &s))
goto end;
}
if ( !dbus_message_iter_close_container(&it, &lms)
|| !dbus_message_iter_open_container(&it, DBUS_TYPE_ARRAY, "{sv}", &props))
goto end;
/* The layout mode goes along only where Mutter lets it be chosen;
* elsewhere naming it is an error even at its current value */
if (st->has_layout_mode && st->supports_layout_change
&& !mdc_append_dict_u32(&props, "layout-mode", st->layout_mode))
goto end;
if (!dbus_message_iter_close_container(&it, &props))
goto end;
reply = dbus_connection_send_with_reply_and_block(conn, msg,
MDC_TIMEOUT_MS, &err);
if (dbus_error_is_set(&err))
{
RARCH_ERR("[Mutter] Switching %s to %dx%d %.3f Hz failed: %s.\n",
mon->connector, best->w, best->h, best->refresh, err.message);
goto end;
}
RARCH_LOG("[Mutter] %s switched to %dx%d %.3f Hz.\n",
mon->connector, best->w, best->h, best->refresh);
result = MUTTER_DC_OK;
end:
if (dbus_error_is_set(&err))
dbus_error_free(&err);
if (reply)
dbus_message_unref(reply);
if (msg)
dbus_message_unref(msg);
free(chosen);
free(x);
free(y);
mdc_state_free(st);
mdc_disconnect(conn);
return result;
}
+81
View File
@@ -0,0 +1,81 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2010-2026 - The RetroArch team
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
/* GNOME's display modes through Mutter's org.gnome.Mutter.DisplayConfig
* D-Bus interface - the one GNOME Settings uses. Under GNOME neither
* XWayland's RandR (every mode at the desktop's current rate, and a
* "switch" that only scales the window) nor any Wayland protocol can
* list or change the monitor's real modes; this can.
*
* Only built with HAVE_DBUS. Every call opens its own private session
* bus connection and closes it again, so nothing here outlives a call,
* and each call is bounded by a D-Bus timeout. */
#ifndef __MUTTER_DISPLAYCONFIG_H
#define __MUTTER_DISPLAYCONFIG_H
#include <boolean.h>
#include <retro_common_api.h>
#include "../video_display_server.h"
RETRO_BEGIN_DECLS
/* Which head a call is for, most specific first: a connector name
* ("DP-1", from wl_output v4), a point in the logical layout (the
* centre of the RetroArch window on XWayland), a 1-based logical
* monitor index. Anything unset or unmatched falls through to the
* primary logical monitor. */
typedef struct mutter_dc_target
{
const char *connector;
int x, y;
int monitor_index;
bool have_point;
} mutter_dc_target_t;
enum mutter_dc_result
{
/* Mutter is not on the session bus, or no session bus: the caller
* does what it did before */
MUTTER_DC_UNAVAILABLE = -1,
/* Mutter answered and refused (no such mode, invalid layout) */
MUTTER_DC_FAILED = 0,
MUTTER_DC_OK = 1
};
/* true when org.gnome.Mutter.DisplayConfig has an owner on the
* session bus. Never autolaunches a bus. */
bool mutter_displayconfig_available(void);
/* The modes of the target head, as the menu's resolution list: sorted
* by size then rate, one entry per size and rate, the scanned-out one
* current. *list is malloc'd; the caller frees it. */
enum mutter_dc_result mutter_displayconfig_get_resolution_list(
const mutter_dc_target_t *target,
video_display_config_t **list, unsigned *len);
/* Switch the target head to the listed mode of that size nearest the
* rate; a zero width or height keeps the current one. Applied as a
* temporary configuration: not saved, and no "keep these settings?"
* prompt. Every other monitor keeps its mode, position, scale,
* transform and colour settings. */
enum mutter_dc_result mutter_displayconfig_set_resolution(
const mutter_dc_target_t *target,
unsigned dims, int int_hz, float hz);
RETRO_END_DECLS
#endif
+83
View File
@@ -18,6 +18,7 @@
#include <string.h>
#include <boolean.h>
#include <compat/strl.h>
#include <retro_miscellaneous.h>
#include <wayland-client.h>
@@ -27,6 +28,10 @@
#include "../../verbosity.h"
#ifdef HAVE_DBUS
#include "../common/mutter_displayconfig.h"
#endif
typedef struct
{
struct wl_display *dpy;
@@ -43,6 +48,11 @@ typedef struct
* sysfs node the EDID lives under; Wayland itself has no
* protocol for the EDID */
char name[32];
#ifdef HAVE_DBUS
/* GNOME: modes listed and switched through Mutter's D-Bus
* interface; no Wayland protocol does this under Mutter */
bool mutter;
#endif
} dispserv_wl_t;
/* wl_output listener callbacks */
@@ -157,6 +167,10 @@ static void *wl_display_server_init(void)
wl_display_roundtrip(serv->dpy);
wayland_drm_lease_report(serv->dpy);
#ifdef HAVE_DBUS
serv->mutter = mutter_displayconfig_available();
#endif
return serv;
}
@@ -174,6 +188,66 @@ static void wl_display_server_destroy(void *data)
free(serv);
}
#ifdef HAVE_DBUS
/* The head this client's wl_output names (wl_output v4), else
* Mutter's primary */
static void wl_display_server_mutter_target(dispserv_wl_t *serv,
int monitor_index, mutter_dc_target_t *t)
{
memset(t, 0, sizeof(*t));
t->connector = serv->name[0] ? serv->name : NULL;
t->monitor_index = monitor_index;
}
static void *wl_display_server_get_resolution_list(void *data,
unsigned *len)
{
mutter_dc_target_t t;
video_display_config_t *list = NULL;
dispserv_wl_t *serv = (dispserv_wl_t*)data;
*len = 0;
if (!serv || !serv->mutter)
return NULL;
wl_display_server_mutter_target(serv, 0, &t);
if (mutter_displayconfig_get_resolution_list(&t, &list, len)
!= MUTTER_DC_OK)
return NULL;
return list;
}
static bool wl_display_server_set_resolution(void *data,
unsigned dims, int int_hz, float hz, int center,
int monitor_index, int xoffset, int padjust)
{
mutter_dc_target_t t;
dispserv_wl_t *serv = (dispserv_wl_t*)data;
if (!serv || !serv->mutter)
return false;
wl_display_server_mutter_target(serv, monitor_index, &t);
if (mutter_displayconfig_set_resolution(&t, dims, int_hz, hz)
!= MUTTER_DC_OK)
return false;
/* Mutter has applied it by the time it replies; the wl_output
* mode events for the new mode are on the socket */
if (serv->dpy)
wl_display_roundtrip(serv->dpy);
return true;
}
/* Without Mutter there is nothing to list, and the menu entry and the
* refresh rate autoswitch stay off as they always were */
static uint32_t wl_display_server_get_flags(void *data)
{
uint32_t flags = 0;
dispserv_wl_t *serv = (dispserv_wl_t*)data;
if (!serv || !serv->mutter)
BIT32_SET(flags, DISPSERV_CTX_NO_RESOLUTION_LIST);
return flags;
}
#endif
static float wl_display_server_get_refresh_rate(void *data)
{
dispserv_wl_t *serv = (dispserv_wl_t*)data;
@@ -267,8 +341,13 @@ const video_display_server_t dispserv_wl = {
NULL, /* set_window_opacity */
NULL, /* set_window_progress */
NULL, /* set_window_decorations */
#ifdef HAVE_DBUS
wl_display_server_set_resolution,
wl_display_server_get_resolution_list,
#else
NULL, /* set_resolution */
NULL, /* get_resolution_list */
#endif
NULL, /* get_output_options */
NULL, /* set_screen_orientation */
NULL, /* get_screen_orientation */
@@ -277,7 +356,11 @@ const video_display_server_t dispserv_wl = {
NULL, /* get_video_output_prev */
NULL, /* get_video_output_next */
wl_display_server_get_metrics,
#ifdef HAVE_DBUS
wl_display_server_get_flags,
#else
NULL, /* get_flags */
#endif
NULL, /* get_scanline */
NULL, /* wait_vblank */
NULL, /* modeline_list_outputs */
+67 -4
View File
@@ -46,6 +46,9 @@
#include "../../retroarch.h"
#include "../../verbosity.h"
#include "edid_sysfs.h"
#if defined(HAVE_XRANDR) && defined(HAVE_DBUS)
#include "../common/mutter_displayconfig.h"
#endif
enum dispserv_x11_flags
{
@@ -1438,6 +1441,30 @@ static int x11_res_list_qsort(const void *pa, const void *pb)
return 0;
}
#ifdef HAVE_DBUS
/* XWayland lists every mode at the desktop's current rate and only
* scales a fullscreen window when "switched"; the real modes belong to
* the compositor. Under GNOME, Mutter's D-Bus interface has them. Its
* outputs are all named XWAYLAND<n>; a real X server never is, so on
* one nothing below talks to D-Bus at all. */
static bool x11_res_is_xwayland(const XRROutputInfo *oi)
{
return oi && oi->name && !strncmp(oi->name, "XWAYLAND", 8);
}
/* The Mutter head to ask about: the one under the RetroArch window
* (XWayland's root coordinates are Mutter's logical layout), else the
* monitor index, else the primary */
static void x11_res_mutter_target(Display *dpy, int monitor_index,
mutter_dc_target_t *t)
{
memset(t, 0, sizeof(*t));
t->monitor_index = monitor_index;
t->have_point = x11_ml_target_point(dpy,
monitor_index > 0 ? monitor_index - 1 : -1, &t->x, &t->y);
}
#endif
static void *x11_display_server_get_resolution_list(void *data,
unsigned *len)
{
@@ -1457,10 +1484,25 @@ static void *x11_display_server_get_resolution_list(void *data,
if ( (res = XRRGetScreenResourcesCurrent(dpy,
RootWindow(dpy, DefaultScreen(dpy))))
&& x11_res_pick_output(dpy, res,
RootWindow(dpy, DefaultScreen(dpy)), 0, &oi, &ci)
&& oi->nmode > 0
&& (conf = (video_display_config_t*)
calloc(oi->nmode, sizeof(*conf))))
RootWindow(dpy, DefaultScreen(dpy)), 0, &oi, &ci))
{
#ifdef HAVE_DBUS
if (x11_res_is_xwayland(oi))
{
mutter_dc_target_t t;
x11_res_mutter_target(dpy, 0, &t);
/* Mutter's answer stands, list or none; without Mutter the
* XWayland list below is what there is */
if (mutter_displayconfig_get_resolution_list(&t, &conf, &n)
!= MUTTER_DC_UNAVAILABLE)
goto done;
}
#endif
if (oi->nmode > 0)
conf = (video_display_config_t*)calloc(oi->nmode, sizeof(*conf));
}
if (conf)
{
for (i = 0; i < oi->nmode; i++)
{
@@ -1504,6 +1546,9 @@ static void *x11_display_server_get_resolution_list(void *data,
conf[j].idx = j;
}
#ifdef HAVE_DBUS
done:
#endif
if (ci)
XRRFreeCrtcInfo(ci);
if (oi)
@@ -1561,6 +1606,24 @@ static bool x11_display_server_set_resolution(void *data,
|| !x11_res_pick_output(dpy, res, root, monitor_index, &oi, &ci))
goto end;
#ifdef HAVE_DBUS
if (x11_res_is_xwayland(oi))
{
mutter_dc_target_t t;
enum mutter_dc_result r;
x11_res_mutter_target(dpy, monitor_index, &t);
r = mutter_displayconfig_set_resolution(&t, dims, int_hz, hz);
if (r != MUTTER_DC_UNAVAILABLE)
{
ok = (r == MUTTER_DC_OK);
/* XWayland follows the compositor a moment later; the kept
* rate must not outlive the mode it was read from */
x11_refresh_invalidate();
goto end;
}
}
#endif
cur_mi = x11_res_find_mode(res, ci->mode);
want_w = VIDEO_SCALE_W(dims);
want_h = VIDEO_SCALE_H(dims);
+5 -1
View File
@@ -34,7 +34,11 @@ enum display_server_flags
DISPSERV_CTX_FLAGS_NONE = 0,
/* The server can apply a video_modeline_t through its modeline_*
* ops (the bit the CRT consumer and the menu look for). */
DISPSERV_CTX_MODELINE
DISPSERV_CTX_MODELINE,
/* The server has resolution list callbacks but nothing to list them
* from right now (the Wayland server away from GNOME): the menu and
* the refresh rate autoswitch treat it as having none. */
DISPSERV_CTX_NO_RESOLUTION_LIST
};
/* One-cycle alias for the bit's previous name. */
+10 -2
View File
@@ -1858,8 +1858,16 @@ int video_display_server_get_edid(uint8_t *out, size_t max)
bool video_display_server_has_resolution_list(void)
{
return (current_display_server
&& current_display_server->get_resolution_list);
video_driver_state_t *video_st = &video_driver_st;
if ( !current_display_server
|| !current_display_server->get_resolution_list)
return false;
if ( current_display_server->get_flags
&& BIT32_GET(current_display_server->get_flags(
video_st->current_display_server_data),
DISPSERV_CTX_NO_RESOLUTION_LIST))
return false;
return true;
}
void *video_display_server_get_resolution_list(unsigned *size)
+1
View File
@@ -388,6 +388,7 @@ VIDEO CONTEXT
#ifdef HAVE_DBUS
#include "../gfx/common/dbus_common.c"
#include "../gfx/common/mutter_displayconfig.c"
#endif
#ifndef HAVE_OPENGLES
+48
View File
@@ -0,0 +1,48 @@
TARGET := mutter_displayconfig_test
# Links the real Mutter DisplayConfig client and the real X11 and
# Wayland display servers against libdbus, libX11/libXrandr and
# libwayland-client. run.sh supplies the session bus, the mock Mutter
# and the X/Wayland servers each case needs.
REPO_ROOT := ../../..
LRC := $(REPO_ROOT)/libretro-common
SOURCES := mutter_displayconfig_test.c \
$(REPO_ROOT)/gfx/common/mutter_displayconfig.c \
$(REPO_ROOT)/gfx/display_servers/dispserv_x11.c \
$(REPO_ROOT)/gfx/display_servers/dispserv_wl.c \
$(REPO_ROOT)/gfx/common/xinerama_common.c \
$(LRC)/compat/compat_strl.c
CFLAGS += -Wall -std=gnu99 -g -O0 \
-DHAVE_DBUS -DHAVE_XRANDR -DHAVE_XINERAMA -DHAVE_WAYLAND \
-DTEST_X11 -DTEST_WL \
-I$(REPO_ROOT) -I$(LRC)/include \
$(shell pkg-config --cflags dbus-1 wayland-client)
LDLIBS += $(shell pkg-config --libs dbus-1 wayland-client) \
-lXrandr -lXinerama -lX11 -lm
# Objects under obj/, never next to the sources: other samples build
# some of the same files with other flags.
OBJS := $(addprefix obj/,$(notdir $(SOURCES:.c=.o)))
VPATH := $(sort $(dir $(SOURCES)))
ifneq ($(SANITIZER),)
CFLAGS := -fsanitize=$(SANITIZER) -fno-omit-frame-pointer $(CFLAGS)
LDFLAGS := -fsanitize=$(SANITIZER) $(LDFLAGS)
endif
all: $(TARGET)
obj/%.o: %.c
@mkdir -p obj
$(CC) -c -o $@ $< $(CFLAGS)
$(TARGET): $(OBJS)
$(CC) -o $@ $^ $(LDFLAGS) $(LDLIBS)
clean:
rm -rf $(TARGET) obj
.PHONY: clean
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
# A stand-in for GNOME's org.gnome.Mutter.DisplayConfig, with the
# signatures of Mutter's own interface XML and the checks
# ApplyMonitorsConfig makes before it touches a monitor: the serial
# must be the current one, the method known, every connector known,
# every mode one of that connector's, every scale one of that mode's,
# and every lit monitor configured. A config that passes becomes the
# current state (serial bumped), so a second call sees the first.
#
# Two monitors side by side: DP-1, primary, a 2560x1440 panel at
# 240/120/60 Hz (plus 1920x1080) in HDR colour mode, and HDMI-1 to its
# right with underscanning on. DP-1 lists its 240 Hz mode twice, fixed
# and variable rate, the way Mutter 47+ does.
#
# Test-only methods on the same interface: LastApply (what the last
# accepted ApplyMonitorsConfig asked for, as one line), Calls (how many
# GetCurrentState / ApplyMonitorsConfig calls came in) and Reset.
import sys
import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib
NAME = "org.gnome.Mutter.DisplayConfig"
PATH = "/org/gnome/Mutter/DisplayConfig"
def mode(mid, w, h, hz, scales, current=False):
return {"id": mid, "w": w, "h": h, "hz": hz, "scales": scales,
"current": current}
def initial():
return {
"serial": 7,
"monitors": [
{"connector": "DP-1", "props": {"color-mode": dbus.UInt32(1),
"supported-color-modes":
dbus.Array([dbus.UInt32(0), dbus.UInt32(1)], signature="u")},
"modes": [
mode("[email protected]", 2560, 1440, 239.913, [1.0, 1.25, 1.5, 2.0], True),
mode("[email protected]+vrr", 2560, 1440, 239.913, [1.0, 1.25, 1.5, 2.0]),
mode("[email protected]", 2560, 1440, 119.877, [1.0, 1.25, 1.5, 2.0]),
mode("[email protected]", 2560, 1440, 59.951, [1.0, 1.25, 1.5, 2.0]),
mode("[email protected]", 1920, 1080, 60.0, [1.0, 1.25]),
]},
{"connector": "HDMI-1", "props": {"is-underscanning": True},
"modes": [
mode("[email protected]", 1920, 1080, 60.0, [1.0], True),
mode("[email protected]", 1280, 720, 60.0, [1.0]),
]},
],
# x, y, scale, transform, primary, connectors
"logical": [
[0, 0, 1.0, 0, True, ["DP-1"]],
[2560, 0, 1.0, 0, False, ["HDMI-1"]],
],
}
class DisplayConfig(dbus.service.Object):
def __init__(self, bus):
super().__init__(bus, PATH)
self.st = initial()
self.last = ""
self.n_get = 0
self.n_apply = 0
@dbus.service.method(NAME, in_signature="",
out_signature="ua((ssss)a(siiddada{sv})a{sv})a(iiduba(ssss)a{sv})a{sv}")
def GetCurrentState(self):
self.n_get += 1
mons = []
for m in self.st["monitors"]:
modes = []
for md in m["modes"]:
props = {"is-current": dbus.Boolean(md["current"])}
if md["current"]:
props["is-preferred"] = dbus.Boolean(True)
modes.append((md["id"], dbus.Int32(md["w"]), dbus.Int32(md["h"]),
dbus.Double(md["hz"]), dbus.Double(1.0),
dbus.Array([dbus.Double(s) for s in md["scales"]], signature="d"),
dbus.Dictionary(props, signature="sv")))
spec = (m["connector"], "VND", "Panel", "0001")
mprops = dict(m["props"])
mprops["display-name"] = m["connector"]
mons.append((spec, dbus.Array(modes, signature="(siiddada{sv})"),
dbus.Dictionary(mprops, signature="sv")))
lms = []
for x, y, sc, tr, pr, cons in self.st["logical"]:
specs = [(c, "VND", "Panel", "0001") for c in cons]
lms.append((dbus.Int32(x), dbus.Int32(y), dbus.Double(sc), dbus.UInt32(tr),
dbus.Boolean(pr), dbus.Array(specs, signature="(ssss)"),
dbus.Dictionary({}, signature="sv")))
props = {"layout-mode": dbus.UInt32(1),
"supports-changing-layout-mode": dbus.Boolean(True)}
return (dbus.UInt32(self.st["serial"]),
dbus.Array(mons, signature="((ssss)a(siiddada{sv})a{sv})"),
dbus.Array(lms, signature="(iiduba(ssss)a{sv})"),
dbus.Dictionary(props, signature="sv"))
@dbus.service.method(NAME, in_signature="uua(iiduba(ssa{sv}))a{sv}",
out_signature="")
def ApplyMonitorsConfig(self, serial, method, logical, props):
self.n_apply += 1
err = "org.freedesktop.DBus.Error.AccessDenied"
if serial != self.st["serial"]:
raise dbus.exceptions.DBusException("The requested configuration is based on stale information", name=err)
if method not in (0, 1, 2):
raise dbus.exceptions.DBusException("Invalid method", name="org.freedesktop.DBus.Error.InvalidArgs")
mons = {m["connector"]: m for m in self.st["monitors"]}
seen = set()
new_logical = []
chosen = {}
parts = []
for x, y, sc, tr, pr, monspecs in logical:
cons = []
mparts = []
for con, mid, mprops in monspecs:
if con not in mons:
raise dbus.exceptions.DBusException("Invalid connector " + con, name="org.freedesktop.DBus.Error.InvalidArgs")
md = [md for md in mons[con]["modes"] if md["id"] == mid]
if not md:
raise dbus.exceptions.DBusException("Invalid mode " + mid, name="org.freedesktop.DBus.Error.InvalidArgs")
if not any(abs(s - sc) < 0.0001 for s in md[0]["scales"]):
raise dbus.exceptions.DBusException("Scale not supported", name="org.freedesktop.DBus.Error.InvalidArgs")
seen.add(con)
cons.append(con)
chosen[con] = mid
mparts.append("%s=%s{%s}" % (con, mid, ",".join(
"%s:%s" % (k, (str(bool(v)).lower() if isinstance(v, dbus.Boolean) else int(v)))
for k, v in sorted(mprops.items()))))
new_logical.append([int(x), int(y), float(sc), int(tr), bool(pr), cons])
parts.append("%d,%d,%.2f,%d,%s[%s]" % (x, y, sc, tr, "p" if pr else "-", ";".join(mparts)))
for m in self.st["monitors"]:
if m["connector"] not in seen:
raise dbus.exceptions.DBusException("Monitor " + m["connector"] + " not configured", name="org.freedesktop.DBus.Error.InvalidArgs")
gp = ",".join("%s:%d" % (k, int(v)) for k, v in sorted(props.items()))
self.last = "method=%d serial=%d lm=%s props={%s}" % (method, serial, " ".join(parts), gp)
if method == 0:
return
for con, mid in chosen.items():
for md in mons[con]["modes"]:
md["current"] = (md["id"] == mid)
self.st["logical"] = new_logical
self.st["serial"] += 1
@dbus.service.method(NAME, in_signature="", out_signature="s")
def LastApply(self):
return self.last
@dbus.service.method(NAME, in_signature="", out_signature="uu")
def Calls(self):
return (dbus.UInt32(self.n_get), dbus.UInt32(self.n_apply))
@dbus.service.method(NAME, in_signature="", out_signature="")
def Reset(self):
self.st = initial()
self.last = ""
self.n_get = 0
self.n_apply = 0
def main():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
obj = DisplayConfig(bus)
# The BusName object owns the name; dropped, it releases it
name = dbus.service.BusName(NAME, bus)
print("mock mutter ready", flush=True)
GLib.MainLoop().run()
del obj, name
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,458 @@
/* Copyright (C) 2010-2026 The RetroArch team
*
* ---------------------------------------------------------------------------------------
* The following license statement only applies to this file (mutter_displayconfig_test.c).
* ---------------------------------------------------------------------------------------
*
* Permission is hereby granted, free of charge,
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* Screen Resolution under GNOME, through Mutter's DisplayConfig D-Bus
* interface, and proof that nothing changes anywhere else.
*
* On GNOME's Wayland session RetroArch's X11 driver runs on XWayland,
* which lists every mode at the desktop's current rate (a 240 Hz panel
* shows no 60 Hz mode) and "switches" by scaling the window; the
* Wayland driver had no resolution list at all. Mutter's D-Bus
* interface is the only way to the real modes, and gfx/common/
* mutter_displayconfig.c now uses it from both drivers.
*
* Run by run.sh, which starts a private session bus and, per case,
* mock_mutter.py (Mutter's signatures and its ApplyMonitorsConfig
* checks), a real Xorg dummy server, or Xwayland under a headless
* Weston. One case per invocation:
*
* nobus no session bus: everything unavailable, nothing
* autolaunched
* nomutter a bus without Mutter: unavailable
* mutter the list, switches (rate, size with the neighbour
* moved, rate only, whole hertz, another head by
* connector), refusals, and what went on the wire
* x11-xorg dispserv_x11 on a real X server with Mutter on the
* bus: the XRandR list, and Mutter never asked
* x11-xwayland dispserv_x11 on XWayland with Mutter: Mutter's list
* and Mutter's switch
* x11-xwayland-nomutter XWayland without Mutter: XWayland's list,
* exactly as before
* wl dispserv_wl with Mutter: the list, the switch, and
* the resolution list not flagged away
* wl-nomutter dispserv_wl without Mutter: flagged away, as before
*/
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dbus/dbus.h>
#include <retro_miscellaneous.h>
#include "../../../gfx/video_display_server.h"
#include "../../../gfx/common/mutter_displayconfig.h"
#ifdef TEST_X11
#include <X11/Xlib.h>
Display *g_x11_dpy = NULL;
Window g_x11_win = 0;
int g_x11_screen = 0;
extern const video_display_server_t dispserv_x11;
#endif
#ifdef TEST_WL
struct wl_display;
void wayland_drm_lease_report(struct wl_display *dpy) { (void)dpy; }
extern const video_display_server_t dispserv_wl;
#endif
static int s_verbose;
static void vlog(const char *fmt, va_list ap)
{
if (s_verbose)
vfprintf(stderr, fmt, ap);
}
void RARCH_DBG(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(fmt, ap); va_end(ap); }
void RARCH_LOG(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(fmt, ap); va_end(ap); }
void RARCH_WARN(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(fmt, ap); va_end(ap); }
void RARCH_ERR(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vlog(fmt, ap); va_end(ap); }
void video_monitor_set_refresh_rate(float hz) { (void)hz; }
/* ------------------------------------------------------------------
* The mock's test-only methods
* ------------------------------------------------------------------ */
static DBusMessage *mock_call(const char *method)
{
DBusError err;
DBusMessage *msg, *reply;
DBusConnection *c;
dbus_error_init(&err);
if (!(c = dbus_bus_get_private(DBUS_BUS_SESSION, &err)))
{
dbus_error_free(&err);
return NULL;
}
msg = dbus_message_new_method_call("org.gnome.Mutter.DisplayConfig",
"/org/gnome/Mutter/DisplayConfig", "org.gnome.Mutter.DisplayConfig",
method);
reply = dbus_connection_send_with_reply_and_block(c, msg, 2000, &err);
dbus_message_unref(msg);
if (dbus_error_is_set(&err))
dbus_error_free(&err);
dbus_connection_close(c);
dbus_connection_unref(c);
return reply;
}
static void mock_last(char *s, size_t len)
{
const char *v = "";
DBusMessage *r = mock_call("LastApply");
if (r)
dbus_message_get_args(r, NULL, DBUS_TYPE_STRING, &v, DBUS_TYPE_INVALID);
snprintf(s, len, "%s", v);
if (r)
dbus_message_unref(r);
}
static void mock_calls(unsigned *get, unsigned *apply)
{
dbus_uint32_t g = 0, a = 0;
DBusMessage *r = mock_call("Calls");
if (r)
{
dbus_message_get_args(r, NULL, DBUS_TYPE_UINT32, &g,
DBUS_TYPE_UINT32, &a, DBUS_TYPE_INVALID);
dbus_message_unref(r);
}
*get = g;
*apply = a;
}
static void mock_reset(void)
{
DBusMessage *r = mock_call("Reset");
if (r)
dbus_message_unref(r);
}
/* ------------------------------------------------------------------
* Checks
* ------------------------------------------------------------------ */
#define FAIL(...) do { fprintf(stderr, "FAIL: " __VA_ARGS__); fputc('\n', stderr); return 1; } while (0)
static int expect_last(const char *want, const char *what)
{
char got[2048];
mock_last(got, sizeof(got));
if (strcmp(got, want))
FAIL("%s: Mutter was asked\n %s\nwanted\n %s", what, got, want);
printf("[pass] %s\n", what);
return 0;
}
static int find(const video_display_config_t *l, unsigned n,
unsigned w, unsigned h, float hz)
{
unsigned i;
for (i = 0; i < n; i++)
{
float d = l[i].refreshrate_float - hz;
if (VIDEO_SCALE_W(l[i].dims) == w && VIDEO_SCALE_H(l[i].dims) == h
&& d < 0.01f && d > -0.01f)
return (int)i;
}
return -1;
}
/* DP-1's modes, as the mock lists them: the 240 Hz mode twice (fixed
* and variable rate) must come out once */
static int check_dp1_list(const video_display_config_t *l, unsigned n,
float current_hz, const char *what)
{
unsigned i;
int c;
if (n != 4)
FAIL("%s: %u entries, want 4", what, n);
if (find(l, n, 1920, 1080, 60.0f) != 0
|| find(l, n, 2560, 1440, 59.951f) != 1
|| find(l, n, 2560, 1440, 119.877f) != 2
|| find(l, n, 2560, 1440, 239.913f) != 3)
FAIL("%s: entries missing or out of order", what);
c = find(l, n, 2560, 1440, current_hz);
for (i = 0; i < n; i++)
if (l[i].current != ((int)i == c) || l[i].idx != i
|| l[i].refreshrate != (unsigned)(l[i].refreshrate_float + 0.001f))
FAIL("%s: entry %u mislabelled (current %d idx %u rate %u)", what, i,
l[i].current, l[i].idx, l[i].refreshrate);
printf("[pass] %s: 4 modes, 60/120/240 Hz, current %.3f Hz\n", what, current_hz);
return 0;
}
static int case_unavailable(const char *what)
{
video_display_config_t *l = (video_display_config_t*)1;
unsigned n = 99;
if (mutter_displayconfig_available())
FAIL("%s: reported available", what);
if (mutter_displayconfig_get_resolution_list(NULL, &l, &n) != MUTTER_DC_UNAVAILABLE
|| l || n)
FAIL("%s: list not unavailable", what);
if (mutter_displayconfig_set_resolution(NULL, 0, 60, 60.0f) != MUTTER_DC_UNAVAILABLE)
FAIL("%s: set not unavailable", what);
printf("[pass] %s: unavailable, caller keeps its old behaviour\n", what);
return 0;
}
#define LAYOUT "props={layout-mode:1}"
static int case_mutter(void)
{
mutter_dc_target_t hdmi;
video_display_config_t *l = NULL;
unsigned n = 0, g, a;
if (!mutter_displayconfig_available())
FAIL("mock Mutter not seen on the bus");
if (mutter_displayconfig_get_resolution_list(NULL, &l, &n) != MUTTER_DC_OK)
FAIL("list failed");
if (check_dp1_list(l, n, 239.913f, "primary head list"))
return 1;
free(l);
/* 240 -> 60 Hz: HDR colour mode kept on DP-1, underscanning kept on
* HDMI-1, HDMI-1 where it was, temporary, current serial */
if (mutter_displayconfig_set_resolution(NULL,
VIDEO_SCALE_PACK(2560, 1440), 59, 59.951f) != MUTTER_DC_OK)
FAIL("switch to 60 Hz refused");
if (expect_last("method=1 serial=7 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 2560,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"60 Hz: one mode changed, every other setting carried over"))
return 1;
if (mutter_displayconfig_get_resolution_list(NULL, &l, &n) != MUTTER_DC_OK
|| check_dp1_list(l, n, 59.951f, "list after the switch"))
return 1;
free(l);
/* Rate only (size 0), as the refresh rate autoswitch asks */
if (mutter_displayconfig_set_resolution(NULL, 0, 119, 119.877f) != MUTTER_DC_OK)
FAIL("rate-only switch refused");
if (expect_last("method=1 serial=8 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 2560,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"rate only: the size stays"))
return 1;
/* Whole hertz from the menu label alone */
if (mutter_displayconfig_set_resolution(NULL,
VIDEO_SCALE_PACK(2560, 1440), 239, 0.0f) != MUTTER_DC_OK)
FAIL("whole-hertz switch refused");
if (expect_last("method=1 serial=9 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 2560,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"whole hertz: 239 finds 239.913, not the variable-rate twin"))
return 1;
/* Smaller: the head to the right moves in to stay adjacent */
if (mutter_displayconfig_set_resolution(NULL,
VIDEO_SCALE_PACK(1920, 1080), 60, 60.0f) != MUTTER_DC_OK)
FAIL("switch to 1920x1080 refused");
if (expect_last("method=1 serial=10 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 1920,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"smaller mode: the neighbour follows"))
return 1;
/* The other head, by connector: its own list and switch */
memset(&hdmi, 0, sizeof(hdmi));
hdmi.connector = "HDMI-1";
if (mutter_displayconfig_get_resolution_list(&hdmi, &l, &n) != MUTTER_DC_OK
|| n != 2 || find(l, n, 1280, 720, 60.0f) != 0 || !l[1].current)
FAIL("HDMI-1 list wrong (%u entries)", n);
free(l);
if (mutter_displayconfig_set_resolution(&hdmi,
VIDEO_SCALE_PACK(1280, 720), 60, 60.0f) != MUTTER_DC_OK)
FAIL("HDMI-1 switch refused");
if (expect_last("method=1 serial=11 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 1920,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"head chosen by connector"))
return 1;
/* Refusals: a size no head lists, and a rate that one does not */
mock_calls(&g, &a);
if (mutter_displayconfig_set_resolution(NULL,
VIDEO_SCALE_PACK(1234, 567), 60, 60.0f) != MUTTER_DC_FAILED)
FAIL("an unlisted size was not refused");
if (mutter_displayconfig_set_resolution(NULL,
VIDEO_SCALE_PACK(1920, 1080), 144, 144.0f) != MUTTER_DC_FAILED)
FAIL("an unlisted rate was not refused");
{
unsigned g2, a2;
mock_calls(&g2, &a2);
if (a2 != a)
FAIL("a refused switch still sent ApplyMonitorsConfig");
}
printf("[pass] unlisted size and rate refused without asking Mutter\n");
mock_reset();
return 0;
}
#ifdef TEST_X11
static int x11_open(void)
{
if (!(g_x11_dpy = XOpenDisplay(NULL)))
FAIL("no X display");
return 0;
}
static int case_x11_xorg(void)
{
video_display_config_t *l;
unsigned n = 0, g, a;
void *data;
if (x11_open())
return 1;
data = dispserv_x11.init();
l = (video_display_config_t*)dispserv_x11.get_resolution_list(data, &n);
if (!l || n < 10)
FAIL("real X server: %u entries", n);
free(l);
dispserv_x11.destroy(data);
mock_calls(&g, &a);
if (g || a)
FAIL("real X server: Mutter was asked (%u state, %u apply)", g, a);
printf("[pass] real X server: %u XRandR modes, Mutter never asked\n", n);
return 0;
}
static int case_x11_xwayland(bool mutter)
{
video_display_config_t *l;
unsigned n = 0;
void *data;
if (x11_open())
return 1;
data = dispserv_x11.init();
l = (video_display_config_t*)dispserv_x11.get_resolution_list(data, &n);
if (mutter)
{
if (!l || check_dp1_list(l, n, 239.913f, "XWayland + Mutter list"))
return 1;
free(l);
if (!dispserv_x11.set_resolution(data, VIDEO_SCALE_PACK(2560, 1440),
59, 59.951f, 0, 0, 0, 0))
FAIL("XWayland + Mutter switch refused");
if (expect_last("method=1 serial=7 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 2560,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"XWayland + Mutter switch goes to Mutter"))
return 1;
}
else
{
/* XWayland's own list, as before: its emulated sizes, each a
* CVT timing at the one rate the compositor runs */
unsigned i;
float lo, hi;
if (!l || n < 2)
FAIL("XWayland without Mutter: %u entries", n);
lo = hi = l[0].refreshrate_float;
for (i = 1; i < n; i++)
{
if (l[i].refreshrate_float < lo)
lo = l[i].refreshrate_float;
if (l[i].refreshrate_float > hi)
hi = l[i].refreshrate_float;
}
if (hi - lo > 2.0f)
FAIL("XWayland without Mutter: %.3f..%.3f Hz, not XWayland's list", lo, hi);
free(l);
printf("[pass] XWayland without Mutter: XWayland's %u modes, as before\n", n);
}
dispserv_x11.destroy(data);
return 0;
}
#endif
#ifdef TEST_WL
static int case_wl(bool mutter)
{
video_display_config_t *l;
unsigned n = 0;
uint32_t flags;
void *data = dispserv_wl.init();
if (!data)
FAIL("no Wayland display");
if (!dispserv_wl.get_resolution_list || !dispserv_wl.set_resolution
|| !dispserv_wl.get_flags)
FAIL("dispserv_wl has no resolution callbacks in a HAVE_DBUS build");
flags = dispserv_wl.get_flags(data);
l = (video_display_config_t*)dispserv_wl.get_resolution_list(data, &n);
if (mutter)
{
if (BIT32_GET(flags, DISPSERV_CTX_NO_RESOLUTION_LIST))
FAIL("Wayland + Mutter: resolution list flagged away");
if (!l || check_dp1_list(l, n, 239.913f, "Wayland + Mutter list"))
return 1;
free(l);
if (!dispserv_wl.set_resolution(data, VIDEO_SCALE_PACK(2560, 1440),
119, 119.877f, 0, 0, 0, 0))
FAIL("Wayland + Mutter switch refused");
if (expect_last("method=1 serial=7 lm=0,0,1.00,0,p[[email protected]{color-mode:1}]"
" 2560,0,1.00,0,-[[email protected]{enable_underscanning:true}] " LAYOUT,
"Wayland + Mutter switch"))
return 1;
}
else
{
/* What video_display_server_has_resolution_list() reads: the
* menu entry and the autoswitch stay off, as before */
if (!BIT32_GET(flags, DISPSERV_CTX_NO_RESOLUTION_LIST) || l || n
|| dispserv_wl.set_resolution(data, 0, 60, 60.0f, 0, 0, 0, 0))
FAIL("Wayland without Mutter: resolution list offered");
printf("[pass] Wayland without Mutter: no resolution list, as before\n");
}
dispserv_wl.destroy(data);
return 0;
}
#endif
int main(int argc, char **argv)
{
const char *c = argc > 1 ? argv[1] : "";
s_verbose = getenv("MDC_VERBOSE") != NULL;
if (!strcmp(c, "nobus"))
return case_unavailable("no session bus");
if (!strcmp(c, "nomutter"))
return case_unavailable("session bus without Mutter");
if (!strcmp(c, "mutter"))
return case_mutter();
#ifdef TEST_X11
if (!strcmp(c, "x11-xorg"))
return case_x11_xorg();
if (!strcmp(c, "x11-xwayland"))
return case_x11_xwayland(true);
if (!strcmp(c, "x11-xwayland-nomutter"))
return case_x11_xwayland(false);
#endif
#ifdef TEST_WL
if (!strcmp(c, "wl"))
return case_wl(true);
if (!strcmp(c, "wl-nomutter"))
return case_wl(false);
#endif
fprintf(stderr, "unknown case '%s'\n", c);
return 2;
}
+84
View File
@@ -0,0 +1,84 @@
#!/bin/sh
# Every case of mutter_displayconfig_test, each with exactly the servers
# it needs. Needs dbus-daemon, python3-dbus + python3-gi, Xorg with the
# dummy driver, weston and Xwayland. Run from this directory after
# make; exits non-zero on the first failure. XORG="sudo Xorg" where
# Xorg cannot run as the user (the GitHub runners).
set -eu
HERE=$(pwd)
T=$HERE/mutter_displayconfig_test
WORK=$(mktemp -d)
PIDS=""
cleanup()
{
for p in $PIDS; do kill "$p" 2>/dev/null || sudo -n kill "$p" 2>/dev/null || true; done
rm -rf "$WORK"
}
trap cleanup EXIT INT TERM
start_mock()
{
python3 "$HERE/mock_mutter.py" > "$WORK/mock.log" 2>&1 &
MOCK=$!
PIDS="$PIDS $MOCK"
for i in $(seq 1 50); do
grep -q ready "$WORK/mock.log" 2>/dev/null && return 0
sleep 0.1
done
cat "$WORK/mock.log"; exit 1
}
stop_mock()
{
kill "$MOCK" 2>/dev/null || true
wait "$MOCK" 2>/dev/null || true
}
# No session bus at all: nothing must be autolaunched
env -u DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR="$WORK/nort" "$T" nobus
# A private session bus for everything else
export XDG_RUNTIME_DIR="$WORK/rt"
mkdir -p -m 700 "$XDG_RUNTIME_DIR"
dbus-daemon --session --fork --print-address=3 --print-pid=4 \
3> "$WORK/bus.addr" 4> "$WORK/bus.pid"
DBUS_SESSION_BUS_ADDRESS=$(cat "$WORK/bus.addr")
export DBUS_SESSION_BUS_ADDRESS
PIDS="$PIDS $(cat "$WORK/bus.pid")"
"$T" nomutter
start_mock
"$T" mutter
stop_mock
# A real X server with Mutter on the bus: Mutter must not be asked
${XORG:-Xorg} :91 -config "$HERE/../display_servers_x11_live/xorg-dummy.conf" \
-logfile "$WORK/xorg.log" -noreset -nolisten tcp -ac > "$WORK/xorg.out" 2>&1 &
PIDS="$PIDS $!"
for i in $(seq 1 60); do DISPLAY=:91 xrandr > /dev/null 2>&1 && break; sleep 0.25; done
start_mock
DISPLAY=:91 "$T" x11-xorg
stop_mock
# Headless Weston with Xwayland: GNOME's shape without GNOME
weston --backend=headless --socket=mdc-wl --idle-time=0 \
> "$WORK/weston.log" 2>&1 &
PIDS="$PIDS $!"
for i in $(seq 1 60); do [ -S "$XDG_RUNTIME_DIR/mdc-wl" ] && break; sleep 0.25; done
WAYLAND_DISPLAY=mdc-wl Xwayland :92 -noreset > "$WORK/xwayland.log" 2>&1 &
PIDS="$PIDS $!"
for i in $(seq 1 60); do DISPLAY=:92 xrandr > /dev/null 2>&1 && break; sleep 0.25; done
DISPLAY=:92 "$T" x11-xwayland-nomutter
WAYLAND_DISPLAY=mdc-wl "$T" wl-nomutter
start_mock
DISPLAY=:92 "$T" x11-xwayland
stop_mock
start_mock
WAYLAND_DISPLAY=mdc-wl "$T" wl
stop_mock
echo "ALL OK"