OpenGL context in Xlib

OpenGL context in Xlib

Index


When developing a graphics oriented program using OpenGL, the initial OpenGL context boilerplate is nowadays almost a thing that we take for granted. There are several choices out there in the form of libraries and frameworks from which we can choose, load and use. But what happens under the hood?

This note is more about discovering and tinkering with OpenGL. For real use cases, using third party libraries like GLFW3 and/or GLAD is recommended. Specially if aiming for cross-platform support.

To recap on the usual path to follow when creating an OpenGL context, we first need to somehow load the core OpenGL functions. This is because OpenGL in itself is not a library but an API specification. The most common loaders out there are GLEW and GLAD, being the later one more modular and tailored.

Once our program knows which OpenGL functions it can use, it is time to create a context, and map it to a Window. We also want to handle the I/O events that happen in that Window. This part is often handled by libraries like GLFW3 or SDL2, that make the heavy lifting for us.

How to do it

Instead of using those handy third party libraries and loaders, we can rely on the Xlib library and its GLX implementations to create a modern OpenGL context.

The process is mostly straight forward. It may resemble a bit to how you get a Vulkan triangle in a window, but in a fraction of the code lines required. The steps are defined below, based on a core example from a Khronos Wiki page.

Initial boilerplate

Let's start with the basic boilerplate to load the OpenGL library and create the context.

In a plain main.c file we need to include the following headers:

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <GL/gl.h>
#include <GL/glx.h>

then we can add some definitions that will help later:

#define WIN_WIDTH 800
#define WIN_HEIGHT 600

// https://registry.khronos.org/OpenGL/extensions/ARB/GLX_ARB_create_context.txt
#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092

and we can populate the bare minimum needed to create the C program:

Display *dp;
Window win;
XSetWindowAttributes swa;
Colormap cmap;

int main(int argc, char **argv) {
    return 0;
}

finally, let's keep the compile command at hand:

$ cc -o out main.c -lGL -lX11

Verify that OpenGL is available

With the header files in place, we can make use of the function glXQueryVersion(3) to check if we have the desired GLX extension available.

The function itself only returns major and minor version of the GLX extension, given a display. If we wrap it in a helper function it can look like this:

int check_glx_version(Display *d) {
    int glx_major, glx_minor;
    if (!glXQueryVersion(d, &glx_major, &glx_minor) || ((glx_major == 1) && (glx_minor < 3)) || (glx_major < 1)) {
        printf("invalid GLX version. Expected > 1.3, got %d.%d\n", glx_major, glx_minor);
        return -1;
    }
    return 0;
}

Get a correct visual

The next step is to get a correct visual. Out of the available options we are going to get one based on a GLX frambe buffer config.

In order to get the best suited frame buffer, the function glXChooseFBConfig(3) can be used. It returns a list of available frame buffers of type GLXFBConfig structs, based on a list of attributes that we define. Once we have the list we can select the one with most samples and sample buffers.

First of all we need to define our array of desired attributes:

const int vattr[] = {
    GLX_X_RENDERABLE,    1,
    GLX_DRAWABLE_TYPE,   GLX_WINDOW_BIT,
    GLX_X_VISUAL_TYPE,   GLX_TRUE_COLOR,
    GLX_RENDER_TYPE,     GLX_RGBA_BIT,
    GLX_DOUBLEBUFFER,    1,
    GLX_RED_SIZE,        8,
    GLX_GREEN_SIZE,      8,
    GLX_BLUE_SIZE,       8,
    GLX_ALPHA_SIZE,      8,
    GLX_DEPTH_SIZE,      24,
    GLX_STENCIL_SIZE,    8,
    None
};

using the program glxinfo you can retrieve the list of attributes available

The visual attribute array should be terminated by None, which denotes the end of the list. The rest of the parameters we have set are described below:

This info will be used in tandem with the glXChooseFBConfig(3) function mentioned earlier, to obtain a list of available frame buffers. For each avaliable frame buffer we will create a XVisualInfo struct. If we have a valid XVisualInfo struct for a particular frame buffer, we can use the glXGetFBConfigAttrib(3) function to get the number of samples and sample buffers.

