13 Jun 2021 - tsp
Last update 13 Jun 2021
12 mins
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:
libx11
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.To release the context later on release all allocated resources:
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.
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;
}
}
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);
}
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);
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);
}
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;
}
}
}
}
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.
The sample code can be found in a GitHub GIST
This article is tagged:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/