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