GLXFBConfig get_best_fbc(Display *d) {
    int fbcount = 0;
    GLXFBConfig *fbc = glXChooseFBConfig(d, DefaultScreen(d), vattr, &fbcount);

    if (!fbc) {
        return NULL;
    }

    int best_fbc = -1, best_num_samp = -1, best_sample_buf = -1;

    for(int i = 0; i < fbcount; ++i) {
        XVisualInfo *vi = glXGetVisualFromFBConfig(d, fbc[i]);
        if (vi) {
            int sbuf, samples;
            glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLE_BUFFERS, &sbuf);
            glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLES, &samples);

            if (best_fbc < 0 || (sbuf && samples) > best_num_samp) {
                best_fbc = i;
                best_num_samp = samples;
                best_sample_buf = sbuf;
            }
        }
        XFree(vi);
    }

    printf("selected best fbconfig %d, with SAMPLE_BUFFERS %d, SAMPLES %d. ",
            best_fbc, best_sample_buf, best_num_samp);
    
    GLXFBConfig best = fbc[best_fbc];

    // free the fbc list allocated before
    XFree(fbc);

    return best;
}

Create a window

Creating a X window for a modern OpenGL context requires some specific steps that are often simplified when not using OpenGL.

Instead of using XCreateSimpleWindow(3), we have to use XCreateWindow(3), since the first method inherits some values from the parent that we need to manually set. Since we already know our best frame buffer config, we can create a specific Visual using the glXGetVisualFromFBConfig(3) function and the GLX GLXFBConfig struct that we got from get_best_fbc().

In addition to that, we need to create two more variables for our window creation, one of the type XSetWindowAttributes and the other of the type Colormap. The struct XSetWindowAttributes contains all the attributes that we need to create our window. The struct Colormap contains the colormap used by the window.

swa.colormap = cmap = XCreateColormap(dp, RootWindow(dp, vi->screen), vi->visual, 0);
swa.background_pixmap = None;
swa.border_pixel = 0;
swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask |
        ButtonPressMask | ButtonReleaseMask | StructureNotifyMask | PointerMotionMask;

Finally we can create the window as follows:

win = XCreateWindow(dp, RootWindow(dp, vi->screen), 0, 0, WIDTH, HEIGHT, 0, 
                    vi->depth, InputOutput, vi->visual, CWBorderPixel|CWColormap|CWEventMask,
                    &swa);

if (!win) {
    return 1;
}

XFree(vi);

XMapWindow(dp, win);

Create a context

We can now create a modern OpenGL context to use along with X11. This is the more complex or tricky part when using OpenGL in raw mode with X11.

First of all, we need to create a context attributes' list that we can pass to glXCreateContextAttribsARB(3).

const int ctx_attr[] = {
    GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
    GLX_CONTEXT_MINOR_VERSION_ARB, 3,
    None
};

As we did with the visual attribute list, we need to include the None command at the end of the context acttributes array. For this context, we need to include the GLX_CONTEXT_MAJOR_VERSION_ARB and GLX_CONTEXT_MINOR_VERSION_ARB attributes, that will specify against which OpenGL version we want to create the context (in this case OpenGL 3.3).

We then need to perform some safe checking before moving forward. We also need to check if we have the required GLX_ARB_create_context extension available to us. The function glXQueryExtensionsString(3) returns a list of supported GLX extensions that comes in a specific format. In the machine running this test, the output looks like this:

GLX_ARB_context_flush_control GLX_ARB_create_context GLX_ARB_create_context_no_error GLX_ARB_create_context_profile GLX_ARB_fbconfig_float GLX_ARB_framebuffer_sRGB GLX_ARB_get_proc_address GLX_ARB_multisample GLX_EXT_create_context_es2_profile GLX_EXT_create_context_es_profile GLX_EXT_fbconfig_packed_float GLX_EXT_framebuffer_sRGB GLX_EXT_no_config_context GLX_EXT_texture_from_pixmap GLX_EXT_visual_info GLX_EXT_visual_rating GLX_MESA_copy_sub_buffer GLX_MESA_query_renderer GLX_SGIS_multisample GLX_SGIX_fbconfig GLX_SGIX_pbuffer GLX_SGIX_visual_select_group GLX_SGI_make_current_read 

