OpenGL context creation without libraries using Xlib

13 Jun 2021 - tsp
Last update 13 Jun 2021
Reading time 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:

To release the context later on release all allocated resources:

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:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support