diff --git a/docs/README-wayland.md b/docs/README-wayland.md index 7b4330a580..93f830422c 100644 --- a/docs/README-wayland.md +++ b/docs/README-wayland.md @@ -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. diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 2e0b196be7..e809b1abd5 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -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. * diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index b57052ebc6..da60ee0259 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -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); diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h index c9c75d1308..8d6c8a8627 100644 --- a/src/video/wayland/SDL_waylandvideo.h +++ b/src/video/wayland/SDL_waylandvideo.h @@ -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