So we can wrap the function in a helper function that parses the extension list and checks if the desired extension is available:

/* here 0 means false aka no support and 1 means true aka has support */
int check_glx_extension(Display *d, const char *ext) {
    const char *terminator, *loc;
    // get default screen's GLX extension list
    const char *extensions = glXQueryExtensionsString(d, DefaultScreen(d));

    if (extensions == NULL || ext == NULL) return 0;

    for(;;) {
        loc = strstr(extensions, ext);
        if (loc == NULL) break;

        terminator = loc + strlen(ext);
        if ((loc == extensions || *(loc - 1) == ' ') &&
            (*terminator == ' ' || *terminator == '\0')) return 1;

        extensions = terminator;
    }

    return 0;
}

The following part seems to be stablished by the Khronos extension registry, so it's better to just follow it up. Create a type alias for the glXCreateContextAttribsARB function, which is part of the GLX_ARB_create_context extension.

typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);

Then we can use it as follows in the code below. This will give us a pointer that we can later use to validate if we can create a modern OpenGL context.

glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddress((const GLubyte*)"glXCreateContextAttribsARB");

Combining the previous two prerequisites, we can now check if we can create a modern OpenGL context:

if (!check_glx_extension(dp, "GLX_ARB_create_context") || !glXCreateContextAttribsARB) {
    return NULL;
}

This is a good step in the code where to implement some fallback logic to previous versions of OpenGL, or even a software based rendering if we want the application to run in computers without modern OpenGL support. In order to keep things simple, it's omitted in this example.

If everything went well, we are ready for the GLXContext creation following the next steps:

GLXContext ctx = glXCreateContextAttribsARB(dp, fbc, NULL, 1, ctx_attr);

XSync(dp, 0);

if (!ctx) return NULL;

if (!glXIsDirect(dp, ctx))
    printf("Indirect GLX rendering context obtained. ");
else
    printf("Direct GLX rendering context obtained. ");

glXMakeCurrent(dp, win, ctx);

glClearColor(0.7f, 0.7f, 0.7f, 1.0f);

Run the main loop

Having all the puzzle pieces in place, we can glue it all together and implement a main loop. Since we are using X11, we can use the XNextEvent(3) function to poll for events, and we can create some simple callback functions to handle them.

The main loop of our program is going to perform as follows:

  1. Poll for events from the X server. XPending(3) returns the number of pending events that have not been processed yet.
  2. Handle events using XNextEvent(3). It copies the next event from the queue into the XEvent structure, and removes that event from the queue. We can then select which action to execute based on the event type, using a switch statement or similar. In this case, we will handle the ConfigureNotify event, which is triggered when the window is resized, and the KeyRelease event, from which we want to detect if the user presses the ESC key, which will end the program.
  3. Clear the screen buffers using glClear(3). In this example we are clearing the color buffer, the depth buffer, and the stencil buffer.
  4. Render the scene. This is the part where the OpenGL commands are executed.
  5. Swap front and back buffers using glXSwapBuffers(3).
void clear_screen_buffers() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}

void handle_resize(Display *d, Window w) {
    XWindowAttributes wattr;
    XGetWindowAttributes(d, w, &wattr);

    glViewport(0, 0, wattr.width, wattr.height);
}

void glx_loop(Display *d, Window win) {
    XEvent ev;
    KeySym ks;
    for(;;) {
        while (XPending(d)) {
            XNextEvent(d, &ev);

            switch(ev.type) {
                case ConfigureNotify:
                    handle_resize(d, win);
                    break;
                case KeyRelease:
                    ks = XLookupKeysym((XKeyEvent *)&ev, 0);
                    if (ks == XK_Escape) {
                        return;
                    }
                    break;
                default:
                    break;
            }
        }
        clear_screen_buffers();

        // OpenGL render here

        glXSwapBuffers(d, win);
    }
}

