OpenGL context creation without libraries using Xlib
13 Jun 2021 - tsp
Last update 13 Jun 2021
12 mins
Introduction and summary
This article is mainly a short note to myself about the initialization of OpenGL (3.0)
in X11 environments without the usage of any external windowing toolkits or helper
libraries just depending on Xlib. X11 environments cover nearly all Unices
and Linux systems - except for the ones that moved on to Wayland.
Why would one want to do initialization without a library such aus GLUT
and what does one loose when doing so? Basically I do this since I support the
idea of minimizing external dependencies out of stability reasons. The less
dependencies are present the less can be broken when an external library makes
an incompatible change - and the less one has to worry about stuff like licensing.
This is especially important when one writes applications that should be compile-
and runable even in a few decades from now - one just also has to keep an eye on
which APIs oneās using when binding to any external service. Itās for example safe
to assume that POSIX functions exposed by platforms stay stable over many decades,
itās safe to assume that the usual OpenGL functions will be still supported. Itās
not safe to assume that for example Linux specific stuff, Kernel images or many
libraries from the Linux ecosystem will stay stable. What does one loose? Libraries
such as GLUT provide an easy abstraction from a variety of platforms - FreeGLUT
for example supports X11 based platforms, Wayland based platforms, MacOS X, Windows, etc.
One has to develop one set of routines for all of these platforms when doing
it oneself - i.e. develop the abstraction routines oneself. This is reasonable
when doing simple stuff - but usually not if one just wants to develop an
application and get an full blown windowing toolkit.
The steps required for basic context creation are:
- Open the X11 display using
libx11
- Checking the GLX version
- Getting a list of framebuffer configurations that match a set of specified
requirements (for example depth of colors, alpha buffer support, the types
of drawables and the render type)
- Select the best configuration available.
- Getting a visual
- Creating the colormap
- Create and map a window
- Determine a list of extensions
- Use
glXCreateContextAttribsARB to create a new context. This is an
extension that first has to be queried via glXGetProcAddressARB. If this
is not supported use the legacy glXCreateNewContext method to create
the context. First try to get an OpenGL 3 context, if this is not possible query
an OpenGL 2.x context.
- Maybe interesting for later: Determine if itās a direct or indirect context.
- Make the context current
To release the context later on release all allocated resources:
- Make current a NULL context
- Destroy the context
- Destroy the window
- Release colormap
- Close the display
Step-by-step
Opening the display server connection
Open the display:
Display *display = XOpenDisplay(NULL);
if(!display) {
printf("Failed to open X display\n");
return 1;
}
The parameter to XOpenDisplay is the name of the display that should be used.
When passing NULL the content of the DISPLAY environment variable
is used - which is whatās usually expected by users anyways. If this procedure
fails it returns NULL, else a reference to a display structure that wraps
all resources associated with the display server connection.
GLX version check
In the next step weāre going to check if weāre satisfied with the GLX version:
/*
Verify GLX version
*/
{
int dwGLXVersionMajor;
int dwGLXVersionMinor;
if(!glXQueryVersion(display, &dwGLXVersionMajor, &dwGLXVersionMinor)) {
printf("Failed to query GLX version\n");
XCloseDisplay(display);
return 1;
}
if((dwGLXVersionMajor < 1) || (dwGLXVersionMinor < 3)) {
printf("Invalid GLX version %d.%d\n", dwGLXVersionMajor, dwGLXVersionMinor);
XCloseDisplay(display);
return 1;
}
}
Framebuffer configuration
Now one has to get the best matching supported framebuffer configuration. One has
to note that OpenGL does - in contrast to DirectX - not provide an automatic
conversion in case one requests an unsupported format. This is done by supplying
a list of required features:
static int visualAttributes[] = {
GLX_RENDER_TYPE, GLX_RGBA_BIT,
GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
GLX_RED_SIZE, 8,
GLX_GREEN_SIZE, 8,
GLX_BLUE_SIZE, 8,
GLX_ALPHA_SIZE, 8,
GLX_DEPTH_SIZE, 24,
GLX_STENCIL_SIZE, 8,
GLX_X_RENDERABLE, True,
GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT,
GLX_DOUBLEBUFFER, True,
None
};
In this case the request is for 8 bit ARGB true color surfaces with a 24 bit
depth and an 8 bit stencil buffer. It also requests that the framebuffer has
to be renderable to the X display (GLX_X_RENDERABLE) - it should be
renderable into a window (GLX_DRAWABLE_TYPE) and the surface should
support double buffering. One asks OpenGL by using glXChooseFBConfig
for all compatible configurations - and then iterating over them and selecting
the best one according to some heuristics (for example the most samples per
pixel by checking the visual info):
GLXFBConfig cfgChoosen;
XVisualInfo* viChoosen;
{
int dwConfigurationCount;
int i;
GLXFBConfig* fbConfig = glXChooseFBConfig(
display,
DefaultScreen(display),
visualAttributes,
&dwConfigurationCount
);
if((!fbConfig) || (dwConfigurationCount == 0)) {
printf("Failed to query framebuffer configuration\n");
XCloseDisplay(display);
return 1;
}
#ifdef DEBUG
printf("Matching visuals:\n");
#endif
int dwIndexBestConfig = 0;
int dwBestSamples = 0;
for(i=0; i < dwConfigurationCount; i++) {
XVisualInfo* vi = glXGetVisualFromFBConfig(display, fbConfig[i]);
if(vi) {
int dwSampleBuffers = 0;
int dwSamples = 0;
glXGetFBConfigAttrib(display, fbConfig[i], GLX_SAMPLE_BUFFERS, &dwSampleBuffers);
glXGetFBConfigAttrib(display, fbConfig[i], GLX_SAMPLES, &dwSamples );
#ifdef DEBUG
printf("\t%02lx, Sample buffers: %d, Samples: %d\n", vi->visualid, dwSampleBuffers, dwSamples);
#endif
if((dwBestSamples < dwSamples) && (dwSampleBuffers > 0)) {
dwBestSamples = dwSamples;
dwIndexBestConfig = i;
}
}
}
memcpy(&cfgChoosen, &(fbConfig[dwIndexBestConfig]), sizeof(cfgChoosen));
XFree(fbConfig);
}
Window creation
The next step is already the window creation. This requires the previously
gathered information from the visual:
XVisualInfo* viChoosen = glXGetVisualFromFBConfig(display, cfgChoosen);
/*
This atom will be used only to catch the deletion of the window
by the window manager
*/
Atom wmDeleteMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
Colormap cmap;
cmap = XCreateColormap(
display,
RootWindow(display, viChoosen->screen),
viChoosen->visual,
AllocNone
);
XSetWindowAttributes swa;
swa.colormap = cmap;
swa.background_pixmap = None;
swa.border_pixel = 0;
swa.event_mask = StructureNotifyMask;
Window wndWindow = XCreateWindow(
display,
RootWindow(display, viChoosen->screen),
0, 0,
250, 250,
0,
viChoosen->depth,
InputOutput,
viChoosen->visual,
CWBorderPixel|CWColormap|CWEventMask,
&swa
);
if(!wndWindow) {
printf("Failed to create window\n");
XFree(viChoosen);
XCloseDisplay(display);
return 1;
}
XFree(viChoosen);
As usual for X applications one then has to map the window - this displays
the window as soon as the X library syncs to the display server (keep in mind
that the Xlib will do some kind of caching of messages when playing around):
XStoreName(display, wndWindow, "OpenGL context window");
XMapWindow(display, wndWindow);
/*
Register our usage of the WM_DELETE_WINDOW message to catch
deletion of the window inside of our event loop.
*/
XSetWMProtocols(display, wndWindow, &wmDeleteMessage, 1);
OpenGL context creation
Now comes the interesting part - querying the GLX_ARB_create_context extension
and using this extension to create a new context. This is the way to create context
for OpenGL 3 since newer versions might deprecate older versions and thus deprecate
downwards compatibility - that is guaranteed when using glXCreateNewContext.
Thus newer versions have to be requested with this extension. In case this extension
is not supported we could use a fallback to the glXCreateNewContext way. When
the extension is present one can even make a context current without providing
a default framebuffer.
To query the extension one first has to request the extension list and then
search this list for the required extension. Itās a good idea to put this into
a separate set of utility functions usually. The queried list is just an ASCII
list of all supported extensions separated by spaces. The simplest way is to
split all supported extensions at the separating spaces.
static bool glCheckExtensionSupported(
const char* lpExtensionString,
const char* lpExtensionName
) {
unsigned long int dwCurrentStart = 0;
unsigned long int dwCurrentEnd = 0;
unsigned long int extStrLen;
if((lpExtensionName == NULL) || (lpExtensionString == NULL)) {
return false;
}
extStrLen = strlen(lpExtensionString);
if((lpExtensionString[0] == 0x00) || (extStrLen == 0)) {
return false;
}
for(; dwCurrentEnd <= extStrLen; dwCurrentEnd = dwCurrentEnd + 1) {
if((lpExtensionString[dwCurrentEnd] != ' ') && (lpExtensionString[dwCurrentEnd] != 0x00)) {
continue;
}
if(strncmp(lpExtensionName, &(lpExtensionString[dwCurrentStart]), dwCurrentEnd - dwCurrentStart) == 0) {
return true;
}
if(lpExtensionString[dwCurrentEnd] == 0x00) {
return false;
}
dwCurrentStart = dwCurrentEnd + 1;
}
return false;
}
One also has to define the type of the method that weāre going to query the
function pointer for:
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);
This can now be used to check support for GLX_ARB_create_context. In case
itās supported one queries the address of the method using glXGetProcAddressARB:
const char* lpGLExtensions = glXQueryExtensionsString(display, DefaultScreen(display));
if(!glCheckExtensionSupported(lpGLExtensions, "GLX_ARB_create_context")) {
printf("GLX_ARB_create_context is not supported\n");
XFree(viChoosen);
XCloseDisplay(display);
return 1;
}
glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddressARB((const GLubyte*)"glXCreateContextAttribsARB");
Before doing context creation weāll define a simple error handler that will be
invoked by OpenGL in case of an allocation error - and weāll simply set a global
flag that will be polled at the required stages.
typedef int (*lpfnErrorHandler)(Display* display, XErrorEvent* ev);
static bool bGLnitErrorRaised = false;
static int glInitErrorHandler(Display* display, XErrorEvent* ev) {
bGLnitErrorRaised = true;
return 0;
}
Now we can go on and request our context:
GLXContext ctx = 0;
{
lpfnErrorHandler oldHandler = XSetErrorHandler(&glInitErrorHandler);
int contextAttributes[] = {
GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
GLX_CONTEXT_MINOR_VERSION_ARB, 0,
None
};
ctx = glXCreateContextAttribsARB(
display,
cfgChoosen,
0,
True,
contextAttributes
);
/* Flush */
XSync(display, False);
if(bGLnitErrorRaised || (!ctx)) {
printf("Failed to create 3.0 context\n");
/*
Fallback context creation ... IF we want to support OpenGL < 3.0
*/
bGLnitErrorRaised = false;
/*
Use Major version 1, minor version 0. Will return the newest
possible context version ...
*/
contextAttributes[1] = 1;
contextAttributes[3] = 0;
ctx = glXCreateContextAttribsARB(
display,
cfgChoosen,
0,
True,
contextAttributes
);
if(bGLnitErrorRaised || (!ctx)) {
printf("Failed to create any legacy context\n");
XDestroyWindow(display, wndWindow);
XCloseDisplay(display);
return 1;
}
printf("Created legacy context ...\n");
}
/* Restore old error handler */
XSetErrorHandler(oldHandler);
}
A simple event and drawing loop
Now the context is ready to be used - one just has to make it current and
perform the desired operations. This is usually done inside a tight loop
that is also built to handle X events. In case one wants to build something
interactive like a simulation or game one should use a non blocking loop
for X11 events - or even handle them in a separate thread. For any synchronous
application directly using the blocking XNextEvent might be sufficient.
In the following example the frame is updated every 250 milliseconds until the
window is closed.
glXMakeCurrent(display, wndWindow, ctx);
unsigned long int dwLastFrame = 0;
float fRed = 0.0f;
float fGreen = 0.0f;
float fBlue = 0.0f;
for(;;) {
if(XEventsQueued(display, QueuedAfterReading)) {
XEvent xev;
XNextEvent(display, &xev);
if(xev.type == ClientMessage) {
if(xev.xclient.data.l[0] == wmDeleteMessage) {
glXMakeCurrent(display, 0, 0);
XDestroyWindow(display, xev.xclient.window);
wndWindow = 0;
break;
}
}
/* HANDLE EVENTS HERE */
} else {
/*
Here the time since the last frame is calculated in a
busy waiting mechanism. This is ugly but might be interesting
for some specific applications. Usually you should choose
either the approach of waiting for events using the
blocking XNextEvent (when programming applications that only
change on UI events) or use some kind of event notification
mechanism like kqueue/kevent - one can obtain the handle used
by Xlib by using ConnectionNumber(display) and use this directly
in kqueue / select.
*/
unsigned long int dwTS;
unsigned long int dwDeltaTS;
struct timespec spec;
clock_gettime(CLOCK_REALTIME, &spec);
dwTS = spec.tv_sec * 1000 + spec.tv_nsec / 1000000;
if(dwTS > dwLastFrame) {
dwDeltaTS = dwTS - dwLastFrame;
} else {
dwDeltaTS = (~0) - dwTS + dwLastFrame;
}
if(dwDeltaTS > 250) {
/* Next frame ... */
dwLastFrame = dwTS;
glClearColor(fRed, fGreen, fBlue, 1);
glClear(GL_COLOR_BUFFER_BIT);
glXSwapBuffers(display, wndWindow);
fRed = fRed + 0.1;
if(fRed > 1) {
fRed = 0;
fGreen = fGreen + 0.1;
}
if(fGreen > 1.0) {
fGreen = 0;
fBlue = fBlue + 0.1;
}
if(fBlue > 1) {
fBlue = 0;
}
}
}
}
Cleanup
After the window has closed one of course should do the mandatory clean up:
if((ctx) && (wndWindow)) {
glXMakeCurrent(display, 0, 0);
glXDestroyContext(display, ctx);
}
if(wndWindow) { XDestroyWindow(display, wndWindow); wndWindow = 0; }
if(display) { XCloseDisplay(display); display = NULL; }
Thatās basically all required to create an usable OpenGL context. Usually one
has to query a handful of more extensions to write usable applications but that
really has been the hard part of initializing OpenGL. The remaining part building
an application using OpenGL is more simple - at least in the sense of being way
more logical as soon as you understand the pipeline youāre working with.
Code summary
The sample code can be found in a GitHub GIST
This article is tagged: