1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#ifdef SDL_VIDEO_DRIVER_WAYLAND
25
26#include "../../core/linux/SDL_system_theme.h"
27#include "../../events/SDL_events_c.h"
28
29#include "SDL_waylandclipboard.h"
30#include "SDL_waylandcolor.h"
31#include "SDL_waylandevents_c.h"
32#include "SDL_waylandkeyboard.h"
33#include "SDL_waylandmessagebox.h"
34#include "SDL_waylandmouse.h"
35#include "SDL_waylandopengles.h"
36#include "SDL_waylandvideo.h"
37#include "SDL_waylandvulkan.h"
38#include "SDL_waylandwindow.h"
39
40#include <fcntl.h>
41#include <sys/types.h>
42#include <unistd.h>
43#include <xkbcommon/xkbcommon.h>
44
45#include <wayland-util.h>
46
47#include "alpha-modifier-v1-client-protocol.h"
48#include "cursor-shape-v1-client-protocol.h"
49#include "fractional-scale-v1-client-protocol.h"
50#include "frog-color-management-v1-client-protocol.h"
51#include "idle-inhibit-unstable-v1-client-protocol.h"
52#include "input-timestamps-unstable-v1-client-protocol.h"
53#include "keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
54#include "pointer-constraints-unstable-v1-client-protocol.h"
55#include "primary-selection-unstable-v1-client-protocol.h"
56#include "relative-pointer-unstable-v1-client-protocol.h"
57#include "tablet-v2-client-protocol.h"
58#include "text-input-unstable-v3-client-protocol.h"
59#include "viewporter-client-protocol.h"
60#include "xdg-activation-v1-client-protocol.h"
61#include "xdg-decoration-unstable-v1-client-protocol.h"
62#include "xdg-dialog-v1-client-protocol.h"
63#include "xdg-foreign-unstable-v2-client-protocol.h"
64#include "xdg-output-unstable-v1-client-protocol.h"
65#include "xdg-shell-client-protocol.h"
66#include "xdg-toplevel-icon-v1-client-protocol.h"
67#include "color-management-v1-client-protocol.h"
68
69#ifdef HAVE_LIBDECOR_H
70#include <libdecor.h>
71#endif
72
73#define WAYLANDVID_DRIVER_NAME "wayland"
74
75// Clamp certain core protocol versions on older versions of libwayland.
76#if SDL_WAYLAND_CHECK_VERSION(1, 22, 0)
77#define SDL_WL_COMPOSITOR_VERSION 6
78#else
79#define SDL_WL_COMPOSITOR_VERSION 4
80#endif
81
82#if SDL_WAYLAND_CHECK_VERSION(1, 22, 0)
83#define SDL_WL_SEAT_VERSION 9
84#elif SDL_WAYLAND_CHECK_VERSION(1, 21, 0)
85#define SDL_WL_SEAT_VERSION 8
86#else
87#define SDL_WL_SEAT_VERSION 5
88#endif
89
90#if SDL_WAYLAND_CHECK_VERSION(1, 20, 0)
91#define SDL_WL_OUTPUT_VERSION 4
92#else
93#define SDL_WL_OUTPUT_VERSION 3
94#endif
95
96#ifdef SDL_USE_LIBDBUS
97#include "../../core/linux/SDL_dbus.h"
98
99#define DISPLAY_INFO_NODE "org.gnome.Mutter.DisplayConfig"
100#define DISPLAY_INFO_PATH "/org/gnome/Mutter/DisplayConfig"
101#define DISPLAY_INFO_METHOD "GetCurrentState"
102#endif
103
104/* GNOME doesn't expose displays in any particular order, but we can find the
105 * primary display and its logical coordinates via a DBus method.
106 */
107static bool Wayland_GetGNOMEPrimaryDisplayCoordinates(int *x, int *y)
108{
109#ifdef SDL_USE_LIBDBUS
110 SDL_DBusContext *dbus = SDL_DBus_GetContext();
111 if (dbus == NULL) {
112 return false;
113 }
114 DBusMessage *reply = NULL;
115 DBusMessageIter iter[3];
116 DBusMessage *msg = dbus->message_new_method_call(DISPLAY_INFO_NODE,
117 DISPLAY_INFO_PATH,
118 DISPLAY_INFO_NODE,
119 DISPLAY_INFO_METHOD);
120
121 if (msg) {
122 reply = dbus->connection_send_with_reply_and_block(dbus->session_conn, msg, DBUS_TIMEOUT_USE_DEFAULT, NULL);
123 dbus->message_unref(msg);
124 }
125
126 if (reply) {
127 // Serial (don't care)
128 dbus->message_iter_init(reply, &iter[0]);
129 if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_UINT32) {
130 goto error;
131 }
132
133 // Physical monitor array (don't care)
134 dbus->message_iter_next(&iter[0]);
135 if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_ARRAY) {
136 goto error;
137 }
138
139 // Logical monitor array of structs
140 dbus->message_iter_next(&iter[0]);
141 if (dbus->message_iter_get_arg_type(&iter[0]) != DBUS_TYPE_ARRAY) {
142 goto error;
143 }
144
145 // First logical monitor struct
146 dbus->message_iter_recurse(&iter[0], &iter[1]);
147 if (dbus->message_iter_get_arg_type(&iter[1]) != DBUS_TYPE_STRUCT) {
148 goto error;
149 }
150
151 do {
152 int logical_x, logical_y;
153 dbus_bool_t primary;
154
155 // Logical X
156 dbus->message_iter_recurse(&iter[1], &iter[2]);
157 if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_INT32) {
158 goto error;
159 }
160 dbus->message_iter_get_basic(&iter[2], &logical_x);
161
162 // Logical Y
163 dbus->message_iter_next(&iter[2]);
164 if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_INT32) {
165 goto error;
166 }
167 dbus->message_iter_get_basic(&iter[2], &logical_y);
168
169 // Scale (don't care)
170 dbus->message_iter_next(&iter[2]);
171 if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_DOUBLE) {
172 goto error;
173 }
174
175 // Transform (don't care)
176 dbus->message_iter_next(&iter[2]);
177 if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_UINT32) {
178 goto error;
179 }
180
181 // Primary display boolean
182 dbus->message_iter_next(&iter[2]);
183 if (dbus->message_iter_get_arg_type(&iter[2]) != DBUS_TYPE_BOOLEAN) {
184 goto error;
185 }
186 dbus->message_iter_get_basic(&iter[2], &primary);
187
188 if (primary) {
189 *x = logical_x;
190 *y = logical_y;
191
192 // We found the primary display: success.
193 dbus->message_unref(reply);
194 return true;
195 }
196 } while (dbus->message_iter_next(&iter[1]));
197 }
198
199error:
200 if (reply) {
201 dbus->message_unref(reply);
202 }
203#endif
204 return false;
205}
206
207// Sort the list of displays into a deterministic order
208static int SDLCALL Wayland_DisplayPositionCompare(const void *a, const void *b)
209{
210 const SDL_DisplayData *da = *(SDL_DisplayData **)a;
211 const SDL_DisplayData *db = *(SDL_DisplayData **)b;
212
213 const bool a_at_origin = da->x == 0 && da->y == 0;
214 const bool b_at_origin = db->x == 0 && db->y == 0;
215
216 // Sort the display at 0,0 to be beginning of the list, as that will be the fallback primary.
217 if (a_at_origin && !b_at_origin) {
218 return -1;
219 }
220 if (b_at_origin && !a_at_origin) {
221 return 1;
222 }
223 if (da->x < db->x) {
224 return -1;
225 }
226 if (da->x > db->x) {
227 return 1;
228 }
229 if (da->y < db->y) {
230 return -1;
231 }
232 if (da->y > db->y) {
233 return 1;
234 }
235
236 // If no position information is available, use the connector name.
237 if (da->wl_output_name && db->wl_output_name) {
238 return SDL_strcmp(da->wl_output_name, db->wl_output_name);
239 }
240
241 return 0;
242}
243
244/* Wayland doesn't have the native concept of a primary display, but there are clients that
245 * will base their resolution lists on, or automatically make themselves fullscreen on, the
246 * first listed output, which can lead to problems if the first listed output isn't
247 * necessarily the best display for this. This attempts to find a primary display, first by
248 * querying the GNOME DBus property, then trying to determine the 'best' display if that fails.
249 * If all displays are equal, the one at position 0,0 will become the primary.
250 *
251 * The primary is determined by the following criteria, in order:
252 * - Landscape is preferred over portrait
253 * - The highest native resolution
254 * - A higher HDR range is preferred
255 * - Higher refresh is preferred (ignoring small differences)
256 * - Lower scale values are preferred (larger display)
257 */
258static int Wayland_GetPrimaryDisplay(SDL_VideoData *vid)
259{
260 static const int REFRESH_DELTA = 4000;
261
262 // Query the DBus interface to see if the coordinates of the primary display are exposed.
263 int x, y;
264 if (Wayland_GetGNOMEPrimaryDisplayCoordinates(&x, &y)) {
265 for (int i = 0; i < vid->output_count; ++i) {
266 if (vid->output_list[i]->x == x && vid->output_list[i]->y == y) {
267 return i;
268 }
269 }
270 }
271
272 // Otherwise, choose the 'best' display.
273 int best_width = 0;
274 int best_height = 0;
275 double best_scale = 0.0;
276 float best_headroom = 0.0f;
277 int best_refresh = 0;
278 bool best_is_landscape = false;
279 int best_index = 0;
280
281 for (int i = 0; i < vid->output_count; ++i) {
282 const SDL_DisplayData *d = vid->output_list[i];
283 const bool is_landscape = d->orientation != SDL_ORIENTATION_PORTRAIT && d->orientation != SDL_ORIENTATION_PORTRAIT_FLIPPED;
284 bool have_new_best = false;
285
286 if (!best_is_landscape && is_landscape) { // Favor landscape over portrait displays.
287 have_new_best = true;
288 } else if (!best_is_landscape || is_landscape) { // Ignore portrait displays if a landscape was already found.
289 if (d->pixel_width > best_width || d->pixel_height > best_height) {
290 have_new_best = true;
291 } else if (d->pixel_width == best_width && d->pixel_height == best_height) {
292 if (d->HDR.HDR_headroom > best_headroom) { // Favor a higher HDR luminance range
293 have_new_best = true;
294 } else if (d->HDR.HDR_headroom == best_headroom) {
295 if (d->refresh - best_refresh > REFRESH_DELTA) { // Favor a higher refresh rate, but ignore small differences (e.g. 59.97 vs 60.1)
296 have_new_best = true;
297 } else if (d->scale_factor < best_scale && SDL_abs(d->refresh - best_refresh) <= REFRESH_DELTA) {
298 // Prefer a lower scale display if the difference in refresh rate is small.
299 have_new_best = true;
300 }
301 }
302 }
303 }
304
305 if (have_new_best) {
306 best_width = d->pixel_width;
307 best_height = d->pixel_height;
308 best_scale = d->scale_factor;
309 best_headroom = d->HDR.HDR_headroom;
310 best_refresh = d->refresh;
311 best_is_landscape = is_landscape;
312 best_index = i;
313 }
314 }
315
316 return best_index;
317}
318
319static void Wayland_SortOutputsByPriorityHint(SDL_VideoData *vid)
320{
321 const char *name_hint = SDL_GetHint(SDL_HINT_VIDEO_DISPLAY_PRIORITY);
322
323 if (name_hint) {
324 char *saveptr;
325 char *str = SDL_strdup(name_hint);
326 SDL_DisplayData **sorted_list = SDL_malloc(sizeof(SDL_DisplayData *) * vid->output_count);
327
328 if (str && sorted_list) {
329 int sorted_index = 0;
330
331 // Sort the requested displays to the front of the list.
332 const char *token = SDL_strtok_r(str, ",", &saveptr);
333 while (token) {
334 for (int i = 0; i < vid->output_count; ++i) {
335 SDL_DisplayData *d = vid->output_list[i];
336 if (d && d->wl_output_name && SDL_strcmp(token, d->wl_output_name) == 0) {
337 sorted_list[sorted_index++] = d;
338 vid->output_list[i] = NULL;
339 break;
340 }
341 }
342
343 token = SDL_strtok_r(NULL, ",", &saveptr);
344 }
345
346 // Append the remaining outputs to the end of the list.
347 for (int i = 0; i < vid->output_count; ++i) {
348 if (vid->output_list[i]) {
349 sorted_list[sorted_index++] = vid->output_list[i];
350 }
351 }
352
353 // Copy the sorted list to the output list.
354 SDL_memcpy(vid->output_list, sorted_list, sizeof(SDL_DisplayData *) * vid->output_count);
355 }
356
357 SDL_free(str);
358 SDL_free(sorted_list);
359 }
360}
361
362static void Wayland_SortOutputs(SDL_VideoData *vid)
363{
364 // Sort by position or connector name, so the order of outputs is deterministic.
365 SDL_qsort(vid->output_list, vid->output_count, sizeof(SDL_DisplayData *), Wayland_DisplayPositionCompare);
366
367 // Find a suitable primary display and move it to the front of the list.
368 const int primary_index = Wayland_GetPrimaryDisplay(vid);
369 if (primary_index) {
370 SDL_DisplayData *primary = vid->output_list[primary_index];
371 SDL_memmove(&vid->output_list[1], &vid->output_list[0], sizeof(SDL_DisplayData *) * primary_index);
372 vid->output_list[0] = primary;
373 }
374
375 // Apply the ordering hint, if specified.
376 Wayland_SortOutputsByPriorityHint(vid);
377}
378
379static void display_handle_done(void *data, struct wl_output *output);
380
381// Initialization/Query functions
382static bool Wayland_VideoInit(SDL_VideoDevice *_this);
383static bool Wayland_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect);
384static void Wayland_VideoQuit(SDL_VideoDevice *_this);
385
386static const char *SDL_WAYLAND_surface_tag = "sdl-window";
387static const char *SDL_WAYLAND_output_tag = "sdl-output";
388
389void SDL_WAYLAND_register_surface(struct wl_surface *surface)
390{
391 wl_proxy_set_tag((struct wl_proxy *)surface, &SDL_WAYLAND_surface_tag);
392}
393
394void SDL_WAYLAND_register_output(struct wl_output *output)
395{
396 wl_proxy_set_tag((struct wl_proxy *)output, &SDL_WAYLAND_output_tag);
397}
398
399bool SDL_WAYLAND_own_surface(struct wl_surface *surface)
400{
401 return wl_proxy_get_tag((struct wl_proxy *)surface) == &SDL_WAYLAND_surface_tag;
402}
403
404bool SDL_WAYLAND_own_output(struct wl_output *output)
405{
406 return wl_proxy_get_tag((struct wl_proxy *)output) == &SDL_WAYLAND_output_tag;
407}
408
409/* External surfaces may have their own user data attached, the modification of which
410 * can cause problems with external toolkits. Instead, external windows are kept in
411 * their own list, and a search is conducted to find a matching surface.
412 */
413static struct wl_list external_window_list;
414
415void Wayland_AddWindowDataToExternalList(SDL_WindowData *data)
416{
417 WAYLAND_wl_list_insert(&external_window_list, &data->external_window_list_link);
418}
419
420void Wayland_RemoveWindowDataFromExternalList(SDL_WindowData *data)
421{
422 WAYLAND_wl_list_remove(&data->external_window_list_link);
423}
424
425SDL_WindowData *Wayland_GetWindowDataForOwnedSurface(struct wl_surface *surface)
426{
427 if (SDL_WAYLAND_own_surface(surface)) {
428 return (SDL_WindowData *)wl_surface_get_user_data(surface);
429 } else if (!WAYLAND_wl_list_empty(&external_window_list)) {
430 SDL_WindowData *p;
431 wl_list_for_each (p, &external_window_list, external_window_list_link) {
432 if (p->surface == surface) {
433 return p;
434 }
435 }
436 }
437
438 return NULL;
439}
440
441static void Wayland_DeleteDevice(SDL_VideoDevice *device)
442{
443 SDL_VideoData *data = device->internal;
444 if (data->display && !data->display_externally_owned) {
445 WAYLAND_wl_display_flush(data->display);
446 WAYLAND_wl_display_disconnect(data->display);
447 SDL_ClearProperty(SDL_GetGlobalProperties(), SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER);
448 }
449 if (device->wakeup_lock) {
450 SDL_DestroyMutex(device->wakeup_lock);
451 }
452 SDL_free(data);
453 SDL_free(device);
454 SDL_WAYLAND_UnloadSymbols();
455}
456
457typedef struct
458{
459 bool has_fifo_v1;
460} SDL_WaylandPreferredData;
461
462static void wayland_preferred_check_handle_global(void *data, struct wl_registry *registry, uint32_t id,
463 const char *interface, uint32_t version)
464{
465 SDL_WaylandPreferredData *d = data;
466
467 if (SDL_strcmp(interface, "wp_fifo_manager_v1") == 0) {
468 d->has_fifo_v1 = true;
469 }
470}
471
472static void wayland_preferred_check_remove_global(void *data, struct wl_registry *registry, uint32_t id)
473{
474 // No need to do anything here.
475}
476
477static const struct wl_registry_listener preferred_registry_listener = {
478 wayland_preferred_check_handle_global,
479 wayland_preferred_check_remove_global
480};
481
482static bool Wayland_IsPreferred(struct wl_display *display)
483{
484 struct wl_registry *registry = wl_display_get_registry(display);
485 SDL_WaylandPreferredData preferred_data = { 0 };
486
487 if (!registry) {
488 SDL_SetError("Failed to get the Wayland registry");
489 return false;
490 }
491
492 wl_registry_add_listener(registry, &preferred_registry_listener, &preferred_data);
493
494 WAYLAND_wl_display_roundtrip(display);
495
496 wl_registry_destroy(registry);
497
498 return preferred_data.has_fifo_v1;
499}
500
501static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
502{
503 SDL_VideoDevice *device;
504 SDL_VideoData *data;
505 struct SDL_WaylandInput *input;
506 struct wl_display *display = SDL_GetPointerProperty(SDL_GetGlobalProperties(),
507 SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, NULL);
508 bool display_is_external = !!display;
509
510 // Are we trying to connect to or are currently in a Wayland session?
511 if (!SDL_getenv("WAYLAND_DISPLAY")) {
512 const char *session = SDL_getenv("XDG_SESSION_TYPE");
513 if (session && SDL_strcasecmp(session, "wayland") != 0) {
514 return NULL;
515 }
516 }
517
518 if (!SDL_WAYLAND_LoadSymbols()) {
519 return NULL;
520 }
521
522 if (!display) {
523 display = WAYLAND_wl_display_connect(NULL);
524 if (!display) {
525 SDL_WAYLAND_UnloadSymbols();
526 return NULL;
527 }
528 }
529
530 /*
531 * If we are checking for preferred Wayland, then let's query for
532 * fifo-v1's existence, so we don't regress GPU-bound performance
533 * and frame-pacing by default due to swapchain starvation.
534 */
535 if (require_preferred_protocols && !Wayland_IsPreferred(display)) {
536 if (!display_is_external) {
537 WAYLAND_wl_display_disconnect(display);
538 }
539 SDL_WAYLAND_UnloadSymbols();
540 return NULL;
541 }
542
543 data = SDL_calloc(1, sizeof(*data));
544 if (!data) {
545 if (!display_is_external) {
546 WAYLAND_wl_display_disconnect(display);
547 }
548 SDL_WAYLAND_UnloadSymbols();
549 return NULL;
550 }
551
552 input = SDL_calloc(1, sizeof(*input));
553 if (!input) {
554 SDL_free(data);
555 if (!display_is_external) {
556 WAYLAND_wl_display_disconnect(display);
557 }
558 SDL_WAYLAND_UnloadSymbols();
559 return NULL;
560 }
561
562 input->display = data;
563 input->sx_w = wl_fixed_from_int(0);
564 input->sy_w = wl_fixed_from_int(0);
565 input->xkb.current_group = XKB_GROUP_INVALID;
566
567 data->initializing = true;
568 data->display = display;
569 data->input = input;
570 data->display_externally_owned = display_is_external;
571 data->scale_to_display_enabled = SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_SCALE_TO_DISPLAY, false);
572 WAYLAND_wl_list_init(&external_window_list);
573
574 // Initialize all variables that we clean on shutdown
575 device = SDL_calloc(1, sizeof(SDL_VideoDevice));
576 if (!device) {
577 SDL_free(input);
578 SDL_free(data);
579 if (!display_is_external) {
580 WAYLAND_wl_display_disconnect(display);
581 }
582 SDL_WAYLAND_UnloadSymbols();
583 return NULL;
584 }
585
586 if (!display_is_external) {
587 SDL_SetPointerProperty(SDL_GetGlobalProperties(),
588 SDL_PROP_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, display);
589 }
590
591 device->internal = data;
592 device->wakeup_lock = SDL_CreateMutex();
593
594 // Set the function pointers
595 device->VideoInit = Wayland_VideoInit;
596 device->VideoQuit = Wayland_VideoQuit;
597 device->GetDisplayBounds = Wayland_GetDisplayBounds;
598 device->SuspendScreenSaver = Wayland_SuspendScreenSaver;
599
600 device->PumpEvents = Wayland_PumpEvents;
601 device->WaitEventTimeout = Wayland_WaitEventTimeout;
602 device->SendWakeupEvent = Wayland_SendWakeupEvent;
603
604#ifdef SDL_VIDEO_OPENGL_EGL
605 device->GL_SwapWindow = Wayland_GLES_SwapWindow;
606 device->GL_GetSwapInterval = Wayland_GLES_GetSwapInterval;
607 device->GL_SetSwapInterval = Wayland_GLES_SetSwapInterval;
608 device->GL_MakeCurrent = Wayland_GLES_MakeCurrent;
609 device->GL_CreateContext = Wayland_GLES_CreateContext;
610 device->GL_LoadLibrary = Wayland_GLES_LoadLibrary;
611 device->GL_UnloadLibrary = Wayland_GLES_UnloadLibrary;
612 device->GL_GetProcAddress = Wayland_GLES_GetProcAddress;
613 device->GL_DestroyContext = Wayland_GLES_DestroyContext;
614 device->GL_GetEGLSurface = Wayland_GLES_GetEGLSurface;
615#endif
616
617 device->CreateSDLWindow = Wayland_CreateWindow;
618 device->ShowWindow = Wayland_ShowWindow;
619 device->HideWindow = Wayland_HideWindow;
620 device->RaiseWindow = Wayland_RaiseWindow;
621 device->SetWindowFullscreen = Wayland_SetWindowFullscreen;
622 device->MaximizeWindow = Wayland_MaximizeWindow;
623 device->MinimizeWindow = Wayland_MinimizeWindow;
624 device->SetWindowMouseRect = Wayland_SetWindowMouseRect;
625 device->SetWindowMouseGrab = Wayland_SetWindowMouseGrab;
626 device->SetWindowKeyboardGrab = Wayland_SetWindowKeyboardGrab;
627 device->RestoreWindow = Wayland_RestoreWindow;
628 device->SetWindowBordered = Wayland_SetWindowBordered;
629 device->SetWindowResizable = Wayland_SetWindowResizable;
630 device->SetWindowPosition = Wayland_SetWindowPosition;
631 device->SetWindowSize = Wayland_SetWindowSize;
632 device->SetWindowMinimumSize = Wayland_SetWindowMinimumSize;
633 device->SetWindowMaximumSize = Wayland_SetWindowMaximumSize;
634 device->SetWindowParent = Wayland_SetWindowParent;
635 device->SetWindowModal = Wayland_SetWindowModal;
636 device->SetWindowOpacity = Wayland_SetWindowOpacity;
637 device->SetWindowTitle = Wayland_SetWindowTitle;
638 device->SetWindowIcon = Wayland_SetWindowIcon;
639 device->GetWindowSizeInPixels = Wayland_GetWindowSizeInPixels;
640 device->GetWindowContentScale = Wayland_GetWindowContentScale;
641 device->GetWindowICCProfile = Wayland_GetWindowICCProfile;
642 device->GetDisplayForWindow = Wayland_GetDisplayForWindow;
643 device->DestroyWindow = Wayland_DestroyWindow;
644 device->SetWindowHitTest = Wayland_SetWindowHitTest;
645 device->FlashWindow = Wayland_FlashWindow;
646 device->HasScreenKeyboardSupport = Wayland_HasScreenKeyboardSupport;
647 device->ShowWindowSystemMenu = Wayland_ShowWindowSystemMenu;
648 device->SyncWindow = Wayland_SyncWindow;
649
650#ifdef SDL_USE_LIBDBUS
651 if (SDL_SystemTheme_Init())
652 device->system_theme = SDL_SystemTheme_Get();
653#endif
654
655 device->GetTextMimeTypes = Wayland_GetTextMimeTypes;
656 device->SetClipboardData = Wayland_SetClipboardData;
657 device->GetClipboardData = Wayland_GetClipboardData;
658 device->HasClipboardData = Wayland_HasClipboardData;
659 device->StartTextInput = Wayland_StartTextInput;
660 device->StopTextInput = Wayland_StopTextInput;
661 device->UpdateTextInputArea = Wayland_UpdateTextInputArea;
662
663#ifdef SDL_VIDEO_VULKAN
664 device->Vulkan_LoadLibrary = Wayland_Vulkan_LoadLibrary;
665 device->Vulkan_UnloadLibrary = Wayland_Vulkan_UnloadLibrary;
666 device->Vulkan_GetInstanceExtensions = Wayland_Vulkan_GetInstanceExtensions;
667 device->Vulkan_CreateSurface = Wayland_Vulkan_CreateSurface;
668 device->Vulkan_DestroySurface = Wayland_Vulkan_DestroySurface;
669 device->Vulkan_GetPresentationSupport = Wayland_Vulkan_GetPresentationSupport;
670#endif
671
672 device->free = Wayland_DeleteDevice;
673
674 device->device_caps = VIDEO_DEVICE_CAPS_MODE_SWITCHING_EMULATED |
675 VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT |
676 VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS |
677 VIDEO_DEVICE_CAPS_SENDS_DISPLAY_CHANGES |
678 VIDEO_DEVICE_CAPS_DISABLE_MOUSE_WARP_ON_FULLSCREEN_TRANSITIONS |
679 VIDEO_DEVICE_CAPS_SENDS_HDR_CHANGES;
680
681 return device;
682}
683
684static SDL_VideoDevice *Wayland_Preferred_CreateDevice(void)
685{
686 return Wayland_CreateDevice(true);
687}
688
689static SDL_VideoDevice *Wayland_Fallback_CreateDevice(void)
690{
691 return Wayland_CreateDevice(false);
692}
693
694VideoBootStrap Wayland_preferred_bootstrap = {
695 WAYLANDVID_DRIVER_NAME, "SDL Wayland video driver",
696 Wayland_Preferred_CreateDevice,
697 Wayland_ShowMessageBox,
698 true
699};
700
701VideoBootStrap Wayland_bootstrap = {
702 WAYLANDVID_DRIVER_NAME, "SDL Wayland video driver",
703 Wayland_Fallback_CreateDevice,
704 Wayland_ShowMessageBox,
705 false
706};
707
708static void xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output,
709 int32_t x, int32_t y)
710{
711 SDL_DisplayData *internal = (SDL_DisplayData *)data;
712
713 internal->x = x;
714 internal->y = y;
715 internal->has_logical_position = true;
716}
717
718static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output,
719 int32_t width, int32_t height)
720{
721 SDL_DisplayData *internal = (SDL_DisplayData *)data;
722
723 internal->logical_width = width;
724 internal->logical_height = height;
725 internal->has_logical_size = true;
726}
727
728static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output)
729{
730 SDL_DisplayData *internal = data;
731
732 /*
733 * xdg-output.done events are deprecated and only apply below version 3 of the protocol.
734 * A wl-output.done event will be emitted in version 3 or higher.
735 */
736 if (zxdg_output_v1_get_version(internal->xdg_output) < 3) {
737 display_handle_done(data, internal->output);
738 }
739}
740
741static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output,
742 const char *name)
743{
744 SDL_DisplayData *internal = (SDL_DisplayData *)data;
745
746 // Deprecated as of wl_output v4.
747 if (wl_output_get_version(internal->output) < WL_OUTPUT_NAME_SINCE_VERSION &&
748 internal->display == 0) {
749 SDL_free(internal->wl_output_name);
750 internal->wl_output_name = SDL_strdup(name);
751 }
752}
753
754static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output,
755 const char *description)
756{
757 SDL_DisplayData *internal = (SDL_DisplayData *)data;
758
759 // Deprecated as of wl_output v4.
760 if (wl_output_get_version(internal->output) < WL_OUTPUT_DESCRIPTION_SINCE_VERSION &&
761 internal->display == 0) {
762 // xdg-output descriptions, if available, supersede wl-output model names.
763 SDL_free(internal->placeholder.name);
764 internal->placeholder.name = SDL_strdup(description);
765 }
766}
767
768static const struct zxdg_output_v1_listener xdg_output_listener = {
769 xdg_output_handle_logical_position,
770 xdg_output_handle_logical_size,
771 xdg_output_handle_done,
772 xdg_output_handle_name,
773 xdg_output_handle_description,
774};
775
776static void AddEmulatedModes(SDL_DisplayData *dispdata, int native_width, int native_height)
777{
778 struct EmulatedMode
779 {
780 int w;
781 int h;
782 };
783
784 // Resolution lists courtesy of XWayland
785 const struct EmulatedMode mode_list[] = {
786 // 16:9 (1.77)
787 { 7680, 4320 },
788 { 6144, 3160 },
789 { 5120, 2880 },
790 { 4096, 2304 },
791 { 3840, 2160 },
792 { 3200, 1800 },
793 { 2880, 1620 },
794 { 2560, 1440 },
795 { 2048, 1152 },
796 { 1920, 1080 },
797 { 1600, 900 },
798 { 1368, 768 },
799 { 1280, 720 },
800 { 864, 486 },
801
802 // 16:10 (1.6)
803 { 2560, 1600 },
804 { 1920, 1200 },
805 { 1680, 1050 },
806 { 1440, 900 },
807 { 1280, 800 },
808
809 // 3:2 (1.5)
810 { 720, 480 },
811
812 // 4:3 (1.33)
813 { 2048, 1536 },
814 { 1920, 1440 },
815 { 1600, 1200 },
816 { 1440, 1080 },
817 { 1400, 1050 },
818 { 1280, 1024 },
819 { 1280, 960 },
820 { 1152, 864 },
821 { 1024, 768 },
822 { 800, 600 },
823 { 640, 480 }
824 };
825
826 int i;
827 SDL_DisplayMode mode;
828 SDL_VideoDisplay *dpy = dispdata->display ? SDL_GetVideoDisplay(dispdata->display) : &dispdata->placeholder;
829 const bool rot_90 = native_width < native_height; // Reverse width/height for portrait displays.
830
831 for (i = 0; i < SDL_arraysize(mode_list); ++i) {
832 SDL_zero(mode);
833 mode.format = dpy->desktop_mode.format;
834 mode.refresh_rate_numerator = dpy->desktop_mode.refresh_rate_numerator;
835 mode.refresh_rate_denominator = dpy->desktop_mode.refresh_rate_denominator;
836
837 if (rot_90) {
838 mode.w = mode_list[i].h;
839 mode.h = mode_list[i].w;
840 } else {
841 mode.w = mode_list[i].w;
842 mode.h = mode_list[i].h;
843 }
844
845 // Only add modes that are smaller than the native mode.
846 if ((mode.w < native_width && mode.h < native_height) ||
847 (mode.w < native_width && mode.h == native_height) ||
848 (mode.w == native_width && mode.h < native_height)) {
849 SDL_AddFullscreenDisplayMode(dpy, &mode);
850 }
851 }
852}
853
854static void display_handle_geometry(void *data,
855 struct wl_output *output,
856 int x, int y,
857 int physical_width,
858 int physical_height,
859 int subpixel,
860 const char *make,
861 const char *model,
862 int transform)
863
864{
865 SDL_DisplayData *internal = (SDL_DisplayData *)data;
866
867 // Apply the change from wl-output only if xdg-output is not supported
868 if (!internal->has_logical_position) {
869 internal->x = x;
870 internal->y = y;
871 }
872 internal->physical_width_mm = physical_width;
873 internal->physical_height_mm = physical_height;
874
875 // The model is only used for the output name if wl_output or xdg-output haven't provided a description.
876 if (internal->display == 0 && !internal->placeholder.name) {
877 internal->placeholder.name = SDL_strdup(model);
878 }
879
880 internal->transform = transform;
881#define TF_CASE(in, out) \
882 case WL_OUTPUT_TRANSFORM_##in: \
883 internal->orientation = SDL_ORIENTATION_##out; \
884 break;
885 if (internal->physical_width_mm >= internal->physical_height_mm) {
886 switch (transform) {
887 TF_CASE(NORMAL, LANDSCAPE)
888 TF_CASE(90, PORTRAIT)
889 TF_CASE(180, LANDSCAPE_FLIPPED)
890 TF_CASE(270, PORTRAIT_FLIPPED)
891 TF_CASE(FLIPPED, LANDSCAPE_FLIPPED)
892 TF_CASE(FLIPPED_90, PORTRAIT_FLIPPED)
893 TF_CASE(FLIPPED_180, LANDSCAPE)
894 TF_CASE(FLIPPED_270, PORTRAIT)
895 }
896 } else {
897 switch (transform) {
898 TF_CASE(NORMAL, PORTRAIT)
899 TF_CASE(90, LANDSCAPE)
900 TF_CASE(180, PORTRAIT_FLIPPED)
901 TF_CASE(270, LANDSCAPE_FLIPPED)
902 TF_CASE(FLIPPED, PORTRAIT_FLIPPED)
903 TF_CASE(FLIPPED_90, LANDSCAPE_FLIPPED)
904 TF_CASE(FLIPPED_180, PORTRAIT)
905 TF_CASE(FLIPPED_270, LANDSCAPE)
906 }
907 }
908#undef TF_CASE
909}
910
911static void display_handle_mode(void *data,
912 struct wl_output *output,
913 uint32_t flags,
914 int width,
915 int height,
916 int refresh)
917{
918 SDL_DisplayData *internal = (SDL_DisplayData *)data;
919
920 if (flags & WL_OUTPUT_MODE_CURRENT) {
921 internal->pixel_width = width;
922 internal->pixel_height = height;
923
924 /*
925 * Don't rotate this yet, wl-output coordinates are transformed in
926 * handle_done and xdg-output coordinates are pre-transformed.
927 */
928 if (!internal->has_logical_size) {
929 internal->logical_width = width;
930 internal->logical_height = height;
931 }
932
933 internal->refresh = refresh;
934 }
935}
936
937static void display_handle_done(void *data,
938 struct wl_output *output)
939{
940 const bool mode_emulation_enabled = SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_MODE_EMULATION, true);
941 SDL_DisplayData *internal = (SDL_DisplayData *)data;
942 SDL_VideoData *video = internal->videodata;
943 SDL_DisplayMode native_mode, desktop_mode;
944
945 /*
946 * When using xdg-output, two wl-output.done events will be emitted:
947 * one at the completion of wl-display and one at the completion of xdg-output.
948 *
949 * All required events must be received before proceeding.
950 */
951 const int event_await_count = 1 + (internal->xdg_output != NULL);
952
953 internal->wl_output_done_count = SDL_min(internal->wl_output_done_count + 1, event_await_count + 1);
954
955 if (internal->wl_output_done_count < event_await_count) {
956 return;
957 }
958
959 // If the display was already created, reset and rebuild the mode list.
960 SDL_VideoDisplay *dpy = SDL_GetVideoDisplay(internal->display);
961 if (dpy) {
962 SDL_ResetFullscreenDisplayModes(dpy);
963 }
964
965 // The native display resolution
966 SDL_zero(native_mode);
967 native_mode.format = SDL_PIXELFORMAT_XRGB8888;
968
969 // Transform the pixel values, if necessary.
970 if (internal->transform & WL_OUTPUT_TRANSFORM_90) {
971 native_mode.w = internal->pixel_height;
972 native_mode.h = internal->pixel_width;
973 } else {
974 native_mode.w = internal->pixel_width;
975 native_mode.h = internal->pixel_height;
976 }
977 native_mode.refresh_rate_numerator = internal->refresh;
978 native_mode.refresh_rate_denominator = 1000;
979
980 if (internal->has_logical_size) { // If xdg-output is present...
981 if (native_mode.w != internal->logical_width || native_mode.h != internal->logical_height) {
982 // ...and the compositor scales the logical viewport...
983 if (video->viewporter) {
984 // ...and viewports are supported, calculate the true scale of the output.
985 internal->scale_factor = (double)native_mode.w / (double)internal->logical_width;
986 } else {
987 // ...otherwise, the 'native' pixel values are a multiple of the logical screen size.
988 internal->pixel_width = internal->logical_width * (int)internal->scale_factor;
989 internal->pixel_height = internal->logical_height * (int)internal->scale_factor;
990 }
991 } else {
992 /* ...and the output viewport is not scaled in the global compositing
993 * space, the output dimensions need to be divided by the scale factor.
994 */
995 internal->logical_width /= (int)internal->scale_factor;
996 internal->logical_height /= (int)internal->scale_factor;
997 }
998 } else {
999 /* Calculate the points from the pixel values, if xdg-output isn't present.
1000 * Use the native mode pixel values since they are pre-transformed.
1001 */
1002 internal->logical_width = native_mode.w / (int)internal->scale_factor;
1003 internal->logical_height = native_mode.h / (int)internal->scale_factor;
1004 }
1005
1006 // The scaled desktop mode
1007 SDL_zero(desktop_mode);
1008 desktop_mode.format = SDL_PIXELFORMAT_XRGB8888;
1009
1010 if (!video->scale_to_display_enabled) {
1011 desktop_mode.w = internal->logical_width;
1012 desktop_mode.h = internal->logical_height;
1013 desktop_mode.pixel_density = (float)internal->scale_factor;
1014 } else {
1015 desktop_mode.w = native_mode.w;
1016 desktop_mode.h = native_mode.h;
1017 desktop_mode.pixel_density = 1.0f;
1018 }
1019
1020 desktop_mode.refresh_rate_numerator = internal->refresh;
1021 desktop_mode.refresh_rate_denominator = 1000;
1022
1023 if (internal->display > 0) {
1024 dpy = SDL_GetVideoDisplay(internal->display);
1025 } else {
1026 dpy = &internal->placeholder;
1027 }
1028
1029 if (video->scale_to_display_enabled) {
1030 SDL_SetDisplayContentScale(dpy, (float)internal->scale_factor);
1031 }
1032
1033 // Set the desktop display mode.
1034 SDL_SetDesktopDisplayMode(dpy, &desktop_mode);
1035
1036 // Expose the unscaled, native resolution if the scale is 1.0 or viewports are available...
1037 if (internal->scale_factor == 1.0 || video->viewporter) {
1038 SDL_AddFullscreenDisplayMode(dpy, &native_mode);
1039 if (native_mode.w != desktop_mode.w ||
1040 native_mode.h != desktop_mode.h) {
1041 SDL_AddFullscreenDisplayMode(dpy, &desktop_mode);
1042 }
1043 } else {
1044 // ...otherwise expose the integer scaled variants of the desktop resolution down to 1.
1045 int i;
1046
1047 desktop_mode.pixel_density = 1.0f;
1048
1049 for (i = (int)internal->scale_factor; i > 0; --i) {
1050 desktop_mode.w = internal->logical_width * i;
1051 desktop_mode.h = internal->logical_height * i;
1052 SDL_AddFullscreenDisplayMode(dpy, &desktop_mode);
1053 }
1054 }
1055
1056 // Add emulated modes if wp_viewporter is supported and mode emulation is enabled.
1057 if (video->viewporter && mode_emulation_enabled) {
1058 // The transformed display pixel width/height must be used here.
1059 AddEmulatedModes(internal, native_mode.w, native_mode.h);
1060 }
1061
1062 SDL_SetDisplayHDRProperties(dpy, &internal->HDR);
1063
1064 if (internal->display == 0) {
1065 // First time getting display info, initialize the VideoDisplay
1066 if (internal->physical_width_mm >= internal->physical_height_mm) {
1067 internal->placeholder.natural_orientation = SDL_ORIENTATION_LANDSCAPE;
1068 } else {
1069 internal->placeholder.natural_orientation = SDL_ORIENTATION_PORTRAIT;
1070 }
1071 internal->placeholder.current_orientation = internal->orientation;
1072 internal->placeholder.internal = internal;
1073
1074 // During initialization, the displays will be added after enumeration is complete.
1075 if (!video->initializing) {
1076 internal->display = SDL_AddVideoDisplay(&internal->placeholder, true);
1077 SDL_free(internal->placeholder.name);
1078 SDL_zero(internal->placeholder);
1079 }
1080 } else {
1081 SDL_SendDisplayEvent(dpy, SDL_EVENT_DISPLAY_ORIENTATION, internal->orientation, 0);
1082 }
1083}
1084
1085static void display_handle_scale(void *data,
1086 struct wl_output *output,
1087 int32_t factor)
1088{
1089 SDL_DisplayData *internal = (SDL_DisplayData *)data;
1090 internal->scale_factor = factor;
1091}
1092
1093static void display_handle_name(void *data, struct wl_output *wl_output, const char *name)
1094{
1095 SDL_DisplayData *internal = (SDL_DisplayData *)data;
1096
1097 SDL_free(internal->wl_output_name);
1098 internal->wl_output_name = SDL_strdup(name);
1099}
1100
1101static void display_handle_description(void *data, struct wl_output *wl_output, const char *description)
1102{
1103 SDL_DisplayData *internal = (SDL_DisplayData *)data;
1104
1105 if (internal->display == 0) {
1106 // The description, if available, supersedes the model name.
1107 SDL_free(internal->placeholder.name);
1108 internal->placeholder.name = SDL_strdup(description);
1109 }
1110}
1111
1112static const struct wl_output_listener output_listener = {
1113 display_handle_geometry, // Version 1
1114 display_handle_mode, // Version 1
1115 display_handle_done, // Version 2
1116 display_handle_scale, // Version 2
1117 display_handle_name, // Version 4
1118 display_handle_description // Version 4
1119};
1120
1121static void handle_output_image_description_changed(void *data,
1122 struct wp_color_management_output_v1 *wp_color_management_output_v1)
1123{
1124 SDL_DisplayData *display = (SDL_DisplayData *)data;
1125 // wl_display.done is called after this event, so the display HDR status will be updated there.
1126 Wayland_GetColorInfoForOutput(display, false);
1127}
1128
1129static const struct wp_color_management_output_v1_listener wp_color_management_output_listener = {
1130 handle_output_image_description_changed
1131};
1132
1133static bool Wayland_add_display(SDL_VideoData *d, uint32_t id, uint32_t version)
1134{
1135 struct wl_output *output;
1136 SDL_DisplayData *data;
1137
1138 output = wl_registry_bind(d->registry, id, &wl_output_interface, version);
1139 if (!output) {
1140 return SDL_SetError("Failed to retrieve output.");
1141 }
1142 data = (SDL_DisplayData *)SDL_calloc(1, sizeof(*data));
1143 data->videodata = d;
1144 data->output = output;
1145 data->registry_id = id;
1146 data->scale_factor = 1.0f;
1147
1148 wl_output_add_listener(output, &output_listener, data);
1149 SDL_WAYLAND_register_output(output);
1150
1151 // Keep a list of outputs for sorting and deferred protocol initialization.
1152 if (d->output_count == d->output_max) {
1153 d->output_max += 4;
1154 d->output_list = SDL_realloc(d->output_list, sizeof(SDL_DisplayData *) * d->output_max);
1155 }
1156 d->output_list[d->output_count++] = data;
1157
1158 if (data->videodata->xdg_output_manager) {
1159 data->xdg_output = zxdg_output_manager_v1_get_xdg_output(data->videodata->xdg_output_manager, output);
1160 zxdg_output_v1_add_listener(data->xdg_output, &xdg_output_listener, data);
1161 }
1162 if (data->videodata->wp_color_manager_v1) {
1163 data->wp_color_management_output = wp_color_manager_v1_get_output(data->videodata->wp_color_manager_v1, output);
1164 wp_color_management_output_v1_add_listener(data->wp_color_management_output, &wp_color_management_output_listener, data);
1165 Wayland_GetColorInfoForOutput(data, true);
1166 }
1167 return true;
1168}
1169
1170static void Wayland_free_display(SDL_VideoDisplay *display, bool send_event)
1171{
1172 if (display) {
1173 SDL_DisplayData *display_data = display->internal;
1174
1175 /* A preceding surface leave event is not guaranteed when an output is removed,
1176 * so ensure that no window continues to hold a reference to a removed output.
1177 */
1178 for (SDL_Window *window = SDL_GetVideoDevice()->windows; window; window = window->next) {
1179 Wayland_RemoveOutputFromWindow(window->internal, display_data);
1180 }
1181
1182 SDL_free(display_data->wl_output_name);
1183
1184 if (display_data->wp_color_management_output) {
1185 Wayland_FreeColorInfoState(display_data->color_info_state);
1186 wp_color_management_output_v1_destroy(display_data->wp_color_management_output);
1187 }
1188
1189 if (display_data->xdg_output) {
1190 zxdg_output_v1_destroy(display_data->xdg_output);
1191 }
1192
1193 if (wl_output_get_version(display_data->output) >= WL_OUTPUT_RELEASE_SINCE_VERSION) {
1194 wl_output_release(display_data->output);
1195 } else {
1196 wl_output_destroy(display_data->output);
1197 }
1198
1199 SDL_DelVideoDisplay(display->id, send_event);
1200 }
1201}
1202
1203static void Wayland_FinalizeDisplays(SDL_VideoData *vid)
1204{
1205 Wayland_SortOutputs(vid);
1206 for(int i = 0; i < vid->output_count; ++i) {
1207 SDL_DisplayData *d = vid->output_list[i];
1208 d->display = SDL_AddVideoDisplay(&d->placeholder, false);
1209 SDL_free(d->placeholder.name);
1210 SDL_zero(d->placeholder);
1211 }
1212}
1213
1214static void Wayland_init_xdg_output(SDL_VideoData *d)
1215{
1216 for (int i = 0; i < d->output_count; ++i) {
1217 SDL_DisplayData *disp = d->output_list[i];
1218 disp->xdg_output = zxdg_output_manager_v1_get_xdg_output(disp->videodata->xdg_output_manager, disp->output);
1219 zxdg_output_v1_add_listener(disp->xdg_output, &xdg_output_listener, disp);
1220 }
1221}
1222
1223static void Wayland_InitColorManager(SDL_VideoData *d)
1224{
1225 for (int i = 0; i < d->output_count; ++i) {
1226 SDL_DisplayData *disp = d->output_list[i];
1227 disp->wp_color_management_output = wp_color_manager_v1_get_output(disp->videodata->wp_color_manager_v1, disp->output);
1228 wp_color_management_output_v1_add_listener(disp->wp_color_management_output, &wp_color_management_output_listener, disp);
1229 Wayland_GetColorInfoForOutput(disp, true);
1230 }
1231}
1232
1233static void handle_ping_xdg_wm_base(void *data, struct xdg_wm_base *xdg, uint32_t serial)
1234{
1235 xdg_wm_base_pong(xdg, serial);
1236}
1237
1238static const struct xdg_wm_base_listener shell_listener_xdg = {
1239 handle_ping_xdg_wm_base
1240};
1241
1242#ifdef HAVE_LIBDECOR_H
1243static void libdecor_error(struct libdecor *context,
1244 enum libdecor_error error,
1245 const char *message)
1246{
1247 SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "libdecor error (%d): %s", error, message);
1248}
1249
1250static struct libdecor_interface libdecor_interface = {
1251 libdecor_error,
1252};
1253#endif
1254
1255static void display_handle_global(void *data, struct wl_registry *registry, uint32_t id,
1256 const char *interface, uint32_t version)
1257{
1258 SDL_VideoData *d = data;
1259
1260 // printf("WAYLAND INTERFACE: %s\n", interface);
1261
1262 if (SDL_strcmp(interface, "wl_compositor") == 0) {
1263 d->compositor = wl_registry_bind(d->registry, id, &wl_compositor_interface, SDL_min(SDL_WL_COMPOSITOR_VERSION, version));
1264 } else if (SDL_strcmp(interface, "wl_output") == 0) {
1265 Wayland_add_display(d, id, SDL_min(version, SDL_WL_OUTPUT_VERSION));
1266 } else if (SDL_strcmp(interface, "wl_seat") == 0) {
1267 d->input->seat = wl_registry_bind(d->registry, id, &wl_seat_interface, SDL_min(SDL_WL_SEAT_VERSION, version));
1268 Wayland_input_initialize_seat(d);
1269 } else if (SDL_strcmp(interface, "xdg_wm_base") == 0) {
1270 d->shell.xdg = wl_registry_bind(d->registry, id, &xdg_wm_base_interface, SDL_min(version, 6));
1271 xdg_wm_base_add_listener(d->shell.xdg, &shell_listener_xdg, NULL);
1272 } else if (SDL_strcmp(interface, "wl_shm") == 0) {
1273 d->shm = wl_registry_bind(registry, id, &wl_shm_interface, 1);
1274 } else if (SDL_strcmp(interface, "zwp_relative_pointer_manager_v1") == 0) {
1275 d->relative_pointer_manager = wl_registry_bind(d->registry, id, &zwp_relative_pointer_manager_v1_interface, 1);
1276 Wayland_input_init_relative_pointer(d);
1277 } else if (SDL_strcmp(interface, "zwp_pointer_constraints_v1") == 0) {
1278 d->pointer_constraints = wl_registry_bind(d->registry, id, &zwp_pointer_constraints_v1_interface, 1);
1279 } else if (SDL_strcmp(interface, "zwp_keyboard_shortcuts_inhibit_manager_v1") == 0) {
1280 d->key_inhibitor_manager = wl_registry_bind(d->registry, id, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1);
1281 } else if (SDL_strcmp(interface, "zwp_idle_inhibit_manager_v1") == 0) {
1282 d->idle_inhibit_manager = wl_registry_bind(d->registry, id, &zwp_idle_inhibit_manager_v1_interface, 1);
1283 } else if (SDL_strcmp(interface, "xdg_activation_v1") == 0) {
1284 d->activation_manager = wl_registry_bind(d->registry, id, &xdg_activation_v1_interface, 1);
1285 } else if (SDL_strcmp(interface, "zwp_text_input_manager_v3") == 0) {
1286 Wayland_create_text_input_manager(d, id);
1287 } else if (SDL_strcmp(interface, "wl_data_device_manager") == 0) {
1288 d->data_device_manager = wl_registry_bind(d->registry, id, &wl_data_device_manager_interface, SDL_min(3, version));
1289 Wayland_create_data_device(d);
1290 } else if (SDL_strcmp(interface, "zwp_primary_selection_device_manager_v1") == 0) {
1291 d->primary_selection_device_manager = wl_registry_bind(d->registry, id, &zwp_primary_selection_device_manager_v1_interface, 1);
1292 Wayland_create_primary_selection_device(d);
1293 } else if (SDL_strcmp(interface, "zxdg_decoration_manager_v1") == 0) {
1294 d->decoration_manager = wl_registry_bind(d->registry, id, &zxdg_decoration_manager_v1_interface, 1);
1295 } else if (SDL_strcmp(interface, "zwp_tablet_manager_v2") == 0) {
1296 d->tablet_manager = wl_registry_bind(d->registry, id, &zwp_tablet_manager_v2_interface, 1);
1297 Wayland_input_init_tablet_support(d->input, d->tablet_manager);
1298 } else if (SDL_strcmp(interface, "zxdg_output_manager_v1") == 0) {
1299 version = SDL_min(version, 3); // Versions 1 through 3 are supported.
1300 d->xdg_output_manager = wl_registry_bind(d->registry, id, &zxdg_output_manager_v1_interface, version);
1301 Wayland_init_xdg_output(d);
1302 } else if (SDL_strcmp(interface, "wp_viewporter") == 0) {
1303 d->viewporter = wl_registry_bind(d->registry, id, &wp_viewporter_interface, 1);
1304 } else if (SDL_strcmp(interface, "wp_fractional_scale_manager_v1") == 0) {
1305 d->fractional_scale_manager = wl_registry_bind(d->registry, id, &wp_fractional_scale_manager_v1_interface, 1);
1306 } else if (SDL_strcmp(interface, "zwp_input_timestamps_manager_v1") == 0) {
1307 d->input_timestamps_manager = wl_registry_bind(d->registry, id, &zwp_input_timestamps_manager_v1_interface, 1);
1308 if (d->input) {
1309 Wayland_RegisterTimestampListeners(d->input);
1310 }
1311 } else if (SDL_strcmp(interface, "wp_cursor_shape_manager_v1") == 0) {
1312 d->cursor_shape_manager = wl_registry_bind(d->registry, id, &wp_cursor_shape_manager_v1_interface, 1);
1313 if (d->input) {
1314 Wayland_CreateCursorShapeDevice(d->input);
1315 }
1316 } else if (SDL_strcmp(interface, "zxdg_exporter_v2") == 0) {
1317 d->zxdg_exporter_v2 = wl_registry_bind(d->registry, id, &zxdg_exporter_v2_interface, 1);
1318 } else if (SDL_strcmp(interface, "xdg_wm_dialog_v1") == 0) {
1319 d->xdg_wm_dialog_v1 = wl_registry_bind(d->registry, id, &xdg_wm_dialog_v1_interface, 1);
1320 } else if (SDL_strcmp(interface, "wp_alpha_modifier_v1") == 0) {
1321 d->wp_alpha_modifier_v1 = wl_registry_bind(d->registry, id, &wp_alpha_modifier_v1_interface, 1);
1322 } else if (SDL_strcmp(interface, "xdg_toplevel_icon_manager_v1") == 0) {
1323 d->xdg_toplevel_icon_manager_v1 = wl_registry_bind(d->registry, id, &xdg_toplevel_icon_manager_v1_interface, 1);
1324 } else if (SDL_strcmp(interface, "frog_color_management_factory_v1") == 0) {
1325 d->frog_color_management_factory_v1 = wl_registry_bind(d->registry, id, &frog_color_management_factory_v1_interface, 1);
1326 } else if (SDL_strcmp(interface, "wp_color_manager_v1") == 0) {
1327 d->wp_color_manager_v1 = wl_registry_bind(d->registry, id, &wp_color_manager_v1_interface, 1);
1328 Wayland_InitColorManager(d);
1329 }
1330}
1331
1332static void display_remove_global(void *data, struct wl_registry *registry, uint32_t id)
1333{
1334 SDL_VideoData *d = data;
1335
1336 // We don't get an interface, just an ID, so assume it's a wl_output :shrug:
1337 for (int i = 0; i < d->output_count; ++i) {
1338 SDL_DisplayData *disp = d->output_list[i];
1339 if (disp->registry_id == id) {
1340 Wayland_free_display(SDL_GetVideoDisplay(disp->display), true);
1341
1342 if (i < d->output_count) {
1343 SDL_memmove(&d->output_list[i], &d->output_list[i + 1], sizeof(SDL_DisplayData *) * (d->output_count - i - 1));
1344 }
1345
1346 d->output_count--;
1347 break;
1348 }
1349 }
1350}
1351
1352static const struct wl_registry_listener registry_listener = {
1353 display_handle_global,
1354 display_remove_global
1355};
1356
1357#ifdef HAVE_LIBDECOR_H
1358static bool should_use_libdecor(SDL_VideoData *data, bool ignore_xdg)
1359{
1360 if (!SDL_WAYLAND_HAVE_WAYLAND_LIBDECOR) {
1361 return false;
1362 }
1363
1364 if (!SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_ALLOW_LIBDECOR, true)) {
1365 return false;
1366 }
1367
1368 if (SDL_GetHintBoolean(SDL_HINT_VIDEO_WAYLAND_PREFER_LIBDECOR, false)) {
1369 return true;
1370 }
1371
1372 if (ignore_xdg) {
1373 return true;
1374 }
1375
1376 if (data->decoration_manager) {
1377 return false;
1378 }
1379
1380 return true;
1381}
1382#endif
1383
1384bool Wayland_LoadLibdecor(SDL_VideoData *data, bool ignore_xdg)
1385{
1386#ifdef HAVE_LIBDECOR_H
1387 if (data->shell.libdecor != NULL) {
1388 return true; // Already loaded!
1389 }
1390 if (should_use_libdecor(data, ignore_xdg)) {
1391 data->shell.libdecor = libdecor_new(data->display, &libdecor_interface);
1392 return data->shell.libdecor != NULL;
1393 }
1394#endif
1395 return false;
1396}
1397
1398bool Wayland_VideoInit(SDL_VideoDevice *_this)
1399{
1400 SDL_VideoData *data = _this->internal;
1401
1402 data->xkb_context = WAYLAND_xkb_context_new(0);
1403 if (!data->xkb_context) {
1404 return SDL_SetError("Failed to create XKB context");
1405 }
1406
1407 data->registry = wl_display_get_registry(data->display);
1408 if (!data->registry) {
1409 return SDL_SetError("Failed to get the Wayland registry");
1410 }
1411
1412 wl_registry_add_listener(data->registry, &registry_listener, data);
1413
1414 // First roundtrip to receive all registry objects.
1415 WAYLAND_wl_display_roundtrip(data->display);
1416
1417 // Require viewports and xdg-output for display scaling.
1418 if (data->scale_to_display_enabled) {
1419 if (!data->viewporter) {
1420 SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "wayland: Display scaling requires the missing 'wp_viewporter' protocol: disabling");
1421 data->scale_to_display_enabled = false;
1422 }
1423 if (!data->xdg_output_manager) {
1424 SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "wayland: Display scaling requires the missing 'zxdg_output_manager_v1' protocol: disabling");
1425 data->scale_to_display_enabled = false;
1426 }
1427 }
1428
1429 // Now that we have all the protocols, load libdecor if applicable
1430 Wayland_LoadLibdecor(data, false);
1431
1432 // Second roundtrip to receive all output events.
1433 WAYLAND_wl_display_roundtrip(data->display);
1434
1435 Wayland_FinalizeDisplays(data);
1436
1437 Wayland_InitMouse();
1438
1439 WAYLAND_wl_display_flush(data->display);
1440
1441 Wayland_InitKeyboard(_this);
1442
1443 if (data->primary_selection_device_manager) {
1444 _this->SetPrimarySelectionText = Wayland_SetPrimarySelectionText;
1445 _this->GetPrimarySelectionText = Wayland_GetPrimarySelectionText;
1446 _this->HasPrimarySelectionText = Wayland_HasPrimarySelectionText;
1447 }
1448
1449 data->initializing = false;
1450
1451 return true;
1452}
1453
1454static bool Wayland_GetDisplayBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect)
1455{
1456 SDL_VideoData *viddata = _this->internal;
1457 SDL_DisplayData *internal = display->internal;
1458 rect->x = internal->x;
1459 rect->y = internal->y;
1460
1461 // When an emulated, exclusive fullscreen window has focus, treat the mode dimensions as the display bounds.
1462 if (display->fullscreen_window &&
1463 display->fullscreen_window->fullscreen_exclusive &&
1464 display->fullscreen_window->internal->active &&
1465 display->fullscreen_window->current_fullscreen_mode.w != 0 &&
1466 display->fullscreen_window->current_fullscreen_mode.h != 0) {
1467 rect->w = display->fullscreen_window->current_fullscreen_mode.w;
1468 rect->h = display->fullscreen_window->current_fullscreen_mode.h;
1469 } else {
1470 if (!viddata->scale_to_display_enabled) {
1471 rect->w = display->current_mode->w;
1472 rect->h = display->current_mode->h;
1473 } else if (internal->transform & WL_OUTPUT_TRANSFORM_90) {
1474 rect->w = internal->pixel_height;
1475 rect->h = internal->pixel_width;
1476 } else {
1477 rect->w = internal->pixel_width;
1478 rect->h = internal->pixel_height;
1479 }
1480 }
1481 return true;
1482}
1483
1484static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
1485{
1486 SDL_VideoData *data = _this->internal;
1487 int i;
1488
1489 Wayland_FiniMouse(data);
1490
1491 for (i = _this->num_displays - 1; i >= 0; --i) {
1492 SDL_VideoDisplay *display = _this->displays[i];
1493 Wayland_free_display(display, false);
1494 }
1495 SDL_free(data->output_list);
1496
1497 Wayland_display_destroy_input(data);
1498
1499 if (data->pointer_constraints) {
1500 zwp_pointer_constraints_v1_destroy(data->pointer_constraints);
1501 data->pointer_constraints = NULL;
1502 }
1503
1504 if (data->relative_pointer_manager) {
1505 zwp_relative_pointer_manager_v1_destroy(data->relative_pointer_manager);
1506 data->relative_pointer_manager = NULL;
1507 }
1508
1509 if (data->activation_manager) {
1510 xdg_activation_v1_destroy(data->activation_manager);
1511 data->activation_manager = NULL;
1512 }
1513
1514 if (data->idle_inhibit_manager) {
1515 zwp_idle_inhibit_manager_v1_destroy(data->idle_inhibit_manager);
1516 data->idle_inhibit_manager = NULL;
1517 }
1518
1519 if (data->key_inhibitor_manager) {
1520 zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(data->key_inhibitor_manager);
1521 data->key_inhibitor_manager = NULL;
1522 }
1523
1524 Wayland_QuitKeyboard(_this);
1525
1526 if (data->text_input_manager) {
1527 zwp_text_input_manager_v3_destroy(data->text_input_manager);
1528 data->text_input_manager = NULL;
1529 }
1530
1531 if (data->xkb_context) {
1532 WAYLAND_xkb_context_unref(data->xkb_context);
1533 data->xkb_context = NULL;
1534 }
1535
1536 if (data->tablet_manager) {
1537 zwp_tablet_manager_v2_destroy((struct zwp_tablet_manager_v2 *)data->tablet_manager);
1538 data->tablet_manager = NULL;
1539 }
1540
1541 if (data->data_device_manager) {
1542 wl_data_device_manager_destroy(data->data_device_manager);
1543 data->data_device_manager = NULL;
1544 }
1545
1546 if (data->shm) {
1547 wl_shm_destroy(data->shm);
1548 data->shm = NULL;
1549 }
1550
1551 if (data->shell.xdg) {
1552 xdg_wm_base_destroy(data->shell.xdg);
1553 data->shell.xdg = NULL;
1554 }
1555
1556 if (data->decoration_manager) {
1557 zxdg_decoration_manager_v1_destroy(data->decoration_manager);
1558 data->decoration_manager = NULL;
1559 }
1560
1561 if (data->xdg_output_manager) {
1562 zxdg_output_manager_v1_destroy(data->xdg_output_manager);
1563 data->xdg_output_manager = NULL;
1564 }
1565
1566 if (data->viewporter) {
1567 wp_viewporter_destroy(data->viewporter);
1568 data->viewporter = NULL;
1569 }
1570
1571 if (data->primary_selection_device_manager) {
1572 zwp_primary_selection_device_manager_v1_destroy(data->primary_selection_device_manager);
1573 data->primary_selection_device_manager = NULL;
1574 }
1575
1576 if (data->fractional_scale_manager) {
1577 wp_fractional_scale_manager_v1_destroy(data->fractional_scale_manager);
1578 data->fractional_scale_manager = NULL;
1579 }
1580
1581 if (data->input_timestamps_manager) {
1582 zwp_input_timestamps_manager_v1_destroy(data->input_timestamps_manager);
1583 data->input_timestamps_manager = NULL;
1584 }
1585
1586 if (data->cursor_shape_manager) {
1587 wp_cursor_shape_manager_v1_destroy(data->cursor_shape_manager);
1588 data->cursor_shape_manager = NULL;
1589 }
1590
1591 if (data->zxdg_exporter_v2) {
1592 zxdg_exporter_v2_destroy(data->zxdg_exporter_v2);
1593 data->zxdg_exporter_v2 = NULL;
1594 }
1595
1596 if (data->xdg_wm_dialog_v1) {
1597 xdg_wm_dialog_v1_destroy(data->xdg_wm_dialog_v1);
1598 data->xdg_wm_dialog_v1 = NULL;
1599 }
1600
1601 if (data->wp_alpha_modifier_v1) {
1602 wp_alpha_modifier_v1_destroy(data->wp_alpha_modifier_v1);
1603 data->wp_alpha_modifier_v1 = NULL;
1604 }
1605
1606 if (data->xdg_toplevel_icon_manager_v1) {
1607 xdg_toplevel_icon_manager_v1_destroy(data->xdg_toplevel_icon_manager_v1);
1608 data->xdg_toplevel_icon_manager_v1 = NULL;
1609 }
1610
1611 if (data->frog_color_management_factory_v1) {
1612 frog_color_management_factory_v1_destroy(data->frog_color_management_factory_v1);
1613 data->frog_color_management_factory_v1 = NULL;
1614 }
1615
1616 if (data->wp_color_manager_v1) {
1617 wp_color_manager_v1_destroy(data->wp_color_manager_v1);
1618 data->wp_color_manager_v1 = NULL;
1619 }
1620
1621 if (data->compositor) {
1622 wl_compositor_destroy(data->compositor);
1623 data->compositor = NULL;
1624 }
1625
1626 if (data->registry) {
1627 wl_registry_destroy(data->registry);
1628 data->registry = NULL;
1629 }
1630}
1631
1632bool Wayland_VideoReconnect(SDL_VideoDevice *_this)
1633{
1634#if 0 // TODO RECONNECT: Uncomment all when https://invent.kde.org/plasma/kwin/-/wikis/Restarting is completed
1635 SDL_VideoData *data = _this->internal;
1636
1637 SDL_Window *window = NULL;
1638
1639 SDL_GLContext current_ctx = SDL_GL_GetCurrentContext();
1640 SDL_Window *current_window = SDL_GL_GetCurrentWindow();
1641
1642 SDL_GL_MakeCurrent(NULL, NULL);
1643 Wayland_VideoCleanup(_this);
1644
1645 SDL_ResetKeyboard();
1646 SDL_ResetMouse();
1647 if (WAYLAND_wl_display_reconnect(data->display) < 0) {
1648 return false;
1649 }
1650
1651 Wayland_VideoInit(_this);
1652
1653 window = _this->windows;
1654 while (window) {
1655 /* We're going to cheat _just_ for a second and strip the OpenGL flag.
1656 * The Wayland driver actually forces it in CreateWindow, and
1657 * RecreateWindow does a bunch of unloading/loading of libGL, so just
1658 * strip the flag so RecreateWindow doesn't mess with the GL context,
1659 * and CreateWindow will add it right back!
1660 * -flibit
1661 */
1662 window->flags &= ~SDL_WINDOW_OPENGL;
1663
1664 SDL_RecreateWindow(window, window->flags);
1665 window = window->next;
1666 }
1667
1668 Wayland_RecreateCursors();
1669
1670 if (current_window && current_ctx) {
1671 SDL_GL_MakeCurrent (current_window, current_ctx);
1672 }
1673 return true;
1674#else
1675 return false;
1676#endif // 0
1677}
1678
1679void Wayland_VideoQuit(SDL_VideoDevice *_this)
1680{
1681 Wayland_VideoCleanup(_this);
1682
1683#ifdef HAVE_LIBDECOR_H
1684 SDL_VideoData *data = _this->internal;
1685 if (data->shell.libdecor) {
1686 libdecor_unref(data->shell.libdecor);
1687 data->shell.libdecor = NULL;
1688 }
1689#endif
1690}
1691
1692#endif // SDL_VIDEO_DRIVER_WAYLAND
1693