Clean up

At the end of the program, we need to clean up the resources we allocated.

As with other Xlib programs, there is an order to follow when cleaning up before closing our program. First we need to destroy the context, then the window followed by the freeing of the colormap. Finally we need to close the display.

glXMakeCurrent(dp, 0, 0);
glXDestroyContext(dp, ctx);

XDestroyWindow(dp, win);
XFreeColormap(dp, cmap);
XCloseDisplay(dp);

Complete C example

The following code contains all the previously described steps in a C program.

A more in depth example of the modern OpenGL context creation with C and Xlib can be found here.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <GL/gl.h>
#include <GL/glx.h>

#define WIDTH 800
#define HEIGHT 600

/*
 * main reference from
 * https://www.khronos.org/opengl/wiki/Tutorial:_OpenGL_3.0_Context_Creation_(GLX)
 * https://registry.khronos.org/OpenGL/extensions/ARB/GLX_ARB_create_context.txt
 */

#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091
#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092
typedef GLXContext (*glXCreateContextAttribsARBProc)(Display*, GLXFBConfig, GLXContext, Bool, const int*);

// visual attributes to get a matching FB config
const int vattr[] = {
    GLX_X_RENDERABLE,    1,
    GLX_X_VISUAL_TYPE,   GLX_TRUE_COLOR,
    GLX_RENDER_TYPE,     GLX_RGBA_BIT,
    GLX_DRAWABLE_TYPE,   GLX_WINDOW_BIT,
    GLX_DOUBLEBUFFER,    1,
    GLX_RED_SIZE,        8,
    GLX_GREEN_SIZE,      8,
    GLX_BLUE_SIZE,       8,
    GLX_ALPHA_SIZE,      8,
    GLX_DEPTH_SIZE,      24,
    GLX_STENCIL_SIZE,    8,
    None
};

const int ctx_attr[] = {
    GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
    GLX_CONTEXT_MINOR_VERSION_ARB, 3,
    None
};

int check_glx_version(Display *d) {
    int glx_major, glx_minor;

    if (!glXQueryVersion(d, &glx_major, &glx_minor) || ((glx_major == 1) && (glx_minor < 3)) || (glx_major < 1)) {
        printf("invalid GLX version. Expected > 1.3, got %d.%d\n", glx_major, glx_minor);
        return -1;
    }

    return 0;
}

/* here 0 means false aka no support and 1 means true aka has support */
int check_glx_extension(Display *d, const char *ext) {
    const char *terminator, *loc;
    // get default screen's GLX extension list
    const char *extensions = glXQueryExtensionsString(d, DefaultScreen(d));

    if (extensions == NULL || ext == NULL) return 0;

    for(;;) {
        loc = strstr(extensions, ext);
        if (loc == NULL) break;

        terminator = loc + strlen(ext);
        if ((loc == extensions || *(loc - 1) == ' ') &&
            (*terminator == ' ' || *terminator == '\0')) return 1;

        extensions = terminator;
    }

    return 0;
}

GLXFBConfig get_best_fbc(Display *d) {
    int fbcount = 0;
    GLXFBConfig *fbc = glXChooseFBConfig(d, DefaultScreen(d), vattr, &fbcount);

    if (!fbc) {
        return NULL;
    }

    int best_fbc = -1, best_num_samp = -1, best_sample_buf = -1;

    for(int i = 0; i < fbcount; ++i) {
        XVisualInfo *vi = glXGetVisualFromFBConfig(d, fbc[i]);
        if (vi) {
            int sbuf, samples;
            glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLE_BUFFERS, &sbuf);
            glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLES, &samples);

            // printf("matching fbconfig %d, visualID 0x%2x: SAMPLE_BUFFERS = %d, SAMPLES = %d\n",
            //      i, (unsigned int)vi->visualid, sbuf, samples);

            if (best_fbc < 0 || (sbuf && samples) > best_num_samp) {
                best_fbc = i;
                best_num_samp = samples;
                best_sample_buf = sbuf;
            }
        }
        XFree(vi);
    }

    printf("selected best fbconfig %d, with SAMPLE_BUFFERS %d, SAMPLES %d. ",
            best_fbc, best_sample_buf, best_num_samp);
    
    GLXFBConfig best = fbc[best_fbc];

    //free the fbc list allocated before
    XFree(fbc);

    return best;
}

