wayland: Expose displays in a deterministic order, and attempt to better handle selecting a primary display.
Wayland can expose displays in any arbitrary order, and doesn't have the native concept of a primary display. However, there are games that presume that the first listed display is the primary, which can lead to problems if that output isn't necessarily the ideal one, as they may use that display to build a resolution list or as the default fullscreen output. This sorts displays by position, then attempts to find the primary display, first by querying the explicit ordering hint, then the GNOME DBus property, then tries to determine the 'best' display according to the criteria that is generally ideal for games and media playback. The makes the list of displays deterministic, as long as the desktop configuration remains static, with a reasonably appropriate one prioritized as primary, even if there is no explicit way to retrieve the primary display. In the case where a user has a particularly esoteric physical display configuration, the new hint enables explicitly overriding the sorting and selection logic, allowing the entire display order to be customized, if necessary.
This commit is contained in:
parent
075ae7db7d
commit
553fc5fe0e
@ -28,6 +28,12 @@ encounter limitations or behavior that is different from other windowing systems
|
||||
applications _must_ have an event loop and processes messages on a regular basis, or the application can appear
|
||||
unresponsive to both the user and desktop compositor.
|
||||
|
||||
### The display reported as the primary by ```SDL_GetPrimaryDisplay()``` is incorrect
|
||||
|
||||
- Wayland doesn't natively have the concept of a primary display, so SDL attempts to determine it by querying various
|
||||
system settings, and falling back to a selection algorithm if this fails. If it is incorrect, it can be manually
|
||||
overridden by setting the ```SDL_VIDEO_DISPLAY_PRIORITY``` hint.
|
||||
|
||||
### ```SDL_SetWindowPosition()``` doesn't work on non-popup windows
|
||||
|
||||
- Wayland does not allow toplevel windows to position themselves programmatically.
|
||||
|
@ -3131,6 +3131,28 @@ extern "C" {
|
||||
*/
|
||||
#define SDL_HINT_VIDEO_ALLOW_SCREENSAVER "SDL_VIDEO_ALLOW_SCREENSAVER"
|
||||
|
||||
/**
|
||||
* A comma separated list containing the names of the displays that SDL should
|
||||
* sort to the front of the display list.
|
||||
*
|
||||
* When this hint is set, displays with matching name strings will be prioritized in
|
||||
* the list of displays, as exposed by calling SDL_GetDisplays(), with the first listed
|
||||
* becoming the primary display. The naming convention can vary depending on the environment,
|
||||
* but it is usually a connector name (e.g. 'DP-1', 'DP-2', 'HDMI-1', etc...).
|
||||
*
|
||||
* On X11 and Wayland desktops, the connector names associated with displays can typically be
|
||||
* found by using the `xrandr` utility.
|
||||
*
|
||||
* This hint is currently supported on the following drivers:
|
||||
*
|
||||
* - Wayland (wayland)
|
||||
*
|
||||
* This hint should be set before SDL is initialized.
|
||||
*
|
||||
* \since This hint is available since SDL 3.1.5.
|
||||
*/
|
||||
#define SDL_HINT_VIDEO_DISPLAY_PRIORITY "SDL_VIDEO_DISPLAY_PRIORITY"
|
||||
|
||||
/**
|
||||
* Tell the video driver that we only want a double buffer.
|
||||
*
|
||||
|
@ -244,52 +244,210 @@ static const struct kde_output_order_v1_listener kde_output_order_listener = {
|
||||
handle_kde_output_order_done
|
||||
};
|
||||
|
||||
static void Wayland_SortOutputs(SDL_VideoData *vid)
|
||||
// Sort the list of displays into a deterministic order
|
||||
static int SDLCALL Wayland_DisplayPositionCompare(const void *a, const void *b)
|
||||
{
|
||||
SDL_DisplayData *d;
|
||||
int p_x, p_y;
|
||||
const SDL_DisplayData *da = *(SDL_DisplayData **)a;
|
||||
const SDL_DisplayData *db = *(SDL_DisplayData **)b;
|
||||
|
||||
/* KDE provides the kde-output-order-v1 protocol, which gives us the full preferred display
|
||||
* ordering in the form of a list of wl_output.name strings (connector names).
|
||||
*/
|
||||
if (!WAYLAND_wl_list_empty(&vid->output_order)) {
|
||||
struct wl_list sorted_list;
|
||||
SDL_WaylandConnectorName *c;
|
||||
const bool a_at_origin = da->x == 0 && da->y == 0;
|
||||
const bool b_at_origin = db->x == 0 && db->y == 0;
|
||||
|
||||
// Sort the outputs by connector name.
|
||||
WAYLAND_wl_list_init(&sorted_list);
|
||||
wl_list_for_each (c, &vid->output_order, link) {
|
||||
wl_list_for_each (d, &vid->output_list, link) {
|
||||
if (d->wl_output_name && SDL_strcmp(c->wl_output_name, d->wl_output_name) == 0) {
|
||||
// Remove from the current list and Append the next node to the end of the new list.
|
||||
WAYLAND_wl_list_remove(&d->link);
|
||||
WAYLAND_wl_list_insert(sorted_list.prev, &d->link);
|
||||
break;
|
||||
// Sort the display at 0,0 to be beginning of the list, as that will be the fallback primary.
|
||||
if (a_at_origin && !b_at_origin) {
|
||||
return -1;
|
||||
}
|
||||
if (b_at_origin && !a_at_origin) {
|
||||
return 1;
|
||||
}
|
||||
if (da->x < db->x) {
|
||||
return -1;
|
||||
}
|
||||
if (da->x > db->x) {
|
||||
return 1;
|
||||
}
|
||||
if (da->y < db->y) {
|
||||
return -1;
|
||||
}
|
||||
if (da->y > db->y) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If no position information is available, use the connector name.
|
||||
if (da->wl_output_name && db->wl_output_name) {
|
||||
return SDL_strcmp(da->wl_output_name, db->wl_output_name);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Wayland doesn't have the native concept of a primary display, but there are clients that
|
||||
* will base their resolution lists on, or automatically make themselves fullscreen on, the
|
||||
* first listed output, which can lead to problems if the first listed output isn't
|
||||
* necessarily the best display for this. This attempts to find a primary display, first by
|
||||
* querying the GNOME DBus property, then trying to determine the 'best' display if that fails.
|
||||
* If all displays are equal, the one at position 0,0 will become the primary.
|
||||
*
|
||||
* The primary is determined by the following criteria, in order:
|
||||
* - The highest native resolution
|
||||
* - Landscape is preferred over portrait
|
||||
* - TODO: A higher HDR range is preferred
|
||||
* - Higher refresh is preferred (ignoring small differences)
|
||||
* - Lower scale values are preferred (larger display)
|
||||
*/
|
||||
static int Wayland_GetPrimaryDisplay(SDL_VideoData *vid)
|
||||
{
|
||||
static const int REFRESH_DELTA = 4000;
|
||||
|
||||
// Query the DBus interface to see if the coordinates of the primary display are exposed.
|
||||
int x, y;
|
||||
if (Wayland_GetGNOMEPrimaryDisplayCoordinates(&x, &y)) {
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
if (vid->output_list[i]->x == x && vid->output_list[i]->y == y) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, choose the 'best' display.
|
||||
int best_width = 0;
|
||||
int best_height = 0;
|
||||
double best_scale = 0.0;
|
||||
int best_refresh = 0;
|
||||
bool best_is_landscape = false;
|
||||
int best_index = 0;
|
||||
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
const SDL_DisplayData *d = vid->output_list[i];
|
||||
const bool is_landscape = d->orientation != SDL_ORIENTATION_PORTRAIT && d->orientation != SDL_ORIENTATION_PORTRAIT_FLIPPED;
|
||||
bool have_new_best = false;
|
||||
|
||||
if (d->pixel_width > best_width || d->pixel_height > best_height) {
|
||||
have_new_best = true;
|
||||
} else if (d->pixel_width == best_width && d->pixel_height == best_height) {
|
||||
if (!best_is_landscape && is_landscape) { // Favor landscape over portrait displays.
|
||||
have_new_best = true;
|
||||
} else if (!best_is_landscape || is_landscape) { // Ignore portrait displays if a landscape was already found.
|
||||
if (d->refresh - best_refresh > REFRESH_DELTA) { // Favor a higher refresh rate, but ignore small differences (e.g. 59.97 vs 60.1)
|
||||
have_new_best = true;
|
||||
} else if (d->scale_factor < best_scale && SDL_abs(d->refresh - best_refresh) <= REFRESH_DELTA) {
|
||||
// Prefer a lower scale display if the difference in refresh rate is small.
|
||||
have_new_best = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!WAYLAND_wl_list_empty(&vid->output_list)) {
|
||||
if (have_new_best) {
|
||||
best_width = d->pixel_width;
|
||||
best_height = d->pixel_height;
|
||||
best_scale = d->scale_factor;
|
||||
best_refresh = d->refresh;
|
||||
best_is_landscape = is_landscape;
|
||||
best_index = i;
|
||||
}
|
||||
}
|
||||
|
||||
return best_index;
|
||||
}
|
||||
|
||||
static bool Wayland_SortOutputsByPriorityHint(SDL_VideoData *vid)
|
||||
{
|
||||
const char *name_hint = SDL_GetHint(SDL_HINT_VIDEO_DISPLAY_PRIORITY);
|
||||
|
||||
if (name_hint) {
|
||||
char *saveptr;
|
||||
char *str = SDL_strdup(name_hint);
|
||||
SDL_DisplayData **sorted_list = SDL_malloc(sizeof(SDL_DisplayData *) * vid->output_count);
|
||||
int sorted_index = 0;
|
||||
|
||||
if (str && sorted_list) {
|
||||
// Sort the requested displays to the front of the list.
|
||||
const char *token = SDL_strtok_r(str, ",", &saveptr);
|
||||
while (token) {
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
SDL_DisplayData *d = vid->output_list[i];
|
||||
if (d && d->wl_output_name && SDL_strcmp(token, d->wl_output_name) == 0) {
|
||||
sorted_list[sorted_index++] = d;
|
||||
vid->output_list[i] = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
token = SDL_strtok_r(NULL, ",", &saveptr);
|
||||
}
|
||||
|
||||
// Append the remaining outputs to the end of the list.
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
if (vid->output_list[i]) {
|
||||
sorted_list[sorted_index++] = vid->output_list[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the sorted list to the output list.
|
||||
SDL_memcpy(vid->output_list, sorted_list, sizeof(SDL_DisplayData *) * vid->output_count);
|
||||
}
|
||||
|
||||
SDL_free(str);
|
||||
SDL_free(sorted_list);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void Wayland_SortOutputs(SDL_VideoData *vid)
|
||||
{
|
||||
bool have_primary = false;
|
||||
|
||||
/* KDE provides the kde-output-order-v1 protocol, which gives us the full preferred display
|
||||
* ordering in the form of a list of wl_output.name strings.
|
||||
*/
|
||||
if (!WAYLAND_wl_list_empty(&vid->output_order)) {
|
||||
SDL_WaylandConnectorName *c;
|
||||
SDL_DisplayData **sorted_list = SDL_malloc(sizeof(SDL_DisplayData *) * vid->output_count);
|
||||
int sorted_index = 0;
|
||||
|
||||
if (sorted_list) {
|
||||
// Sort the outputs by connector name.
|
||||
wl_list_for_each (c, &vid->output_order, link) {
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
SDL_DisplayData *d = vid->output_list[i];
|
||||
if (d && d->wl_output_name && SDL_strcmp(c->wl_output_name, d->wl_output_name) == 0) {
|
||||
sorted_list[sorted_index++] = d;
|
||||
vid->output_list[i] = NULL;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If any displays were omitted during the sort, append them to the new list.
|
||||
* This shouldn't happen, but better safe than sorry.
|
||||
*/
|
||||
WAYLAND_wl_list_insert_list(sorted_list.prev, &vid->output_list);
|
||||
}
|
||||
|
||||
// Set the output list to the sorted list.
|
||||
WAYLAND_wl_list_init(&vid->output_list);
|
||||
WAYLAND_wl_list_insert_list(&vid->output_list, &sorted_list);
|
||||
} else if (Wayland_GetGNOMEPrimaryDisplayCoordinates(&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.
|
||||
*/
|
||||
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;
|
||||
for (int i = 0; i < vid->output_count; ++i) {
|
||||
if (vid->output_list[i]) {
|
||||
sorted_list[sorted_index++] = vid->output_list[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Copy the sorted list to the output list.
|
||||
SDL_memcpy(vid->output_list, sorted_list, sizeof(SDL_DisplayData *) * vid->output_count);
|
||||
SDL_free(sorted_list);
|
||||
|
||||
have_primary = true;
|
||||
}
|
||||
} else {
|
||||
// Sort by position or connector name, so the order of outputs is deterministic.
|
||||
SDL_qsort(vid->output_list, vid->output_count, sizeof(SDL_DisplayData *), Wayland_DisplayPositionCompare);
|
||||
}
|
||||
|
||||
// Apply the ordering hint if specified, otherwise, try to find the primary display, if no preferred order is known.
|
||||
if (!Wayland_SortOutputsByPriorityHint(vid) && !have_primary) {
|
||||
const int primary_index = Wayland_GetPrimaryDisplay(vid);
|
||||
if (primary_index) {
|
||||
SDL_DisplayData *primary = vid->output_list[primary_index];
|
||||
SDL_memmove(&vid->output_list[1], &vid->output_list[0], sizeof(SDL_DisplayData *) * primary_index);
|
||||
vid->output_list[0] = primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -485,7 +643,6 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
|
||||
data->input = input;
|
||||
data->display_externally_owned = display_is_external;
|
||||
data->scale_to_display_enabled = SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_SCALE_TO_DISPLAY, false);
|
||||
WAYLAND_wl_list_init(&data->output_list);
|
||||
WAYLAND_wl_list_init(&data->output_order);
|
||||
WAYLAND_wl_list_init(&external_window_list);
|
||||
|
||||
@ -1043,8 +1200,12 @@ static bool Wayland_add_display(SDL_VideoData *d, uint32_t id, uint32_t version)
|
||||
wl_output_add_listener(output, &output_listener, data);
|
||||
SDL_WAYLAND_register_output(output);
|
||||
|
||||
// Keep a list of outputs for deferred xdg-output initialization.
|
||||
WAYLAND_wl_list_insert(d->output_list.prev, &data->link);
|
||||
// Keep a list of outputs for sorting and deferred protocol initialization.
|
||||
if (d->output_count == d->output_max) {
|
||||
d->output_max += 4;
|
||||
d->output_list = SDL_realloc(d->output_list, sizeof(SDL_DisplayData *) * d->output_max);
|
||||
}
|
||||
d->output_list[d->output_count++] = data;
|
||||
|
||||
if (data->videodata->xdg_output_manager) {
|
||||
data->xdg_output = zxdg_output_manager_v1_get_xdg_output(data->videodata->xdg_output_manager, output);
|
||||
@ -1077,19 +1238,15 @@ static void Wayland_free_display(SDL_VideoDisplay *display)
|
||||
wl_output_destroy(display_data->output);
|
||||
}
|
||||
|
||||
// Unlink this display.
|
||||
WAYLAND_wl_list_remove(&display_data->link);
|
||||
|
||||
SDL_DelVideoDisplay(display->id, false);
|
||||
}
|
||||
}
|
||||
|
||||
static void Wayland_FinalizeDisplays(SDL_VideoData *vid)
|
||||
{
|
||||
SDL_DisplayData *d;
|
||||
|
||||
Wayland_SortOutputs(vid);
|
||||
wl_list_for_each (d, &vid->output_list, link) {
|
||||
for(int i = 0; i < vid->output_count; ++i) {
|
||||
SDL_DisplayData *d = vid->output_list[i];
|
||||
d->display = SDL_AddVideoDisplay(&d->placeholder, false);
|
||||
SDL_free(d->placeholder.name);
|
||||
SDL_zero(d->placeholder);
|
||||
@ -1098,10 +1255,10 @@ static void Wayland_FinalizeDisplays(SDL_VideoData *vid)
|
||||
|
||||
static void Wayland_init_xdg_output(SDL_VideoData *d)
|
||||
{
|
||||
SDL_DisplayData *node;
|
||||
wl_list_for_each (node, &d->output_list, link) {
|
||||
node->xdg_output = zxdg_output_manager_v1_get_xdg_output(node->videodata->xdg_output_manager, node->output);
|
||||
zxdg_output_v1_add_listener(node->xdg_output, &xdg_output_listener, node);
|
||||
for(int i = 0; i < d->output_count; ++i) {
|
||||
SDL_DisplayData *disp = d->output_list[i];
|
||||
disp->xdg_output = zxdg_output_manager_v1_get_xdg_output(disp->videodata->xdg_output_manager, disp->output);
|
||||
zxdg_output_v1_add_listener(disp->xdg_output, &xdg_output_listener, disp);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1207,12 +1364,18 @@ static void display_handle_global(void *data, struct wl_registry *registry, uint
|
||||
static void display_remove_global(void *data, struct wl_registry *registry, uint32_t id)
|
||||
{
|
||||
SDL_VideoData *d = data;
|
||||
SDL_DisplayData *node;
|
||||
|
||||
// We don't get an interface, just an ID, so assume it's a wl_output :shrug:
|
||||
wl_list_for_each (node, &d->output_list, link) {
|
||||
if (node->registry_id == id) {
|
||||
Wayland_free_display(SDL_GetVideoDisplay(node->display));
|
||||
for (int i = 0; i < d->output_count; ++i) {
|
||||
SDL_DisplayData *disp = d->output_list[i];
|
||||
if (disp->registry_id == id) {
|
||||
Wayland_free_display(SDL_GetVideoDisplay(disp->display));
|
||||
|
||||
if (i < d->output_count) {
|
||||
SDL_memmove(&d->output_list[i], &d->output_list[i + 1], sizeof(SDL_DisplayData *) * (d->output_count - i - 1));
|
||||
}
|
||||
|
||||
d->output_count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1355,6 +1518,7 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
|
||||
SDL_VideoDisplay *display = _this->displays[i];
|
||||
Wayland_free_display(display);
|
||||
}
|
||||
SDL_free(data->output_list);
|
||||
|
||||
Wayland_display_destroy_input(data);
|
||||
|
||||
|
@ -88,7 +88,9 @@ struct SDL_VideoData
|
||||
|
||||
struct xkb_context *xkb_context;
|
||||
struct SDL_WaylandInput *input;
|
||||
struct wl_list output_list;
|
||||
SDL_DisplayData **output_list;
|
||||
int output_count;
|
||||
int output_max;
|
||||
struct wl_list output_order;
|
||||
|
||||
bool output_order_finalized;
|
||||
@ -115,7 +117,6 @@ struct SDL_DisplayData
|
||||
SDL_DisplayID display;
|
||||
SDL_VideoDisplay placeholder;
|
||||
int wl_output_done_count;
|
||||
struct wl_list link;
|
||||
};
|
||||
|
||||
// Needed here to get wl_surface declaration, fixes GitHub#4594
|
||||
|
Loading…
x
Reference in New Issue
Block a user