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#include "SDL_internal.h"
22
23#if defined(SDL_VIDEO_DRIVER_WAYLAND) && defined(SDL_VIDEO_OPENGL_EGL)
24
25#include "../../core/unix/SDL_poll.h"
26#include "../SDL_sysvideo.h"
27#include "../../events/SDL_windowevents_c.h"
28#include "SDL_waylandvideo.h"
29#include "SDL_waylandopengles.h"
30#include "SDL_waylandwindow.h"
31#include "SDL_waylandevents_c.h"
32
33#include "xdg-shell-client-protocol.h"
34
35// EGL implementation of SDL OpenGL ES support
36
37bool Wayland_GLES_LoadLibrary(SDL_VideoDevice *_this, const char *path)
38{
39 bool result;
40 SDL_VideoData *data = _this->internal;
41
42 result = SDL_EGL_LoadLibrary(_this, path, (NativeDisplayType)data->display, _this->gl_config.egl_platform);
43
44 Wayland_PumpEvents(_this);
45 WAYLAND_wl_display_flush(data->display);
46
47 return result;
48}
49
50SDL_GLContext Wayland_GLES_CreateContext(SDL_VideoDevice *_this, SDL_Window *window)
51{
52 SDL_GLContext context;
53 context = SDL_EGL_CreateContext(_this, window->internal->egl_surface);
54 WAYLAND_wl_display_flush(_this->internal->display);
55
56 return context;
57}
58
59/* Wayland wants to tell you when to provide new frames, and if you have a non-zero
60 swap interval, Mesa will block until a callback tells it to do so. On some
61 compositors, they might decide that a minimized window _never_ gets a callback,
62 which causes apps to hang during swapping forever. So we always set the official
63 eglSwapInterval to zero to avoid blocking inside EGL, and manage this ourselves.
64 If a swap blocks for too long waiting on a callback, we just go on, under the
65 assumption the frame will be wasted, but this is better than freezing the app.
66 I frown upon platforms that dictate this sort of control inversion (the callback
67 is intended for _rendering_, not stalling until vsync), but we can work around
68 this for now. --ryan. */
69/* Addendum: several recent APIs demand this sort of control inversion: Emscripten,
70 libretro, Wayland, probably others...it feels like we're eventually going to have
71 to give in with a future SDL API revision, since we can bend the other APIs to
72 this style, but this style is much harder to bend the other way. :/ */
73bool Wayland_GLES_SetSwapInterval(SDL_VideoDevice *_this, int interval)
74{
75 if (!_this->egl_data) {
76 return SDL_SetError("EGL not initialized");
77 }
78
79 /* technically, this is _all_ adaptive vsync (-1), because we can't
80 actually wait for the _next_ vsync if you set 1, but things that
81 request 1 probably won't care _that_ much. I hope. No matter what
82 you do, though, you never see tearing on Wayland. */
83 if (interval > 1) {
84 interval = 1;
85 } else if (interval < -1) {
86 interval = -1;
87 }
88
89 // !!! FIXME: technically, this should be per-context, right?
90 _this->egl_data->egl_swapinterval = interval;
91 _this->egl_data->eglSwapInterval(_this->egl_data->egl_display, 0);
92 return true;
93}
94
95bool Wayland_GLES_GetSwapInterval(SDL_VideoDevice *_this, int *interval)
96{
97 if (!_this->egl_data) {
98 return SDL_SetError("EGL not initialized");
99 }
100
101 *interval =_this->egl_data->egl_swapinterval;
102 return true;
103}
104
105bool Wayland_GLES_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window)
106{
107 SDL_WindowData *data = window->internal;
108 const int swap_interval = _this->egl_data->egl_swapinterval;
109
110 /* For windows that we know are hidden, skip swaps entirely, if we don't do
111 * this compositors will intentionally stall us indefinitely and there's no
112 * way for an end user to show the window, unlike other situations (i.e.
113 * the window is minimized, behind another window, etc.).
114 *
115 * FIXME: Request EGL_WAYLAND_swap_buffers_with_timeout.
116 * -flibit
117 */
118 if (data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_SHOWN &&
119 data->shell_surface_status != WAYLAND_SHELL_SURFACE_STATUS_WAITING_FOR_FRAME) {
120 return true;
121 }
122
123 /* By default, we wait for the Wayland frame callback and then issue the pageflip (eglSwapBuffers),
124 * but if we want low latency (double buffer scheme), we issue the pageflip and then wait
125 * immediately for the Wayland frame callback.
126 */
127 if (data->double_buffer) {
128 // Feed the frame to Wayland. This will set it so the wl_surface_frame callback can fire again.
129 if (!_this->egl_data->eglSwapBuffers(_this->egl_data->egl_display, data->egl_surface)) {
130 return SDL_EGL_SetError("unable to show color buffer in an OS-native window", "eglSwapBuffers");
131 }
132
133 WAYLAND_wl_display_flush(data->waylandData->display);
134 }
135
136 // Control swap interval ourselves. See comments on Wayland_GLES_SetSwapInterval
137 if (swap_interval != 0 && data->shell_surface_status == WAYLAND_SHELL_SURFACE_STATUS_SHOWN) {
138 SDL_VideoData *videodata = _this->internal;
139 struct wl_display *display = videodata->display;
140 // 20hz, so we'll progress even if throttled to zero.
141 const Uint64 max_wait = SDL_GetTicksNS() + (SDL_NS_PER_SECOND / 20);
142 while (SDL_GetAtomicInt(&data->swap_interval_ready) == 0) {
143 Uint64 now;
144
145 WAYLAND_wl_display_flush(display);
146
147 /* wl_display_prepare_read_queue() will return false if the event queue is not empty.
148 * If the event queue is empty, it will prepare us for our SDL_IOReady() call. */
149 if (WAYLAND_wl_display_prepare_read_queue(display, data->gles_swap_frame_event_queue) != 0) {
150 // We have some pending events. Check if the frame callback happened.
151 WAYLAND_wl_display_dispatch_queue_pending(display, data->gles_swap_frame_event_queue);
152 continue;
153 }
154
155 // Beyond this point, we must either call wl_display_cancel_read() or wl_display_read_events()
156
157 now = SDL_GetTicksNS();
158 if (now >= max_wait) {
159 // Timeout expired. Cancel the read.
160 WAYLAND_wl_display_cancel_read(display);
161 break;
162 }
163
164 if (SDL_IOReady(WAYLAND_wl_display_get_fd(display), SDL_IOR_READ, max_wait - now) <= 0) {
165 // Error or timeout expired without any events for us. Cancel the read.
166 WAYLAND_wl_display_cancel_read(display);
167 break;
168 }
169
170 // We have events. Read and dispatch them.
171 WAYLAND_wl_display_read_events(display);
172 WAYLAND_wl_display_dispatch_queue_pending(display, data->gles_swap_frame_event_queue);
173 }
174 SDL_SetAtomicInt(&data->swap_interval_ready, 0);
175 }
176
177 if (!data->double_buffer) {
178 // Feed the frame to Wayland. This will set it so the wl_surface_frame callback can fire again.
179 if (!_this->egl_data->eglSwapBuffers(_this->egl_data->egl_display, data->egl_surface)) {
180 return SDL_EGL_SetError("unable to show color buffer in an OS-native window", "eglSwapBuffers");
181 }
182
183 WAYLAND_wl_display_flush(data->waylandData->display);
184 }
185
186 return true;
187}
188
189bool Wayland_GLES_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context)
190{
191 bool result;
192
193 if (window && context) {
194 result = SDL_EGL_MakeCurrent(_this, window->internal->egl_surface, context);
195 } else {
196 result = SDL_EGL_MakeCurrent(_this, NULL, NULL);
197 }
198
199 WAYLAND_wl_display_flush(_this->internal->display);
200
201 _this->egl_data->eglSwapInterval(_this->egl_data->egl_display, 0); // see comments on Wayland_GLES_SetSwapInterval.
202
203 return result;
204}
205
206bool Wayland_GLES_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context)
207{
208 bool result = SDL_EGL_DestroyContext(_this, context);
209 WAYLAND_wl_display_flush(_this->internal->display);
210 return result;
211}
212
213EGLSurface Wayland_GLES_GetEGLSurface(SDL_VideoDevice *_this, SDL_Window *window)
214{
215 SDL_WindowData *windowdata = window->internal;
216
217 return windowdata->egl_surface;
218}
219
220#endif // SDL_VIDEO_DRIVER_WAYLAND && SDL_VIDEO_OPENGL_EGL
221