void is_direct_ctx(Display *d, GLXContext ctx) {
    if (!glXIsDirect(d, ctx))
        printf("Indirect GLX rendering context obtained. ");
    else
        printf("Direct GLX rendering context obtained. ");
}

GLXContext glx_create_context(Display *d, GLXFBConfig fbc) {
    glXCreateContextAttribsARBProc glXCreateContextAttribsARB = 0;
    glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddress((const GLubyte*)"glXCreateContextAttribsARB");

    if (!check_glx_extension(d, "GLX_ARB_create_context") || !glXCreateContextAttribsARB) {
        return NULL;
    }

    GLXContext _ctx = glXCreateContextAttribsARB(d, fbc, NULL, 1, ctx_attr);

    XSync(d, 0);

    if (!_ctx) return NULL;

    is_direct_ctx(d, _ctx);

    return _ctx;
}

void clear_screen_buffers() {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
}

void handle_resize(Display *d, Window w) {
    XWindowAttributes wattr;
    XGetWindowAttributes(d, w, &wattr);

    glViewport(0, 0, wattr.width, wattr.height);
}

void glx_loop(Display *d, Window win) {
    XEvent ev;
    KeySym ks;
    for(;;) {
        while (XPending(d)) {
            XNextEvent(d, &ev);

            switch(ev.type) {
                case ConfigureNotify:
                    handle_resize(d, win);
                    break;
                case KeyRelease:
                    ks = XLookupKeysym((XKeyEvent *)&ev, 0);
                    if (ks == XK_Escape) {
                        return;
                    }
                    break;
                default:
                    break;
            }
        }
        clear_screen_buffers();
        glXSwapBuffers(d, win);
    }
}

int main(int argc, char **argv) {
    Display *dp;
    Window win;

    dp = XOpenDisplay(NULL);

    if (!dp) return 1;

    if (check_glx_version(dp) != 0) {
        return 1;
    }

    GLXFBConfig best_fbc = get_best_fbc(dp);

    if (!best_fbc) {
        return 1;
    }

    XVisualInfo *vi = glXGetVisualFromFBConfig(dp, best_fbc);

    if (!vi) {
        return 1;
    }

    XSetWindowAttributes swa;
    Colormap cmap;

    swa.colormap = cmap = XCreateColormap(dp, RootWindow(dp, vi->screen), vi->visual, 0);
    swa.background_pixmap = None;
    swa.border_pixel = 0;
    swa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask |
            ButtonPressMask | ButtonReleaseMask | StructureNotifyMask | PointerMotionMask;

    win = XCreateWindow(dp, RootWindow(dp, vi->screen), 0, 0, WIDTH, HEIGHT, 0, 
                        vi->depth, InputOutput, vi->visual, CWBorderPixel|CWColormap|CWEventMask,
                        &swa);

    if (!win) {
        return 1;
    }

    XFree(vi);

    XMapWindow(dp, win);

    GLXContext ctx = glx_create_context(dp, best_fbc);

    if (!ctx) {
        return 1;
    }

    glXMakeCurrent(dp, win, ctx);

    printf("GLSL version: %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION));
    printf("OpenGL version: %s\n", glGetString(GL_VERSION));
    printf("OpenGL renderer: %s\n", glGetString(GL_RENDERER));

    glClearColor(0.7f, 0.7f, 0.7f, 1.0f);

    glx_loop(dp, win);

    glXMakeCurrent(dp, 0, 0);
    glXDestroyContext(dp, ctx);

    XDestroyWindow(dp, win);
    XFreeColormap(dp, cmap);
    XCloseDisplay(dp);

    return 0;
}