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 gfx.window.xkeyboard;
9 import xcb.xcb;
10 import xcb.xkb;
11 
12 enum gfxXcbLogMask = 0x0800_0000;
13 package immutable gfxXcbLog = LogTag("GFX-XCB");
14 
15 /// List of X atoms that are fetched automatically
16 enum Atom
17 {
18     UTF8_STRING,
19 
20     WM_PROTOCOLS,
21     WM_DELETE_WINDOW,
22     WM_TRANSIENT_FOR,
23     WM_CHANGE_STATE,
24     WM_STATE,
25     _NET_WM_STATE,
26     _NET_WM_STATE_MODAL,
27     _NET_WM_STATE_STICKY,
28     _NET_WM_STATE_MAXIMIZED_VERT,
29     _NET_WM_STATE_MAXIMIZED_HORZ,
30     _NET_WM_STATE_SHADED,
31     _NET_WM_STATE_SKIP_TASKBAR,
32     _NET_WM_STATE_SKIP_PAGER,
33     _NET_WM_STATE_HIDDEN,
34     _NET_WM_STATE_FULLSCREEN,
35     _NET_WM_STATE_ABOVE,
36     _NET_WM_STATE_BELOW,
37     _NET_WM_STATE_DEMANDS_ATTENTION,
38     _NET_WM_STATE_FOCUSED,
39     _NET_WM_NAME,
40 }
41 
42 /// get the response_type field masked for
43 @property ubyte xcbEventType(EvT)(EvT* e)
44 {
45     return (e.response_type & ~0x80);
46 }
47 
48 class XcbDisplay : Display
49 {
50     import gfx.core.rc : atomicRcCode, Rc;
51     import gfx.graal : Backend;
52     import X11.Xlib : XDisplay = Display;
53 
54     mixin(atomicRcCode);
55 
56     private XDisplay *_dpy;
57     private xcb_connection_t* _conn;
58     private xcb_atom_t[Atom] _atoms;
59     private int _mainScreenNum;
60     private uint _xkbFirstEv;
61     private XcbKeyboard _xkb;
62     private xcb_screen_t*[] _screens;
63 
64     private Rc!Instance _instance;
65 
66     private Window[] _windows;
67     private XcbWindow[] _xcbWindows;
68 
69     this(DisplayCreateInfo createInfo)
70     {
71         import std.exception : enforce;
72         import X11.Xlib : XCloseDisplay, XDefaultScreen, XOpenDisplay;
73         import X11.Xlib_xcb : XGetXCBConnection, XSetEventQueueOwner, XCBOwnsEventQueue;
74 
75         gfxXcbLog.trace("opening X display");
76         _dpy = enforce(XOpenDisplay(null));
77         scope(failure) {
78             XCloseDisplay(_dpy);
79         }
80         _conn = enforce(XGetXCBConnection(_dpy));
81         XSetEventQueueOwner(_dpy, XCBOwnsEventQueue);
82         _mainScreenNum = XDefaultScreen(_dpy);
83 
84         initializeAtoms();
85         initializeScreens();
86         initializeInstance(createInfo);
87 
88         _xkb = new XcbKeyboard(_conn, _xkbFirstEv);
89     }
90 
91     override void dispose()
92     {
93         import X11.Xlib : XCloseDisplay;
94 
95         if (_windows.length) {
96             auto ws = _windows.dup;
97             foreach (w; ws) w.close();
98         }
99         assert(!_windows.length);
100 
101         _xkb.dispose();
102 
103         _instance.unload();
104         gfxXcbLog.trace("closing X display");
105         XCloseDisplay(_dpy);
106     }
107 
108     private void initializeAtoms()
109     {
110         import core.stdc.stdlib : free;
111         import std.conv : to;
112         import std..string : toStringz;
113         import std.traits : EnumMembers;
114 
115         xcb_intern_atom_cookie_t[] cookies;
116         cookies.reserve(EnumMembers!Atom.length);
117 
118         foreach (immutable atom; EnumMembers!Atom) // static foreach
119         {
120             auto name = atom.to!string;
121             cookies ~= xcb_intern_atom(_conn, 1,
122                     cast(ushort)name.length, toStringz(name));
123         }
124 
125         foreach (i, immutable atom; EnumMembers!Atom) // static foreach
126         {
127             immutable name = atom.to!string;
128             xcb_generic_error_t* err;
129             auto reply = xcb_intern_atom_reply(_conn, cookies[i], &err);
130             if (err)
131             {
132                 throw new Exception("failed initializing atom " ~ name ~ ": ",
133                         (*err).to!string);
134             }
135             if (reply.atom == XCB_ATOM_NONE)
136             {
137                 throw new Exception("could not retrieve atom " ~ name);
138             }
139             _atoms[atom] = reply.atom;
140             free(reply);
141         }
142     }
143 
144     private void initializeScreens()
145     {
146         xcb_screen_iterator_t iter;
147         for (iter = xcb_setup_roots_iterator(xcb_get_setup(_conn)); iter.rem;
148                 xcb_screen_next(&iter))
149         {
150             _screens ~= iter.data;
151         }
152     }
153 
154     private void initializeInstance(DisplayCreateInfo createInfo)
155     {
156         assert(!_instance);
157 
158         foreach (b; createInfo.backendCreateOrder) {
159             final switch (b) {
160             case Backend.vulkan:
161                 try {
162                     import gfx.vulkan : createVulkanInstance, debugReportInstanceExtensions,
163                             lunarGValidationLayers, VulkanCreateInfo, vulkanInit;
164                     import gfx.vulkan.wsi : xcbSurfaceInstanceExtensions;
165 
166                     gfxXcbLog.trace("Attempting to instantiate Vulkan");
167                     vulkanInit();
168                     VulkanCreateInfo vci;
169                     vci.mandatoryExtensions = xcbSurfaceInstanceExtensions;
170                     vci.optionalExtensions = createInfo.debugCallbackEnabled ?
171                             debugReportInstanceExtensions : [];
172                     vci.optionalLayers = createInfo.validationEnabled ?
173                             lunarGValidationLayers : [];
174                     _instance = createVulkanInstance(vci);
175                 }
176                 catch (Exception ex) {
177                     gfxXcbLog.warningf("Vulkan is not available. %s", ex.msg);
178                 }
179                 break;
180             case Backend.gl3:
181                 try {
182                     import gfx.core.rc : makeRc;
183                     import gfx.gl3 : GlInstance;
184                     import gfx.gl3.context : GlAttribs;
185                     import gfx.window.xcb.context : XcbGlContext;
186                     gfxXcbLog.trace("Attempting to instantiate OpenGL");
187                     auto w = new XcbWindow(this, null, "", true);
188                     w.show(10, 10);
189                     scope(exit) w.close();
190                     auto ctx = makeRc!XcbGlContext(_dpy, _mainScreenNum, GlAttribs.init, w._win);
191                     gfxXcbLog.trace("Creating an OpenGL instance");
192                     _instance = new GlInstance(ctx);
193                 }
194                 catch (Exception ex) {
195                     gfxXcbLog.warningf("OpenGL is not available. %s", ex.msg);
196                 }
197                 break;
198             }
199             if (_instance) break;
200         }
201 
202         if (!_instance) {
203             throw new Exception("Could not instantiate a backend");
204         }
205     }
206 
207     override @property Instance instance() {
208         return _instance;
209     }
210 
211     override @property Window[] windows()
212     {
213         return _windows;
214     }
215 
216     override Window createWindow(in string title)
217     {
218         return new XcbWindow(this, _instance, title, false);
219     }
220 
221     override void pollAndDispatch()
222     {
223         while (true) {
224             auto e = xcb_poll_for_event(_conn);
225             if (!e) break;
226             handleEvent(e);
227         }
228     }
229 
230     private XcbWindow xcbWindow(xcb_window_t win) {
231         foreach(w; _xcbWindows) {
232             if (w._win == win) return w;
233         }
234         return null;
235     }
236 
237     void registerWindow(XcbWindow window) {
238         _windows ~= window;
239         _xcbWindows ~= window;
240     }
241 
242     void unregisterWindow(XcbWindow window) {
243         import std.algorithm : remove;
244         _windows = _windows.remove!(w => w is window);
245         _xcbWindows = _xcbWindows.remove!(w => w is window);
246     }
247 
248     private @property int mainScreenNum()
249     {
250         return _mainScreenNum;
251     }
252 
253     private @property xcb_screen_t* mainScreen()
254     {
255         return _screens[_mainScreenNum];
256     }
257 
258     private void handleEvent(xcb_generic_event_t* e)
259     {
260         immutable xcbType = xcbEventType(e);
261 
262         switch (xcbType)
263         {
264         case XCB_KEY_PRESS:
265             auto ev = cast(xcb_key_press_event_t*)e;
266             auto xcbWin = xcbWindow(ev.event);
267             _xkb.processKeyDown(ev.detail, xcbWin ? xcbWin._onKeyOnHandler : null);
268             break;
269         case XCB_KEY_RELEASE:
270             auto ev = cast(xcb_key_press_event_t*)e;
271             auto xcbWin = xcbWindow(ev.event);
272             _xkb.processKeyUp(ev.detail, xcbWin ? xcbWin._onKeyOffHandler : null);
273             break;
274         case XCB_BUTTON_PRESS:
275             auto ev = cast(xcb_button_press_event_t*)e;
276             auto xcbWin = xcbWindow(ev.event);
277             if (xcbWin && xcbWin._onHandler)
278                 xcbWin._onHandler(MouseEvent(ev.event_x, ev.event_y, _xkb.mods));
279             break;
280         case XCB_BUTTON_RELEASE:
281             auto ev = cast(xcb_button_press_event_t*)e;
282             auto xcbWin = xcbWindow(ev.event);
283             if (xcbWin && xcbWin._offHandler)
284                 xcbWin._offHandler(MouseEvent(ev.event_x, ev.event_y, _xkb.mods));
285             break;
286         case XCB_MOTION_NOTIFY:
287             auto ev = cast(xcb_motion_notify_event_t*)e;
288             auto xcbWin = xcbWindow(ev.event);
289             if (xcbWin && xcbWin._moveHandler)
290                 xcbWin._moveHandler(MouseEvent(ev.event_x, ev.event_y, _xkb.mods));
291             break;
292         case XCB_CONFIGURE_NOTIFY:
293             auto ev = cast(xcb_configure_notify_event_t*)e;
294             auto xcbWin = xcbWindow(ev.event);
295             if (xcbWin) xcbWin.handleConfigureNotify(ev);
296             break;
297         case XCB_PROPERTY_NOTIFY:
298             // auto ev = cast(xcb_configure_notify_event_t*)e;
299             // auto xcbWin = xcbWindow(ev.window);
300             break;
301         case XCB_CLIENT_MESSAGE:
302             auto ev = cast(xcb_client_message_event_t*)e;
303             if (ev.data.data32[0] == atom(Atom.WM_DELETE_WINDOW)) {
304                 auto win = xcbWindow(ev.window);
305                 if (win._onCloseHandler) {
306                     win._closeFlag = win._onCloseHandler();
307                 }
308                 else {
309                     win._closeFlag = true;
310                 }
311             }
312             break;
313         default:
314             if (xcbType == _xkbFirstEv)
315             {
316                 auto genKbd = cast(XkbGenericEvent*)e;
317                 if (genKbd.common.deviceID == _xkb.device)
318                 {
319                     switch (genKbd.common.xkbType)
320                     {
321                     case XCB_XKB_STATE_NOTIFY:
322                         auto es = &genKbd.state;
323                         _xkb.updateState(
324                             es.baseMods, es.latchedMods, es.lockedMods,
325                             es.baseGroup, es.latchedGroup, es.lockedGroup
326                         );
327                         break;
328                     default:
329                         break;
330                     }
331                 }
332             }
333             break;
334         }
335     }
336 
337     private xcb_atom_t atom(Atom atom) const
338     {
339         auto at = (atom in _atoms);
340         if (at)
341             return *at;
342         return XCB_ATOM_NONE;
343     }
344 }
345 
346 private union XkbGenericEvent
347     {
348 
349         struct CommonFields
350         {
351             ubyte response_type;
352             ubyte xkbType;
353             ushort sequence;
354             xcb_timestamp_t time;
355             ubyte deviceID;
356         }
357 
358         CommonFields common;
359         xcb_xkb_new_keyboard_notify_event_t newKbd;
360         xcb_xkb_map_notify_event_t map;
361         xcb_xkb_state_notify_event_t state;
362     }
363 
364 
365 private class XcbKeyboard : XKeyboard
366 {
367     private uint _device;
368 
369     this(xcb_connection_t *connection, out uint xkbFirstEv)
370     {
371         import core.stdc.stdlib : free;
372         import std.exception : enforce;
373         import xkbcommon.x11;
374         import xkbcommon.xkbcommon;
375 
376         xcb_prefetch_extension_data(connection, &xcb_xkb_id);
377 
378         auto reply = xcb_get_extension_data(connection, &xcb_xkb_id);
379         if (!reply || !reply.present) {
380             throw new Exception("XKB extension not supported by X server");
381         }
382         xkbFirstEv = reply.first_event;
383 
384         auto cookie = xcb_xkb_use_extension(connection,
385                 XKB_X11_MIN_MAJOR_XKB_VERSION,
386                 XKB_X11_MIN_MINOR_XKB_VERSION);
387         auto xkbReply = xcb_xkb_use_extension_reply(connection, cookie, null);
388         if (!xkbReply) {
389             throw new Exception("could not get xkb extension");
390         }
391         else if(!xkbReply.supported) {
392             free(xkbReply);
393             throw new Exception("xkb required version not supported");
394         }
395         free(xkbReply);
396 
397         ushort mapParts =
398             XCB_XKB_MAP_PART_KEY_TYPES |
399             XCB_XKB_MAP_PART_KEY_SYMS |
400             XCB_XKB_MAP_PART_MODIFIER_MAP |
401             XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS |
402             XCB_XKB_MAP_PART_KEY_ACTIONS |
403             XCB_XKB_MAP_PART_KEY_BEHAVIORS |
404             XCB_XKB_MAP_PART_VIRTUAL_MODS |
405             XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP;
406 
407         ushort events =
408             XCB_XKB_EVENT_TYPE_NEW_KEYBOARD_NOTIFY |
409             XCB_XKB_EVENT_TYPE_MAP_NOTIFY |
410             XCB_XKB_EVENT_TYPE_STATE_NOTIFY;
411 
412         auto cookie2 = xcb_xkb_select_events_checked(
413                 connection, XCB_XKB_ID_USE_CORE_KBD,
414                 events, 0, events, mapParts, mapParts, null);
415         auto err = xcb_request_check(connection, cookie2);
416         if (err) {
417             throw new Exception("failed to select notify events from xcb xkb");
418         }
419 
420         auto ctx = enforce(
421             xkb_context_new(XKB_CONTEXT_NO_FLAGS), "Could not alloc XKB context"
422         );
423         scope(failure) xkb_context_unref(ctx);
424 
425         _device = xkb_x11_get_core_keyboard_device_id(connection);
426         enforce (_device != -1, "Could not get X11 keyboard device id");
427 
428         auto keymap = enforce(
429             xkb_x11_keymap_new_from_device(ctx, connection, _device,
430                     XKB_KEYMAP_COMPILE_NO_FLAGS),
431             "Could not get X11 Keymap");
432         scope(failure) xkb_keymap_unref(keymap);
433 
434         auto state = enforce(
435             xkb_x11_state_new_from_device(keymap, connection, _device),
436             "Could not alloc X11 XKB state"
437         );
438 
439         super(ctx, keymap, state);
440     }
441 
442     @property uint device()
443     {
444         return _device;
445     }
446 }
447 
448 class XcbWindow : Window
449 {
450     import gfx.graal.presentation : Surface;
451 
452     private XcbDisplay _dpy;
453     private Instance _instance;
454     private xcb_window_t _win;
455     private Surface _surface;
456     private string _title;
457     private uint _width;
458     private uint _height;
459     private ResizeHandler _resizeHandler;
460     private MouseHandler _moveHandler;
461     private MouseHandler _onHandler;
462     private MouseHandler _offHandler;
463     private KeyHandler _onKeyOnHandler;
464     private KeyHandler _onKeyOffHandler;
465     private CloseHandler _onCloseHandler;
466     private bool _closeFlag;
467     private bool _dummy;
468 
469     this(XcbDisplay dpy, Instance instance, in string title, bool dummy)
470     {
471         assert(dpy && (dummy || instance));
472         _dpy = dpy;
473         _instance = instance;
474         _title = title;
475         _dummy = dummy;
476     }
477 
478     override @property string title()
479     {
480         return _title;
481     }
482 
483     override void setTitle(in string title)
484     {
485         import std..string : toStringz;
486 
487         if (_win) {
488             xcb_change_property(_dpy._conn, cast(ubyte) XCB_PROP_MODE_REPLACE, _win,
489                     cast(xcb_atom_t) XCB_ATOM_WM_NAME, cast(xcb_atom_t) XCB_ATOM_STRING,
490                     8, cast(uint) title.length, toStringz(title));
491             xcb_change_property(_dpy._conn, cast(ubyte) XCB_PROP_MODE_REPLACE, _win,
492                     cast(xcb_atom_t) XCB_ATOM_WM_ICON_NAME, cast(xcb_atom_t) XCB_ATOM_STRING,
493                     8, cast(uint) title.length, toStringz(title));
494         }
495         _title = title;
496     }
497 
498     override @property void onResize(ResizeHandler handler) {
499         _resizeHandler = handler;
500     }
501     override @property void onMouseMove(MouseHandler handler) {
502         _moveHandler = handler;
503     }
504     override @property void onMouseOn(MouseHandler handler) {
505         _onHandler = handler;
506     }
507     override @property void onMouseOff(MouseHandler handler) {
508         _offHandler = handler;
509     }
510     override @property void onKeyOn(KeyHandler handler) {
511         _onKeyOnHandler = handler;
512     }
513     override @property void onKeyOff(KeyHandler handler) {
514         _onKeyOffHandler = handler;
515     }
516     override @property void onClose(CloseHandler handler) {
517         _onCloseHandler = handler;
518     }
519 
520     override @property Surface surface() {
521         return _surface;
522     }
523 
524     override @property bool closeFlag() const {
525         return _closeFlag;
526     }
527 
528     override @property void closeFlag(in bool flag) {
529         _closeFlag = flag;
530     }
531 
532     override void show(uint width, uint height)
533     {
534         const screen = _dpy.mainScreen;
535 
536         const cmap = xcb_generate_id(_dpy._conn);
537         _win = xcb_generate_id(_dpy._conn);
538 
539         auto visual = drawArgbVisual(screen);
540         const depth = drawVisualDepth(screen, visual.visual_id);
541 
542         xcb_create_colormap(_dpy._conn, XCB_COLORMAP_ALLOC_NONE, cmap, screen.root, visual.visual_id);
543 
544         immutable mask = XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | XCB_CW_COLORMAP;
545         const uint[] values = [XCB_BACK_PIXMAP_NONE, 0, cmap, 0];
546 
547         auto cookie = xcb_create_window_checked(_dpy._conn, depth,
548                 _win, screen.root, 50, 50, cast(ushort) width, cast(ushort) height, 0,
549                 XCB_WINDOW_CLASS_INPUT_OUTPUT, visual.visual_id, mask, &values[0]);
550 
551         auto err = xcb_request_check(_dpy._conn, cookie);
552         if (err) {
553             import std.format : format;
554             throw new Exception(format("GFX-XCB: could not create window: %s", err.error_code));
555         }
556 
557         _width = width;
558         _height = height;
559 
560         if (_dummy) {
561             xcb_flush(_dpy._conn);
562             return;
563         }
564 
565         // register regular events
566         {
567             const uint[] attrs = [
568                 XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS
569                 | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_ENTER_WINDOW
570                 | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_POINTER_MOTION
571                 | XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_EXPOSURE
572                 | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE, 0
573             ];
574             xcb_change_window_attributes(_dpy._conn, _win,
575                     XCB_CW_EVENT_MASK, &attrs[0]);
576         }
577         // register window close event
578         {
579             const xcb_atom_t[] props = [atom(Atom.WM_DELETE_WINDOW), 0];
580             xcb_change_property(_dpy._conn, XCB_PROP_MODE_REPLACE, _win,
581                     atom(Atom.WM_PROTOCOLS), XCB_ATOM_ATOM, 32, 1, &props[0]);
582         }
583         // setting title
584         setTitle(_title);
585 
586         _dpy.registerWindow(this);
587 
588         xcb_map_window(_dpy._conn, _win);
589         xcb_flush(_dpy._conn);
590 
591         import gfx.graal : Backend;
592         final switch (_instance.backend) {
593         case Backend.vulkan:
594             import gfx.vulkan.wsi : createVulkanXcbSurface;
595             _surface = createVulkanXcbSurface(_instance, _dpy._conn, _win);
596             break;
597         case Backend.gl3:
598             import gfx.gl3 : GlInstance;
599             import gfx.gl3.swapchain : GlSurface;
600             _surface = new GlSurface(_win);
601             auto glInst = cast(GlInstance)_instance;
602             auto ctx = glInst.ctx;
603             ctx.makeCurrent(_win);
604             break;
605         }
606     }
607 
608     override void close()
609     {
610         if (_dummy) {
611             if (_win) {
612                 xcb_destroy_window(_dpy._conn, _win);
613                 _win = 0;
614             }
615         }
616         else {
617             if (_win) {
618                 xcb_unmap_window(_dpy._conn, _win);
619                 xcb_destroy_window(_dpy._conn, _win);
620                 xcb_flush(_dpy._conn);
621                 _win = 0;
622             }
623             _dpy.unregisterWindow(this);
624         }
625     }
626 
627     private xcb_atom_t atom(Atom atom) const
628     {
629         return _dpy.atom(atom);
630     }
631 
632     private void handleConfigureNotify(xcb_configure_notify_event_t* e)
633     {
634         if (!_resizeHandler) return;
635         if (e.width != _width || e.height != _height) {
636             _width = e.width;
637             _height = e.height;
638             _resizeHandler(_width, _height);
639         }
640     }
641 }
642 
643 xcb_visualtype_t *drawArgbVisual(const xcb_screen_t *s)
644 {
645     xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s);
646 
647     if(depth_iter.data) {
648         for(; depth_iter.rem; xcb_depth_next (&depth_iter)) {
649             if(depth_iter.data.depth == 32) {
650                 for(xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
651                     visual_iter.rem; xcb_visualtype_next (&visual_iter))
652                 {
653                     if (visual_iter.data && visual_iter.data.class_ == XCB_VISUAL_CLASS_TRUE_COLOR)
654                         return visual_iter.data;
655                 }
656             }
657         }
658     }
659 
660     throw new Exception("could not find a draw visual");
661 }
662 
663 ubyte drawVisualDepth(const xcb_screen_t *s, xcb_visualid_t vis)
664 {
665     xcb_depth_iterator_t depth_iter = xcb_screen_allowed_depths_iterator(s);
666 
667     if(depth_iter.data) {
668         for(; depth_iter.rem; xcb_depth_next (&depth_iter)) {
669             for(xcb_visualtype_iterator_t visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
670                 visual_iter.rem; xcb_visualtype_next (&visual_iter))
671             {
672                 if(vis == visual_iter.data.visual_id)
673                     return depth_iter.data.depth;
674             }
675         }
676     }
677     throw new Exception("could not find a visuals depth");
678 }