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 | #ifdef SDL_VIDEO_DRIVER_X11 |
24 | |
25 | #include <unistd.h> // For getpid() and readlink() |
26 | |
27 | #include "../../core/linux/SDL_system_theme.h" |
28 | #include "../../events/SDL_keyboard_c.h" |
29 | #include "../../events/SDL_mouse_c.h" |
30 | #include "../SDL_pixels_c.h" |
31 | #include "../SDL_sysvideo.h" |
32 | |
33 | #include "SDL_x11framebuffer.h" |
34 | #include "SDL_x11pen.h" |
35 | #include "SDL_x11touch.h" |
36 | #include "SDL_x11video.h" |
37 | #include "SDL_x11xfixes.h" |
38 | #include "SDL_x11xinput2.h" |
39 | #include "SDL_x11messagebox.h" |
40 | #include "SDL_x11shape.h" |
41 | #include "SDL_x11xsync.h" |
42 | #include "SDL_x11xtest.h" |
43 | |
44 | #ifdef SDL_VIDEO_OPENGL_EGL |
45 | #include "SDL_x11opengles.h" |
46 | #endif |
47 | |
48 | // Initialization/Query functions |
49 | static bool X11_VideoInit(SDL_VideoDevice *_this); |
50 | static void X11_VideoQuit(SDL_VideoDevice *_this); |
51 | |
52 | // X11 driver bootstrap functions |
53 | |
54 | static void X11_DeleteDevice(SDL_VideoDevice *device) |
55 | { |
56 | SDL_VideoData *data = device->internal; |
57 | if (device->vulkan_config.loader_handle) { |
58 | device->Vulkan_UnloadLibrary(device); |
59 | } |
60 | if (data->display) { |
61 | X11_XCloseDisplay(data->display); |
62 | } |
63 | if (data->request_display) { |
64 | X11_XCloseDisplay(data->request_display); |
65 | } |
66 | SDL_free(data->windowlist); |
67 | if (device->wakeup_lock) { |
68 | SDL_DestroyMutex(device->wakeup_lock); |
69 | } |
70 | SDL_free(device->internal); |
71 | SDL_free(device); |
72 | |
73 | SDL_X11_UnloadSymbols(); |
74 | } |
75 | |
76 | static bool X11_IsXWayland(Display *d) |
77 | { |
78 | int opcode, event, error; |
79 | return X11_XQueryExtension(d, "XWAYLAND" , &opcode, &event, &error) == True; |
80 | } |
81 | |
82 | static bool X11_CheckCurrentDesktop(const char *name) |
83 | { |
84 | SDL_Environment *env = SDL_GetEnvironment(); |
85 | |
86 | const char *desktopVar = SDL_GetEnvironmentVariable(env, "DESKTOP_SESSION" ); |
87 | if (desktopVar && SDL_strcasecmp(desktopVar, name) == 0) { |
88 | return true; |
89 | } |
90 | |
91 | desktopVar = SDL_GetEnvironmentVariable(env, "XDG_CURRENT_DESKTOP" ); |
92 | if (desktopVar && SDL_strcasestr(desktopVar, name)) { |
93 | return true; |
94 | } |
95 | |
96 | return false; |
97 | } |
98 | |
99 | static SDL_VideoDevice *X11_CreateDevice(void) |
100 | { |
101 | SDL_VideoDevice *device; |
102 | SDL_VideoData *data; |
103 | const char *display = NULL; // Use the DISPLAY environment variable |
104 | Display *x11_display = NULL; |
105 | |
106 | if (!SDL_X11_LoadSymbols()) { |
107 | return NULL; |
108 | } |
109 | |
110 | /* Need for threading gl calls. This is also required for the proprietary |
111 | nVidia driver to be threaded. */ |
112 | X11_XInitThreads(); |
113 | |
114 | // Open the display first to be sure that X11 is available |
115 | x11_display = X11_XOpenDisplay(display); |
116 | |
117 | if (!x11_display) { |
118 | SDL_X11_UnloadSymbols(); |
119 | return NULL; |
120 | } |
121 | |
122 | // Initialize all variables that we clean on shutdown |
123 | device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); |
124 | if (!device) { |
125 | return NULL; |
126 | } |
127 | data = (struct SDL_VideoData *)SDL_calloc(1, sizeof(SDL_VideoData)); |
128 | if (!data) { |
129 | SDL_free(device); |
130 | return NULL; |
131 | } |
132 | device->internal = data; |
133 | |
134 | data->global_mouse_changed = true; |
135 | |
136 | #ifdef SDL_VIDEO_DRIVER_X11_XFIXES |
137 | data->active_cursor_confined_window = NULL; |
138 | #endif // SDL_VIDEO_DRIVER_X11_XFIXES |
139 | |
140 | data->display = x11_display; |
141 | data->request_display = X11_XOpenDisplay(display); |
142 | if (!data->request_display) { |
143 | X11_XCloseDisplay(data->display); |
144 | SDL_free(device->internal); |
145 | SDL_free(device); |
146 | SDL_X11_UnloadSymbols(); |
147 | return NULL; |
148 | } |
149 | |
150 | device->wakeup_lock = SDL_CreateMutex(); |
151 | |
152 | #ifdef X11_DEBUG |
153 | X11_XSynchronize(data->display, True); |
154 | #endif |
155 | |
156 | /* Steam Deck will have an on-screen keyboard, so check their environment |
157 | * variable so we can make use of SDL_StartTextInput. |
158 | */ |
159 | data->is_steam_deck = SDL_GetHintBoolean("SteamDeck" , false); |
160 | |
161 | // Set the function pointers |
162 | device->VideoInit = X11_VideoInit; |
163 | device->VideoQuit = X11_VideoQuit; |
164 | device->ResetTouch = X11_ResetTouch; |
165 | device->GetDisplayModes = X11_GetDisplayModes; |
166 | device->GetDisplayBounds = X11_GetDisplayBounds; |
167 | device->GetDisplayUsableBounds = X11_GetDisplayUsableBounds; |
168 | device->GetWindowICCProfile = X11_GetWindowICCProfile; |
169 | device->SetDisplayMode = X11_SetDisplayMode; |
170 | device->SuspendScreenSaver = X11_SuspendScreenSaver; |
171 | device->PumpEvents = X11_PumpEvents; |
172 | device->WaitEventTimeout = X11_WaitEventTimeout; |
173 | device->SendWakeupEvent = X11_SendWakeupEvent; |
174 | |
175 | device->CreateSDLWindow = X11_CreateWindow; |
176 | device->SetWindowTitle = X11_SetWindowTitle; |
177 | device->SetWindowIcon = X11_SetWindowIcon; |
178 | device->SetWindowPosition = X11_SetWindowPosition; |
179 | device->SetWindowSize = X11_SetWindowSize; |
180 | device->SetWindowMinimumSize = X11_SetWindowMinimumSize; |
181 | device->SetWindowMaximumSize = X11_SetWindowMaximumSize; |
182 | device->SetWindowAspectRatio = X11_SetWindowAspectRatio; |
183 | device->GetWindowBordersSize = X11_GetWindowBordersSize; |
184 | device->SetWindowOpacity = X11_SetWindowOpacity; |
185 | device->SetWindowParent = X11_SetWindowParent; |
186 | device->SetWindowModal = X11_SetWindowModal; |
187 | device->ShowWindow = X11_ShowWindow; |
188 | device->HideWindow = X11_HideWindow; |
189 | device->RaiseWindow = X11_RaiseWindow; |
190 | device->MaximizeWindow = X11_MaximizeWindow; |
191 | device->MinimizeWindow = X11_MinimizeWindow; |
192 | device->RestoreWindow = X11_RestoreWindow; |
193 | device->SetWindowBordered = X11_SetWindowBordered; |
194 | device->SetWindowResizable = X11_SetWindowResizable; |
195 | device->SetWindowAlwaysOnTop = X11_SetWindowAlwaysOnTop; |
196 | device->SetWindowFullscreen = X11_SetWindowFullscreen; |
197 | device->SetWindowMouseGrab = X11_SetWindowMouseGrab; |
198 | device->SetWindowKeyboardGrab = X11_SetWindowKeyboardGrab; |
199 | device->DestroyWindow = X11_DestroyWindow; |
200 | device->CreateWindowFramebuffer = X11_CreateWindowFramebuffer; |
201 | device->UpdateWindowFramebuffer = X11_UpdateWindowFramebuffer; |
202 | device->DestroyWindowFramebuffer = X11_DestroyWindowFramebuffer; |
203 | device->SetWindowHitTest = X11_SetWindowHitTest; |
204 | device->AcceptDragAndDrop = X11_AcceptDragAndDrop; |
205 | device->UpdateWindowShape = X11_UpdateWindowShape; |
206 | device->FlashWindow = X11_FlashWindow; |
207 | device->ShowWindowSystemMenu = X11_ShowWindowSystemMenu; |
208 | device->SetWindowFocusable = X11_SetWindowFocusable; |
209 | device->SyncWindow = X11_SyncWindow; |
210 | |
211 | #ifdef SDL_VIDEO_DRIVER_X11_XFIXES |
212 | device->SetWindowMouseRect = X11_SetWindowMouseRect; |
213 | #endif // SDL_VIDEO_DRIVER_X11_XFIXES |
214 | |
215 | #ifdef SDL_VIDEO_OPENGL_GLX |
216 | device->GL_LoadLibrary = X11_GL_LoadLibrary; |
217 | device->GL_GetProcAddress = X11_GL_GetProcAddress; |
218 | device->GL_UnloadLibrary = X11_GL_UnloadLibrary; |
219 | device->GL_CreateContext = X11_GL_CreateContext; |
220 | device->GL_MakeCurrent = X11_GL_MakeCurrent; |
221 | device->GL_SetSwapInterval = X11_GL_SetSwapInterval; |
222 | device->GL_GetSwapInterval = X11_GL_GetSwapInterval; |
223 | device->GL_SwapWindow = X11_GL_SwapWindow; |
224 | device->GL_DestroyContext = X11_GL_DestroyContext; |
225 | device->GL_GetEGLSurface = NULL; |
226 | #endif |
227 | #ifdef SDL_VIDEO_OPENGL_EGL |
228 | #ifdef SDL_VIDEO_OPENGL_GLX |
229 | if (SDL_GetHintBoolean(SDL_HINT_VIDEO_FORCE_EGL, false)) { |
230 | #endif |
231 | device->GL_LoadLibrary = X11_GLES_LoadLibrary; |
232 | device->GL_GetProcAddress = X11_GLES_GetProcAddress; |
233 | device->GL_UnloadLibrary = X11_GLES_UnloadLibrary; |
234 | device->GL_CreateContext = X11_GLES_CreateContext; |
235 | device->GL_MakeCurrent = X11_GLES_MakeCurrent; |
236 | device->GL_SetSwapInterval = X11_GLES_SetSwapInterval; |
237 | device->GL_GetSwapInterval = X11_GLES_GetSwapInterval; |
238 | device->GL_SwapWindow = X11_GLES_SwapWindow; |
239 | device->GL_DestroyContext = X11_GLES_DestroyContext; |
240 | device->GL_GetEGLSurface = X11_GLES_GetEGLSurface; |
241 | #ifdef SDL_VIDEO_OPENGL_GLX |
242 | } |
243 | #endif |
244 | #endif |
245 | |
246 | device->GetTextMimeTypes = X11_GetTextMimeTypes; |
247 | device->SetClipboardData = X11_SetClipboardData; |
248 | device->GetClipboardData = X11_GetClipboardData; |
249 | device->HasClipboardData = X11_HasClipboardData; |
250 | device->SetPrimarySelectionText = X11_SetPrimarySelectionText; |
251 | device->GetPrimarySelectionText = X11_GetPrimarySelectionText; |
252 | device->HasPrimarySelectionText = X11_HasPrimarySelectionText; |
253 | device->StartTextInput = X11_StartTextInput; |
254 | device->StopTextInput = X11_StopTextInput; |
255 | device->UpdateTextInputArea = X11_UpdateTextInputArea; |
256 | device->HasScreenKeyboardSupport = X11_HasScreenKeyboardSupport; |
257 | device->ShowScreenKeyboard = X11_ShowScreenKeyboard; |
258 | device->HideScreenKeyboard = X11_HideScreenKeyboard; |
259 | device->IsScreenKeyboardShown = X11_IsScreenKeyboardShown; |
260 | |
261 | device->free = X11_DeleteDevice; |
262 | |
263 | #ifdef SDL_VIDEO_VULKAN |
264 | device->Vulkan_LoadLibrary = X11_Vulkan_LoadLibrary; |
265 | device->Vulkan_UnloadLibrary = X11_Vulkan_UnloadLibrary; |
266 | device->Vulkan_GetInstanceExtensions = X11_Vulkan_GetInstanceExtensions; |
267 | device->Vulkan_CreateSurface = X11_Vulkan_CreateSurface; |
268 | device->Vulkan_DestroySurface = X11_Vulkan_DestroySurface; |
269 | device->Vulkan_GetPresentationSupport = X11_Vulkan_GetPresentationSupport; |
270 | #endif |
271 | |
272 | #ifdef SDL_USE_LIBDBUS |
273 | if (SDL_SystemTheme_Init()) |
274 | device->system_theme = SDL_SystemTheme_Get(); |
275 | #endif |
276 | |
277 | device->device_caps = VIDEO_DEVICE_CAPS_HAS_POPUP_WINDOW_SUPPORT; |
278 | |
279 | /* Openbox doesn't send the new window dimensions when entering fullscreen, so the events must be synthesized. |
280 | * This is otherwise not wanted, as it can break fullscreen window positioning on multi-monitor configurations. |
281 | */ |
282 | if (!X11_CheckCurrentDesktop("openbox" )) { |
283 | device->device_caps |= VIDEO_DEVICE_CAPS_SENDS_DISPLAY_CHANGES; |
284 | } |
285 | |
286 | data->is_xwayland = X11_IsXWayland(x11_display); |
287 | if (data->is_xwayland) { |
288 | device->device_caps |= VIDEO_DEVICE_CAPS_MODE_SWITCHING_EMULATED | |
289 | VIDEO_DEVICE_CAPS_DISABLE_MOUSE_WARP_ON_FULLSCREEN_TRANSITIONS; |
290 | } |
291 | |
292 | return device; |
293 | } |
294 | |
295 | VideoBootStrap X11_bootstrap = { |
296 | "x11" , "SDL X11 video driver" , |
297 | X11_CreateDevice, |
298 | X11_ShowMessageBox, |
299 | false |
300 | }; |
301 | |
302 | static int (*handler)(Display *, XErrorEvent *) = NULL; |
303 | static int X11_CheckWindowManagerErrorHandler(Display *d, XErrorEvent *e) |
304 | { |
305 | if (e->error_code == BadWindow) { |
306 | return 0; |
307 | } else { |
308 | return handler(d, e); |
309 | } |
310 | } |
311 | |
312 | static void X11_CheckWindowManager(SDL_VideoDevice *_this) |
313 | { |
314 | SDL_VideoData *data = _this->internal; |
315 | Display *display = data->display; |
316 | Atom _NET_SUPPORTING_WM_CHECK; |
317 | int status, real_format; |
318 | Atom real_type; |
319 | unsigned long items_read = 0, items_left = 0; |
320 | unsigned char *propdata = NULL; |
321 | Window wm_window = 0; |
322 | #ifdef DEBUG_WINDOW_MANAGER |
323 | char *wm_name; |
324 | #endif |
325 | |
326 | // Set up a handler to gracefully catch errors |
327 | X11_XSync(display, False); |
328 | handler = X11_XSetErrorHandler(X11_CheckWindowManagerErrorHandler); |
329 | |
330 | _NET_SUPPORTING_WM_CHECK = X11_XInternAtom(display, "_NET_SUPPORTING_WM_CHECK" , False); |
331 | status = X11_XGetWindowProperty(display, DefaultRootWindow(display), _NET_SUPPORTING_WM_CHECK, 0L, 1L, False, XA_WINDOW, &real_type, &real_format, &items_read, &items_left, &propdata); |
332 | if (status == Success) { |
333 | if (items_read) { |
334 | wm_window = ((Window *)propdata)[0]; |
335 | } |
336 | if (propdata) { |
337 | X11_XFree(propdata); |
338 | propdata = NULL; |
339 | } |
340 | } |
341 | |
342 | if (wm_window) { |
343 | status = X11_XGetWindowProperty(display, wm_window, _NET_SUPPORTING_WM_CHECK, 0L, 1L, False, XA_WINDOW, &real_type, &real_format, &items_read, &items_left, &propdata); |
344 | if (status != Success || !items_read || wm_window != ((Window *)propdata)[0]) { |
345 | wm_window = None; |
346 | } |
347 | if (status == Success && propdata) { |
348 | X11_XFree(propdata); |
349 | propdata = NULL; |
350 | } |
351 | } |
352 | |
353 | // Reset the error handler, we're done checking |
354 | X11_XSync(display, False); |
355 | X11_XSetErrorHandler(handler); |
356 | |
357 | if (!wm_window) { |
358 | #ifdef DEBUG_WINDOW_MANAGER |
359 | printf("Couldn't get _NET_SUPPORTING_WM_CHECK property\n" ); |
360 | #endif |
361 | return; |
362 | } |
363 | data->net_wm = true; |
364 | |
365 | #ifdef DEBUG_WINDOW_MANAGER |
366 | wm_name = X11_GetWindowTitle(_this, wm_window); |
367 | printf("Window manager: %s\n" , wm_name); |
368 | SDL_free(wm_name); |
369 | #endif |
370 | } |
371 | |
372 | static bool X11_VideoInit(SDL_VideoDevice *_this) |
373 | { |
374 | SDL_VideoData *data = _this->internal; |
375 | |
376 | // Get the process PID to be associated to the window |
377 | data->pid = getpid(); |
378 | |
379 | // I have no idea how random this actually is, or has to be. |
380 | data->window_group = (XID)(((size_t)data->pid) ^ ((size_t)_this)); |
381 | |
382 | // Look up some useful Atoms |
383 | #define GET_ATOM(X) data->atoms.X = X11_XInternAtom(data->display, #X, False) |
384 | GET_ATOM(WM_PROTOCOLS); |
385 | GET_ATOM(WM_DELETE_WINDOW); |
386 | GET_ATOM(WM_TAKE_FOCUS); |
387 | GET_ATOM(WM_NAME); |
388 | GET_ATOM(WM_TRANSIENT_FOR); |
389 | GET_ATOM(_NET_WM_STATE); |
390 | GET_ATOM(_NET_WM_STATE_HIDDEN); |
391 | GET_ATOM(_NET_WM_STATE_FOCUSED); |
392 | GET_ATOM(_NET_WM_STATE_MAXIMIZED_VERT); |
393 | GET_ATOM(_NET_WM_STATE_MAXIMIZED_HORZ); |
394 | GET_ATOM(_NET_WM_STATE_FULLSCREEN); |
395 | GET_ATOM(_NET_WM_STATE_ABOVE); |
396 | GET_ATOM(_NET_WM_STATE_SKIP_TASKBAR); |
397 | GET_ATOM(_NET_WM_STATE_SKIP_PAGER); |
398 | GET_ATOM(_NET_WM_MOVERESIZE); |
399 | GET_ATOM(_NET_WM_STATE_MODAL); |
400 | GET_ATOM(_NET_WM_ALLOWED_ACTIONS); |
401 | GET_ATOM(_NET_WM_ACTION_FULLSCREEN); |
402 | GET_ATOM(_NET_WM_NAME); |
403 | GET_ATOM(_NET_WM_ICON_NAME); |
404 | GET_ATOM(_NET_WM_ICON); |
405 | GET_ATOM(_NET_WM_PING); |
406 | GET_ATOM(_NET_WM_SYNC_REQUEST); |
407 | GET_ATOM(_NET_WM_SYNC_REQUEST_COUNTER); |
408 | GET_ATOM(_NET_WM_WINDOW_OPACITY); |
409 | GET_ATOM(_NET_WM_USER_TIME); |
410 | GET_ATOM(_NET_ACTIVE_WINDOW); |
411 | GET_ATOM(_NET_FRAME_EXTENTS); |
412 | GET_ATOM(_SDL_WAKEUP); |
413 | GET_ATOM(UTF8_STRING); |
414 | GET_ATOM(PRIMARY); |
415 | GET_ATOM(CLIPBOARD); |
416 | GET_ATOM(INCR); |
417 | GET_ATOM(SDL_SELECTION); |
418 | GET_ATOM(TARGETS); |
419 | GET_ATOM(SDL_FORMATS); |
420 | GET_ATOM(XdndAware); |
421 | GET_ATOM(XdndEnter); |
422 | GET_ATOM(XdndLeave); |
423 | GET_ATOM(XdndPosition); |
424 | GET_ATOM(XdndStatus); |
425 | GET_ATOM(XdndTypeList); |
426 | GET_ATOM(XdndActionCopy); |
427 | GET_ATOM(XdndDrop); |
428 | GET_ATOM(XdndFinished); |
429 | GET_ATOM(XdndSelection); |
430 | GET_ATOM(XKLAVIER_STATE); |
431 | |
432 | // Detect the window manager |
433 | X11_CheckWindowManager(_this); |
434 | |
435 | if (!X11_InitModes(_this)) { |
436 | return false; |
437 | } |
438 | |
439 | if (!X11_InitXinput2(_this)) { |
440 | // Assume a mouse and keyboard are attached |
441 | SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL, false); |
442 | SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL, false); |
443 | } |
444 | |
445 | #ifdef SDL_VIDEO_DRIVER_X11_XFIXES |
446 | X11_InitXfixes(_this); |
447 | #endif |
448 | |
449 | X11_InitXsettings(_this); |
450 | |
451 | #ifdef SDL_VIDEO_DRIVER_X11_XSYNC |
452 | X11_InitXsync(_this); |
453 | #endif |
454 | |
455 | #ifdef SDL_VIDEO_DRIVER_X11_XTEST |
456 | X11_InitXTest(_this); |
457 | #endif |
458 | |
459 | #ifndef X_HAVE_UTF8_STRING |
460 | #warning X server does not support UTF8_STRING, a feature introduced in 2000! This is likely to become a hard error in a future libSDL3. |
461 | #endif |
462 | |
463 | if (!X11_InitKeyboard(_this)) { |
464 | return false; |
465 | } |
466 | X11_InitMouse(_this); |
467 | |
468 | X11_InitTouch(_this); |
469 | |
470 | X11_InitPen(_this); |
471 | |
472 | return true; |
473 | } |
474 | |
475 | void X11_VideoQuit(SDL_VideoDevice *_this) |
476 | { |
477 | SDL_VideoData *data = _this->internal; |
478 | |
479 | if (data->clipboard_window) { |
480 | X11_XDestroyWindow(data->display, data->clipboard_window); |
481 | } |
482 | |
483 | if (data->xsettings_window) { |
484 | X11_XDestroyWindow(data->display, data->xsettings_window); |
485 | } |
486 | |
487 | #ifdef X_HAVE_UTF8_STRING |
488 | if (data->im) { |
489 | X11_XCloseIM(data->im); |
490 | } |
491 | #endif |
492 | |
493 | X11_QuitModes(_this); |
494 | X11_QuitKeyboard(_this); |
495 | X11_QuitMouse(_this); |
496 | X11_QuitTouch(_this); |
497 | X11_QuitPen(_this); |
498 | X11_QuitClipboard(_this); |
499 | X11_QuitXsettings(_this); |
500 | } |
501 | |
502 | bool X11_UseDirectColorVisuals(void) |
503 | { |
504 | if (SDL_GetHintBoolean(SDL_HINT_VIDEO_X11_NODIRECTCOLOR, false)) { |
505 | return false; |
506 | } |
507 | return true; |
508 | } |
509 | |
510 | #endif // SDL_VIDEO_DRIVER_X11 |
511 | |