One month ago I began work to investigate what it would take to bring Ultra App Kit, the foundation for our new generation of game development tools, to Linux. Today I am happy to share my progress with you and discuss some of the things I have learned.
Developed by MIT in the year 1984, X11 is an interesting beast that is easy to start with, but can become quite difficult once you get into the details. (Wayland support is of course an obvious step in the not-too-distant future but I have to work with what exists here now today and Ubuntu 20.04 still uses X by default.) The single hardest part had to do with calls to XSetInputFocus on windows that had not yet been mapped. X has a weird asynchronous design, yet XSetInputFocus does not seem to get added to the command queue and instead depends on the mapping (visible) state of a window right now. That means that is you create or show a window and then immediately activate it, an error will occur that looks something like this:
Error of failed request: BadMatch (invalid parameter attributes) Major opcode of failed request: 42 (X_SetInputFocus) Serial number of failed request: 12 Current serial number in output stream: 12
The way around this is to call XMapWindow and then wait on the event queue until a MapNotify event for that window occurs, adding all other events into a list that can be evaluated on the next call to WaitEvent(). The time elapsed is checked inside the loop in case something weird happens and the desired event is never received:
void Window::Show() { if (!Hidden()) return; XMapWindow(display->GetHandle(),xwindow); XMoveResizeWindow(display->GetHandle(), GetHandle(),m_position.x,m_position.y,size.x,size.y); XFlush(display->GetHandle()); XSync(display->GetHandle(),false); auto engine = GameEngine::Get(); XEvent ev = {}; auto start = Millisecs(); while (xhidestate) { XNextEvent(display->GetHandle(),&ev); if (ev.type == MapNotify and ev.xany.window == GetHandle()) { xhidestate = false; return; } if (engine) engine->storedxevents.push_back(ev); Sleep(1); if (Millisecs() - start > 5000) { Warn("MapNotify is taking a long time to be received. This may cause window errors."); return; } } }
POSIX timers are strange creatures that seem to follow rules all their own. A timer callback gets triggered during any call to sleep() when a timer tick has occurred, but a mutex lock inside the callback will freeze the program. I ended up using the much simpler timerfd interface.
Double-buffering, good text rendering, and alpha blending are all different extensions built on top of the base X11 system. Getting all of this to work together took a lot of trial and error. However, I think you will agree based on the screenshots below that this work has been worthwhile.
Multi-select draggable treeview with insertion between nodes
Multi-line text display with optional word wrapping
Hierarchical menu system with real popup windows
For the final leg of development I have set up a small Kickstarter campaign. If you haven't gotten Ultra App Kit yet this is a good opportunity to grab it before the Linux build is released. Ultra App Kit can also be purchase in our store or on Steam.
- 4
1 Comment
Recommended Comments