1 module gfx.window.xcb;
2 
3 version(linux):
4 
5 import gfx.core.log : LogTag;
6 import gfx.graal : Instance;
7 import gfx.window;
8 import xcb.xcb;
9 
10 enum gfxXcbLogMask = 0x0800_0000;
11 package immutable gfxXcbLog = LogTag("GFX-XCB");
12 
13 /// List of X atoms that are fetched automatically
14 enum Atom
15 {
16     UTF8_STRING,
17 
18     WM_PROTOCOLS,
19     WM_DELETE_WINDOW,
20     WM_TRANSIENT_FOR,
21     WM_CHANGE_STATE,
22     WM_STATE,
23     _NET_WM_STATE,
24     _NET_WM_STATE_MODAL,
25     _NET_WM_STATE_STICKY,
26     _NET_WM_STATE_MAXIMIZED_VERT,
27     _NET_WM_STATE_MAXIMIZED_HORZ,
28     _NET_WM_STATE_SHADED,
29     _NET_WM_STATE_SKIP_TASKBAR,
30     _NET_WM_STATE_SKIP_PAGER,
31     _NET_WM_STATE_HIDDEN,
32     _NET_WM_STATE_FULLSCREEN,
33     _NET_WM_STATE_ABOVE,
34     _NET_WM_STATE_BELOW,
35     _NET_WM_STATE_DEMANDS_ATTENTION,
36     _NET_WM_STATE_FOCUSED,
37     _NET_WM_NAME,
38 }
39 
40 /// get the response_type field masked for
41 @property ubyte xcbEventType(EvT)(EvT* e)
42 {
43     return (e.response_type & ~0x80);
44 }
45 
46 class XcbDisplay : Display
47 {
48     import gfx.core.rc : atomicRcCode, Rc;
49     import gfx.graal : Backend;
50     import X11.Xlib : XDisplay = Display;
51 
52     mixin(atomicRcCode);
53 
54     private XDisplay *_dpy;
55     private xcb_connection_t* _conn;
56     private xcb_atom_t[Atom] _atoms;
57     private int _mainScreenNum;
58     private xcb_screen_t*[] _screens;
59 
60     private Rc!Instance _instance;
61 
62     private Window[] _windows;
63     private XcbWindow[] _xcbWindows;
64 
65     this(in Backend[] loadOrder)
66     {
67         import std.exception : enforce;
68         import X11.Xlib : XCloseDisplay, XDefaultScreen, XOpenDisplay;
69         import X11.Xlib_xcb : XGetXCBConnection, XSetEventQueueOwner, XCBOwnsEventQueue;
70 
71         gfxXcbWndLog.trace("opening X display");
72         _dpy = enforce(XOpenDisplay(null));
73         scope(failure) {
74             XCloseDisplay(_dpy);
75         }
76         _conn = enforce(XGetXCBConnection(_dpy));
77         XSetEventQueueOwner(_dpy, XCBOwnsEventQueue);
78         _mainScreenNum = XDefaultScreen(_dpy);
79 
80         initializeAtoms();
81         initializeScreens();
82         initializeInstance(loadOrder);
83     }
84 
85     override void dispose()
86     {
87         import X11.Xlib : XCloseDisplay;
88 
89         if (_windows.length) {
90             auto ws = _windows.dup;
91             foreach (w; ws) w.close();
92         }
93         assert(!_windows.length);
94 
95         _instance.unload();
96         xXcbWndLog.trace("closing X display");
97         XCloseDisplay(_dpy);
98     }
99 
100     private void initializeAtoms()
101     {
102         import core.stdc.stdlib : free;
103         import std.conv : to;
104         import std.string : toStringz;
105         import std.traits : EnumMembers;
106 
107         xcb_intern_atom_cookie_t[] cookies;
108         cookies.reserve(EnumMembers!Atom.length);
109 
110         foreach (immutable atom; EnumMembers!Atom) // static foreach
111         {
112             auto name = atom.to!string;
113             cookies ~= xcb_intern_atom(_conn, 1,
114                     cast(ushort)name.length, toStringz(name));
115         }
116 
117         foreach (i, immutable atom; EnumMembers!Atom) // static foreach
118         {
119             immutable name = atom.to!string;
120             xcb_generic_error_t* err;
121             auto reply = xcb_intern_atom_reply(_conn, cookies[i], &err);
122             if (err)
123             {
124                 throw new Exception("failed initializing atom " ~ name ~ ": ",
125                         (*err).to!string);
126             }
127             if (reply.atom == XCB_ATOM_NONE)
128             {
129                 throw new Exception("could not retrieve atom " ~ name);
130             }
131             _atoms[atom] = reply.atom;
132             free(reply);
133         }
134     }
135 
136     private void initializeScreens()
137     {
138         xcb_screen_iterator_t iter;
139         for (iter = xcb_setup_roots_iterator(xcb_get_setup(_conn)); iter.rem;
140                 xcb_screen_next(&iter))
141         {
142             _screens ~= iter.data;
143         }
144     }
145 
146     private void initializeInstance(in Backend[] loadOrder)
147     {
148         assert(!_instance);
149 
150         foreach (b; loadOrder) {
151             final switch (b) {
152             case Backend.vulkan:
153                 try {
154                     gfxXcbWndLog.trace("Attempting to instantiate Vulkan");
155                     import gfx.vulkan : createVulkanInstance, vulkanInit;
156                     vulkanInit();
157                     _instance = createVulkanInstance();
158                     gfxXcbWndLog.info("Creating a Vulkan instance");
159                 }
160                 catch (Exception ex) {
161                     warningf("Vulkan is not available. %s", ex.msg);
162                 }
163                 break;
164             case Backend.gl3:
165                 try {
166                     import gfx.core.rc : makeRc;
167                     import gfx.gl3 : GlInstance;
168                     import gfx.gl3.context : GlAttribs;
169                     import gfx.window.xcb.context : XcbGlContext;
170                     gfxXcbWndLog.trace("Attempting to instantiate OpenGL");
171                     auto w = new XcbWindow(this, null, true);
172                     w.show(10, 10);
173                     scope(exit) w.close();
174                     auto ctx = makeRc!XcbGlContext(_dpy, _mainScreenNum, GlAttribs.init, w._win);
175                     gfxXcbWndLog.trace("Creating an OpenGL instance");
176                     _instance = new GlInstance(ctx);
177                 }
178                 catch (Exception ex) {
179                     gfxXcbWndLog.warningf("OpenGL is not available. %s", ex.msg);
180                 }
181                 break;
182             }
183             if (_instance) break;
184         }
185 
186         if (!_instance) {
187             throw new Exception("Could not instantiate a backend");
188         }
189     }
190 
191     override @property Instance instance() {
192         return _instance;
193     }
194 
195     override @property Window[] windows()
196     {
197         return _windows;
198     }
199 
200     override Window createWindow()
201     {
202         return new XcbWindow(this, _instance, false);
203     }
204 
205     override void pollAndDispatch()
206     {
207         while (true) {
208             auto e = xcb_poll_for_event(_conn);
209             if (!e) break;
210             handleEvent(e);
211         }
212     }
213 
214     private XcbWindow xcbWindow(xcb_window_t win) {
215         foreach(w; _xcbWindows) {
216             if (w._win == win) return w;
217         }
218         return null;
219     }
220 
221     void registerWindow(XcbWindow window) {
222         _windows ~= window;
223         _xcbWindows ~= window;
224     }
225 
226     void unregisterWindow(XcbWindow window) {
227         import std.algorithm : remove;
228         _windows = _windows.remove!(w => w is window);
229         _xcbWindows = _xcbWindows.remove!(w => w is window);
230     }
231 
232     private @property int mainScreenNum()
233     {
234         return _mainScreenNum;
235     }
236 
237     private @property xcb_screen_t* mainScreen()
238     {
239         return _screens[_mainScreenNum];
240     }
241 
242     private void handleEvent(xcb_generic_event_t* e)
243     {
244         immutable xcbType = xcbEventType(e);
245 
246         switch (xcbType)
247         {
248         case XCB_KEY_PRESS:
249             auto ev = cast(xcb_key_press_event_t*)e;
250             auto xcbWin = xcbWindow(ev.event);
251             if (xcbWin && xcbWin._onKeyOnHandler)
252                 xcbWin._onKeyOnHandler(ev.detail);
253             break;
254         case XCB_KEY_RELEASE:
255             auto ev = cast(xcb_key_press_event_t*)e;
256             auto xcbWin = xcbWindow(ev.event);
257             if (xcbWin && xcbWin._onKeyOffHandler)
258                 xcbWin._onKeyOffHandler(ev.detail);
259             break;
260         case XCB_BUTTON_PRESS:
261             auto ev = cast(xcb_button_press_event_t*)e;
262             auto xcbWin = xcbWindow(ev.event);
263             if (xcbWin && xcbWin._onHandler)
264                 xcbWin._onHandler(ev.event_x, ev.event_y);
265             break;
266         case XCB_BUTTON_RELEASE:
267             auto ev = cast(xcb_button_press_event_t*)e;
268             auto xcbWin = xcbWindow(ev.event);
269             if (xcbWin && xcbWin._offHandler)
270                 xcbWin._offHandler(ev.event_x, ev.event_y);
271             break;
272         case XCB_MOTION_NOTIFY:
273             auto ev = cast(xcb_motion_notify_event_t*)e;
274             auto xcbWin = xcbWindow(ev.event);
275             if (xcbWin && xcbWin._moveHandler)
276                 xcbWin._moveHandler(ev.event_x, ev.event_y);
277             break;
278         case XCB_CONFIGURE_NOTIFY:
279             break;
280         case XCB_PROPERTY_NOTIFY:
281             break;
282         case XCB_CLIENT_MESSAGE:
283             auto ev = cast(xcb_client_message_event_t*)e;
284             if (ev.data.data32[0] == atom(Atom.WM_DELETE_WINDOW)) {
285                 auto win = xcbWindow(ev.window);
286                 if (win._onCloseHandler) {
287                     win._closeFlag = win._onCloseHandler();
288                 }
289                 else {
290                     win._closeFlag = true;
291                 }
292             }
293             break;
294         default:
295             break;
296         }
297     }
298 
299     private xcb_atom_t atom(Atom atom) const
300     {
301         auto at = (atom in _atoms);
302         if (at)
303             return *at;
304         return XCB_ATOM_NONE;
305     }
306 }
307 
308 class XcbWindow : Window
309 {
310     import gfx.graal.presentation : Surface;
311 
312     private XcbDisplay _dpy;
313     private Instance _instance;
314     private xcb_window_t _win;
315     private Surface _surface;
316     private MouseHandler _moveHandler;
317     private MouseHandler _onHandler;
318     private MouseHandler _offHandler;
319     private KeyHandler _onKeyOnHandler;
320     private KeyHandler _onKeyOffHandler;
321     private CloseHandler _onCloseHandler;
322     private bool _closeFlag;
323     private bool _dummy;
324 
325     this(XcbDisplay dpy, Instance instance, bool dummy)
326     {
327         assert(dpy && (dummy || instance));
328         _dpy = dpy;
329         _instance = instance;
330         _dummy = dummy;
331     }
332 
333     override @property void onMouseMove(MouseHandler handler) {
334         _moveHandler = handler;
335     }
336     override @property void onMouseOn(MouseHandler handler) {
337         _onHandler = handler;
338     }
339     override @property void onMouseOff(MouseHandler handler) {
340         _offHandler = handler;
341     }
342     override @property void onKeyOn(KeyHandler handler) {
343         _onKeyOnHandler = handler;
344     }
345     override @property void onKeyOff(KeyHandler handler) {
346         _onKeyOffHandler = handler;
347     }
348     override @property void onClose(CloseHandler handler) {
349         _onCloseHandler = handler;
350     }
351 
352     override @property Surface surface() {
353         return _surface;
354     }
355 
356     override @property bool closeFlag() const {
357         return _closeFlag;
358     }
359 
360     override @property void closeFlag(in bool flag) {
361         _closeFlag = flag;
362     }
363 
364     override void show(uint width, uint height)
365     {
366         const screen = _dpy.mainScreen;
367 
368         const cmap = xcb_generate_id(_dpy._conn);
369         _win = xcb_generate_id(_dpy._conn);
370 
371         auto visual = drawArgbVisual(screen);
372         const depth = drawVisualDepth(screen, visual.visual_id);
373 
374         xcb_create_colormap(_dpy._conn, XCB_COLORMAP_ALLOC_NONE, cmap, screen.root, visual.visual_id);
375 
376         immutable mask = XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | XCB_CW_COLORMAP;
377         const uint[] values = [XCB_BACK_PIXMAP_NONE, 0, cmap, 0];
378 
379         auto cookie = xcb_create_window_checked(_dpy._conn, depth,
380                 _win, screen.root, 50, 50, cast(ushort) width, cast(ushort) height, 0,
381                 XCB_WINDOW_CLASS_INPUT_OUTPUT, visual.visual_id, mask, &values[0]);
382 
383         auto err = xcb_request_check(_dpy._conn, cookie);
384         if (err) {
385             import std.format : format;
386             throw new Exception(format("GFX-XCB: could not create window: %s", err.error_code));
387         }
388 
389         // register regular events
390         {
391             const uint[] attrs = [
392                 XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS
393                 | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_ENTER_WINDOW
394                 | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_POINTER_MOTION
395                 | XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_EXPOSURE
396                 | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE, 0
397             ];
398             xcb_change_window_attributes(_dpy._conn, _win,
399                     XCB_CW_EVENT_MASK, &attrs[0]);
400         }
401         // register window close event
402         {
403             const xcb_atom_t[] props = [atom(Atom.WM_DELETE_WINDOW), 0];
404             xcb_change_property(_dpy._conn, XCB_PROP_MODE_REPLACE, _win,
405                     atom(Atom.WM_PROTOCOLS), XCB_ATOM_ATOM, 32, 1, &props[0]);
406         }
407 
408         if (_dummy) {
409             xcb_flush(_dpy._conn);
410             return;
411         }
412 
413         _dpy.registerWindow(this);
414 
415         xcb_map_window(_dpy._conn, _win);
416         xcb_flush(_dpy._conn);
417 
418         import gfx.graal : Backend;
419         final switch (_instance.backend) {
420         case Backend.vulkan:
421             import gfx.vulkan.wsi : createVulkanXcbSurface;
422             _surface = createVulkanXcbSurface(_instance, _dpy._conn, _win);
423             break;
424         case Backend.gl3:
425             import gfx.gl3 : GlInstance;
426             import gfx.gl3.swapchain : GlSurface;
427             _surface = new GlSurface(_win);
428             auto glInst = cast(GlInstance)_instance;
429             auto ctx = glInst.ctx;
430             ctx.makeCurrent(_win);
431             break;
432         }
433     }
434 
435     override void close()
436     {
437         if (_dummy) {
438             if (_win) {
439                 xcb_destroy_window(_dpy._conn, _win);
440                 _win = 0;
441             }
442         }
443         else {
444             if (_win) {
445                 xcb_unmap_window(_dpy._conn, _win);
446                 xcb_destroy_window(_dpy._conn, _win);
447                 xcb_flush(_dpy._conn);
448                 _win = 0;
449             }
450             _dpy.unregisterWindow(this);
451         }
452     }
453 
454     private xcb_atom_t atom(Atom atom) const
455     {
456         return _dpy.atom(atom);
457     }
458 }
459 
460 xcb_visualtype_t *drawArgbVisual(const xcb_screen_t *s)
461 {
462     xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s);
463 
464     if(depth_iter.data) {
465         for(; depth_iter.rem; xcb_depth_next (&depth_iter)) {
466             if(depth_iter.data.depth == 32) {
467                 for(xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
468                     visual_iter.rem; xcb_visualtype_next (&visual_iter))
469                 {
470                     if (visual_iter.data && visual_iter.data.class_ == XCB_VISUAL_CLASS_TRUE_COLOR)
471                         return visual_iter.data;
472                 }
473             }
474         }
475     }
476 
477     throw new Exception("could not find a draw visual");
478 }
479 
480 ubyte drawVisualDepth(const xcb_screen_t *s, xcb_visualid_t vis)
481 {
482     xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s);
483 
484     if(depth_iter.data) {
485         for(; depth_iter.rem; xcb_depth_next (&depth_iter)) {
486             for(xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
487                 visual_iter.rem; xcb_visualtype_next (&visual_iter))
488             {
489                 if(vis == visual_iter.data.visual_id)
490                     return depth_iter.data.depth;
491             }
492         }
493     }
494     throw new Exception("could not find a visuals depth");
495 }