OpenGL context in Xlib
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:
*dp;
Display ;
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)) {
("invalid GLX version. Expected > 1.3, got %d.%d\n", glx_major, glx_minor);
printfreturn -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[] = {
, 1,
GLX_X_RENDERABLE, GLX_WINDOW_BIT,
GLX_DRAWABLE_TYPE, GLX_TRUE_COLOR,
GLX_X_VISUAL_TYPE, GLX_RGBA_BIT,
GLX_RENDER_TYPE, 1,
GLX_DOUBLEBUFFER, 8,
GLX_RED_SIZE, 8,
GLX_GREEN_SIZE, 8,
GLX_BLUE_SIZE, 8,
GLX_ALPHA_SIZE, 24,
GLX_DEPTH_SIZE, 8,
GLX_STENCIL_SIZE
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:
GLX_X_RENDERABLE
: The X visual must be renderableGLX_DRAWABLE_TYPE
: The drawable type must be WindowGLX_X_VISUAL_TYPE
: The visual type must be True ColorGLX_RENDER_TYPE
: The render type must be RGBAGLX_DOUBLEBUFFER
: The visual must support double bufferingGLX_RED_SIZE
: The red size must be 8GLX_GREEN_SIZE
: The green size must be 8GLX_BLUE_SIZE
: The blue size must be 8GLX_ALPHA_SIZE
: The alpha size must be 8GLX_DEPTH_SIZE
: The depth size must be 24GLX_STENCIL_SIZE
: The stencil size must be 8
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.
(Display *d) {
GLXFBConfig get_best_fbcint fbcount = 0;
*fbc = glXChooseFBConfig(d, DefaultScreen(d), vattr, &fbcount);
GLXFBConfig
if (!fbc) {
return NULL;
}
int best_fbc = -1, best_num_samp = -1, best_sample_buf = -1;
for(int i = 0; i < fbcount; ++i) {
*vi = glXGetVisualFromFBConfig(d, fbc[i]);
XVisualInfo if (vi) {
int sbuf, samples;
(d, fbc[i], GLX_SAMPLE_BUFFERS, &sbuf);
glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLES, &samples);
glXGetFBConfigAttrib
if (best_fbc < 0 || (sbuf && samples) > best_num_samp) {
= i;
best_fbc = samples;
best_num_samp = sbuf;
best_sample_buf }
}
(vi);
XFree}
("selected best fbconfig %d, with SAMPLE_BUFFERS %d, SAMPLES %d. ",
printf, best_sample_buf, best_num_samp);
best_fbc
= fbc[best_fbc];
GLXFBConfig best
// free the fbc list allocated before
(fbc);
XFree
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.
.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 |
swa| ButtonReleaseMask | StructureNotifyMask | PointerMotionMask; ButtonPressMask
Finally we can create the window as follows:
= XCreateWindow(dp, RootWindow(dp, vi->screen), 0, 0, WIDTH, HEIGHT, 0,
win ->depth, InputOutput, vi->visual, CWBorderPixel|CWColormap|CWEventMask,
vi&swa);
if (!win) {
return 1;
}
(vi);
XFree
(dp, win); XMapWindow
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[] = {
, 3,
GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
GLX_CONTEXT_MINOR_VERSION_ARB
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(;;) {
= strstr(extensions, ext);
loc if (loc == NULL) break;
= loc + strlen(ext);
terminator if ((loc == extensions || *(loc - 1) == ' ') &&
(*terminator == ' ' || *terminator == '\0')) return 1;
= terminator;
extensions }
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.
= 0;
glXCreateContextAttribsARBProc glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddress((const GLubyte*)"glXCreateContextAttribsARB"); 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:
- Create a
GLXContext
with theglXCreateContextAttribsARB
function, making use of our previously definedctx_attr
, the best selected framebuffer configuration and the Display handler. - Run
XSync
to ensure that the context is created before we continue. By doing this, the X server flushes the output buffer in order to wait until all requests have been received and processed. - Check if we have a valid
GLXContext
. - Ensure we have a direct rendering context using the helper function
glXIsDirect(3)
. If we don't have a Direct rendering contexts, all rendering commands pass to the X server instead of bypassing it. - Set the current context using
glXMakeCurrent(3)
. There can be only one current context per thread, which is part of theglXMakeCurrent(3)
subroutine's job. It also has the job to attach the Context to a GLX drawable (which can be a window or GLX pixmap). - Execute
glClearColor(3)
to specify the red, green, blue, and alpha clamped values used by theglClear(3)
subroutine to clear the color buffers.
= glXCreateContextAttribsARB(dp, fbc, NULL, 1, ctx_attr);
GLXContext ctx
(dp, 0);
XSync
if (!ctx) return NULL;
if (!glXIsDirect(dp, ctx))
("Indirect GLX rendering context obtained. ");
printfelse
("Direct GLX rendering context obtained. ");
printf
(dp, win, ctx);
glXMakeCurrent
(0.7f, 0.7f, 0.7f, 1.0f); glClearColor
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:
- Poll for events from the X server.
XPending(3)
returns the number of pending events that have not been processed yet. - Handle events using
XNextEvent(3)
. It copies the next event from the queue into theXEvent
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 theConfigureNotify
event, which is triggered when the window is resized, and theKeyRelease
event, from which we want to detect if the user presses the ESC key, which will end the program. - Clear the screen buffers using
glClear(3)
. In this example we are clearing the color buffer, the depth buffer, and the stencil buffer. - Render the scene. This is the part where the OpenGL commands are executed.
- Swap front and back buffers using
glXSwapBuffers(3)
.
void clear_screen_buffers() {
(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glClear}
void handle_resize(Display *d, Window w) {
;
XWindowAttributes wattr(d, w, &wattr);
XGetWindowAttributes
(0, 0, wattr.width, wattr.height);
glViewport}
void glx_loop(Display *d, Window win) {
;
XEvent ev;
KeySym ksfor(;;) {
while (XPending(d)) {
(d, &ev);
XNextEvent
switch(ev.type) {
case ConfigureNotify:
(d, win);
handle_resizebreak;
case KeyRelease:
= XLookupKeysym((XKeyEvent *)&ev, 0);
ks if (ks == XK_Escape) {
return;
}
break;
default:
break;
}
}
();
clear_screen_buffers
// OpenGL render here
(d, win);
glXSwapBuffers}
}
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.
(dp, 0, 0);
glXMakeCurrent(dp, ctx);
glXDestroyContext
(dp, win);
XDestroyWindow(dp, cmap);
XFreeColormap(dp); XCloseDisplay
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[] = {
, 1,
GLX_X_RENDERABLE, GLX_TRUE_COLOR,
GLX_X_VISUAL_TYPE, GLX_RGBA_BIT,
GLX_RENDER_TYPE, GLX_WINDOW_BIT,
GLX_DRAWABLE_TYPE, 1,
GLX_DOUBLEBUFFER, 8,
GLX_RED_SIZE, 8,
GLX_GREEN_SIZE, 8,
GLX_BLUE_SIZE, 8,
GLX_ALPHA_SIZE, 24,
GLX_DEPTH_SIZE, 8,
GLX_STENCIL_SIZE
None};
const int ctx_attr[] = {
, 3,
GLX_CONTEXT_MAJOR_VERSION_ARB, 3,
GLX_CONTEXT_MINOR_VERSION_ARB
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)) {
("invalid GLX version. Expected > 1.3, got %d.%d\n", glx_major, glx_minor);
printfreturn -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(;;) {
= strstr(extensions, ext);
loc if (loc == NULL) break;
= loc + strlen(ext);
terminator if ((loc == extensions || *(loc - 1) == ' ') &&
(*terminator == ' ' || *terminator == '\0')) return 1;
= terminator;
extensions }
return 0;
}
(Display *d) {
GLXFBConfig get_best_fbcint fbcount = 0;
*fbc = glXChooseFBConfig(d, DefaultScreen(d), vattr, &fbcount);
GLXFBConfig
if (!fbc) {
return NULL;
}
int best_fbc = -1, best_num_samp = -1, best_sample_buf = -1;
for(int i = 0; i < fbcount; ++i) {
*vi = glXGetVisualFromFBConfig(d, fbc[i]);
XVisualInfo if (vi) {
int sbuf, samples;
(d, fbc[i], GLX_SAMPLE_BUFFERS, &sbuf);
glXGetFBConfigAttrib(d, fbc[i], GLX_SAMPLES, &samples);
glXGetFBConfigAttrib
// 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) {
= i;
best_fbc = samples;
best_num_samp = sbuf;
best_sample_buf }
}
(vi);
XFree}
("selected best fbconfig %d, with SAMPLE_BUFFERS %d, SAMPLES %d. ",
printf, best_sample_buf, best_num_samp);
best_fbc
= fbc[best_fbc];
GLXFBConfig best
//free the fbc list allocated before
(fbc);
XFree
return best;
}
void is_direct_ctx(Display *d, GLXContext ctx) {
if (!glXIsDirect(d, ctx))
("Indirect GLX rendering context obtained. ");
printfelse
("Direct GLX rendering context obtained. ");
printf}
(Display *d, GLXFBConfig fbc) {
GLXContext glx_create_context= 0;
glXCreateContextAttribsARBProc glXCreateContextAttribsARB = (glXCreateContextAttribsARBProc)glXGetProcAddress((const GLubyte*)"glXCreateContextAttribsARB");
glXCreateContextAttribsARB
if (!check_glx_extension(d, "GLX_ARB_create_context") || !glXCreateContextAttribsARB) {
return NULL;
}
= glXCreateContextAttribsARB(d, fbc, NULL, 1, ctx_attr);
GLXContext _ctx
(d, 0);
XSync
if (!_ctx) return NULL;
(d, _ctx);
is_direct_ctx
return _ctx;
}
void clear_screen_buffers() {
(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glClear}
void handle_resize(Display *d, Window w) {
;
XWindowAttributes wattr(d, w, &wattr);
XGetWindowAttributes
(0, 0, wattr.width, wattr.height);
glViewport}
void glx_loop(Display *d, Window win) {
;
XEvent ev;
KeySym ksfor(;;) {
while (XPending(d)) {
(d, &ev);
XNextEvent
switch(ev.type) {
case ConfigureNotify:
(d, win);
handle_resizebreak;
case KeyRelease:
= XLookupKeysym((XKeyEvent *)&ev, 0);
ks if (ks == XK_Escape) {
return;
}
break;
default:
break;
}
}
();
clear_screen_buffers(d, win);
glXSwapBuffers}
}
int main(int argc, char **argv) {
*dp;
Display ;
Window win
= XOpenDisplay(NULL);
dp
if (!dp) return 1;
if (check_glx_version(dp) != 0) {
return 1;
}
= get_best_fbc(dp);
GLXFBConfig best_fbc
if (!best_fbc) {
return 1;
}
*vi = glXGetVisualFromFBConfig(dp, best_fbc);
XVisualInfo
if (!vi) {
return 1;
}
;
XSetWindowAttributes swa;
Colormap cmap
.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 |
swa| ButtonReleaseMask | StructureNotifyMask | PointerMotionMask;
ButtonPressMask
= XCreateWindow(dp, RootWindow(dp, vi->screen), 0, 0, WIDTH, HEIGHT, 0,
win ->depth, InputOutput, vi->visual, CWBorderPixel|CWColormap|CWEventMask,
vi&swa);
if (!win) {
return 1;
}
(vi);
XFree
(dp, win);
XMapWindow
= glx_create_context(dp, best_fbc);
GLXContext ctx
if (!ctx) {
return 1;
}
(dp, win, ctx);
glXMakeCurrent
("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));
printf
(0.7f, 0.7f, 0.7f, 1.0f);
glClearColor
(dp, win);
glx_loop
(dp, 0, 0);
glXMakeCurrent(dp, ctx);
glXDestroyContext
(dp, win);
XDestroyWindow(dp, cmap);
XFreeColormap(dp);
XCloseDisplay
return 0;
}