Xlib 04: From scratch opengl and shaders with raw Xlib



Introduction

In the last post we looked into how to create a window with basic OpenGL using fixed function pipeline. In this post we will try to recreate the same scene but using more modern aproach using shaders. Here we will “bake” shaders inside our source code.

Here is the final code of 07_window_opengl_core_profile.c:

#include <stdio.h>
#include <X11/Xlib.h>
#include <unistd.h>
#include <stdlib.h>

#define GL_GLEXT_PROTOTYPES
#include <GL/glx.h>
#include <GL/glext.h>

static int DoubleBufferAttributes[] = {
    GLX_RGBA,
    GLX_RED_SIZE, 1,
    GLX_GREEN_SIZE, 1,
    GLX_BLUE_SIZE, 1,
    GLX_DEPTH_SIZE, 12,
    GLX_DOUBLEBUFFER,
    None,
};

void VerifyOrDie(int ResultStatus, const char *Message) {
    if(ResultStatus == 0) {
        fprintf(stderr, "%s", Message);
        exit(2);
    }
}

void CheckShaderCompilation(unsigned int Shader) {
    int ResultStatus;
    char Buffer[512];
    glGetShaderiv(Shader, GL_COMPILE_STATUS, &ResultStatus);

    if(ResultStatus == 0) {
        glGetShaderInfoLog(Shader, sizeof(Buffer), NULL, Buffer);
        printf("ERROR: Shader compilation failed. -----------------------------------\n");
        printf("%s\n", Buffer);
    }
}

void CheckProgramCompilation(unsigned int Program) {
    int  ResultStatus;
    char Buffer[512];
    glGetProgramiv(Program, GL_COMPILE_STATUS, &ResultStatus);

    if(ResultStatus == 0)
    {
        glGetShaderInfoLog(Program, sizeof(Buffer), NULL, Buffer);
        printf("ERROR: Failed compiling program\n");
        printf("%s\n", Buffer);
    }
}


int main()
{
    Display* MainDisplay = XOpenDisplay(0);
    int MainScreen = XDefaultScreen(MainDisplay);
    Window RootWindow = XDefaultRootWindow(MainDisplay);

    int Dummy;
    int ResultStatus = glXQueryExtension(MainDisplay, &Dummy, &Dummy);
    VerifyOrDie(ResultStatus != 0, "Error: X Server has not GLX extension\n");

    XVisualInfo* VisualInfo = glXChooseVisual(MainDisplay, MainScreen, DoubleBufferAttributes);
    VerifyOrDie(VisualInfo != 0, "glXChooseVisual returned 0");
    VerifyOrDie(VisualInfo->class == TrueColor, "No True Color support. Cannot run program without it");

    GLXContext ShareList = None;
    int IsDirectRendering = True;
    GLXContext OpenGLContext = glXCreateContext(MainDisplay, VisualInfo, ShareList, IsDirectRendering);
    VerifyOrDie(OpenGLContext != 0, "ERROR: Couldn't create rendering context\n");

    int WindowX = 0;
    int WindowY = 0;
    int WindowWidth = 800;
    int WindowHeight = 600;
    int BorderWidth = 0;
    int WindowClass = InputOutput;
    int WindowDepth = VisualInfo->depth;
    Visual* WindowVisual = VisualInfo->visual;

    int AttributeValueMask = CWBackPixel | CWEventMask | CWColormap;

    XSetWindowAttributes WindowAttributes = {};
    WindowAttributes.colormap = XCreateColormap(MainDisplay, RootWindow, VisualInfo->visual, AllocNone);
    WindowAttributes.background_pixel = 0xffafe9af;
    WindowAttributes.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | PointerMotionMask;

    Window MainWindow = XCreateWindow(MainDisplay, RootWindow, 
            WindowX, WindowY, WindowWidth, WindowHeight,
            BorderWidth, WindowDepth, WindowClass, WindowVisual,
            AttributeValueMask, &WindowAttributes);

    XStoreName(MainDisplay, MainWindow, "General app");

    glXMakeCurrent(MainDisplay, MainWindow, OpenGLContext);

    XMapWindow(MainDisplay, MainWindow);

    Atom WM_DELETE_WINDOW = XInternAtom(MainDisplay, "WM_DELETE_WINDOW", False);
    if(!XSetWMProtocols(MainDisplay, MainWindow, &WM_DELETE_WINDOW, 1)) {
        printf("Couldn't register WM_DELETE_WINDOW\n");
    }


    /* ------------------------------------------------------------------------------------------------------------ */
    /* ---------- OPENGL ------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */

    float S = 0.5;
    float Vertices[] = {
          -S, -S, 0.0f,     1.0f, 0.0f, 0.0f,
        0.0f,  S, 0.0f,     0.0f, 1.0f, 0.0f,
           S, -S, 0.0f,     0.0f, 0.0f, 1.0f,
    };

    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

    const char* VertexShaderSource = 
        "#version 330 core\n"
        "layout (location = 0) in vec3 Pos;"
        "layout (location = 1) in vec3 InColor;"
        "out vec3 Color;"
        ""
        "void main()"
        "{"
        "   gl_Position = vec4(Pos.x, Pos.y, 0.0f, 1.0f);"
        "   Color = InColor;"
        "}\0";
    unsigned int VertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(VertexShader, 1, &VertexShaderSource, NULL);
    glCompileShader(VertexShader);
    CheckShaderCompilation(VertexShader);

    const char* FragmentShaderSource = 
        "#version 330 core\n"
        "out vec4 FragColor;"
        "in vec3 Color;"
        "void main()"
        "{"
        /* "   FragColor = vec4(1.0f, 0.5f, 0.5f, 1.0f);" */
        "   FragColor = vec4(Color, 1.0f);"
        "}\0";
    unsigned int FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(FragmentShader, 1, &FragmentShaderSource, NULL);
    glCompileShader(FragmentShader);
    CheckShaderCompilation(FragmentShader);

    unsigned int ShaderProgram = glCreateProgram();
    glAttachShader(ShaderProgram, VertexShader);
    glAttachShader(ShaderProgram, FragmentShader);
    glLinkProgram(ShaderProgram);

    CheckProgramCompilation(ShaderProgram);

    glDeleteShader(VertexShader);
    glDeleteShader(FragmentShader);

    glUseProgram(ShaderProgram);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void *)(3*sizeof(float)));
    glEnableVertexAttribArray(1);


    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */

    int IsProgramRunning = 1;
    while(IsProgramRunning) {

        while(XPending(MainDisplay)) {
            XEvent GeneralEvent = {};
            XNextEvent(MainDisplay, &GeneralEvent);
            switch(GeneralEvent.type) {
                case ClientMessage: 
                {
                    IsProgramRunning = 0;
                } break;
            }
        }

        {
            glClearColor(0.0, 0.0, 0.2, 1.0);
            glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

            glDrawArrays(GL_TRIANGLES, 0, 3);

            glXSwapBuffers(MainDisplay, MainWindow);
        }
    }
}

Compile it with gcc 07_window_opengl_core_profile.c -o 07_window_opengl_core_profile -lX11 -lGL

It should look like this.

Basic triangle window with shaders

Explanation

All the OpenGL initialization that we did in previous post is exactly the same. So we first verify that OpenGL is available, get correct visual, get context, create window and enable context. After that we start initialization proces related to shaders and vertex data. In this post we use only VBO (vertext buffer object) and skip usage of VAO.

Initially we create array of vertices and color. I wanted to use only vertices but then decided to add colors just to match visual of previous program. So this array contains 3 vertices. It contains X, Y, Z of vertex followed with R, G, B floats of color. In total we get 3x6=18 floats. So onsider it being 6 floats per vertex. Then we bind this array with glBindBuffer(…). Since we will not be chaning this vertices frequently we can use GL_STATIC_DRAW for glBufferData(…).

Since OpenGL does not not how the data that we linked is structured we need to define it. For that we will use glVertexAttribPointer(…). We give it index, size, type, normalized, stride and pointer. So for vertex position points we will use index 0. The element size is 3 (X, Y, Z) and type of data is **GL_FLOAT. Normalized is to GL_FALSE to not use normalization for our data so that it wont’ change. Stride is set as 6size(float) which means that to get next values for position we need to move 64=24 bytes forward in the data. And the very last we set offset of the first component inside passed data. And then we enable this attribute with glEnableVertexAttribArray(…)

For describing color in the data use pretty much the same approach but need to fix offsets and index. Index will be 1 and data will start 3*4=12 bytes after the start of data. Everthing else stays the same as colors is the same size as position.

For shaders we will be using vertex shader and fragment shader. To simplify things a little we will just “manually” define them in our main C source code as a string. To make it a bit more readable we will use a trick of C language of mergin of multiple strings that are followed one after the other. Considering that C language is has clear delimeters we can skip using new lines (\n) between string. But we still will have to add one new line at the top of definition to separate glsl version declaration. One very important that will be a problem if forgotten is to add null terminator (\0) at the very last string. This is due glShaderSource require source to be null terminated and can be a troublesome to debug if you are new to this and forget to null terminate shader source.

This is Vertex shader:

#version 330 core
layout (location = 0) in vec3 Pos;
layout (location = 1) in vec3 InColor;
out vec3 Color;

void main()
{
   gl_Position = vec4(Pos.x, Pos.y, 0.0f, 1.0f);
   Color = InColor;
}

It is a very simple and straightworward shader just to get things going. Since we decided to pack color information in our VBO we will extract position and color into their respective vec3 input params: Pos and InColor. To compile this shader we need to first request an id with glCreateShader(GL_VERTEX_SHADER) and then provide the source to OpenGL with glShaderSource(…). After that just compile this shader with glCompile(…). Then the code checks the shader for compilation errors and if found it prints it to a terminal.

For Fragment shader we used:

#version 330 core
out vec4 FragColor;
in vec3 Color;
void main()
{
   FragColor = vec4(Color, 1.0f);
}

To compile this shader we use exactly the same approach as for vertex shader. The only thing to pay attention that always gets me is to change GL_VERTEX_SHADER to GL_FRAGMENT_SHADER when you do copy pasting. Apart from the logic is pretty much the same. Since the shader is pretty simple for education purposes we just get extracted color from vertex shader and pass it down the pipleline after converting it vec4.

Once we compiled our shaders it is time to link them into a ‘program’ that will run on the GPU. First we program with glCreateProgram() and then attach both shaders with glAttachShader(…) to that program. After that we link the program with glLinkProgram(…) and check if had any errors in the process. Since our program is not dynamic we just ‘use’ this program once with glUseProgram(…). For a more dynamic application you would need to move this into a render loop.

The setup was a bit daunting but if you look at the rest of the code it looks not that different. We removed all the fixed-function commands for rendering and just call glDrawArrays(GL_TRIANGLES, 0, 3) to draw primitive data that we defined earlier. Basically we asking OpenGL to draw triangle in the data specified earlier starting at index 0 and do 3 of them. This 3 vertices are drawn as one triangle.

Conclusion

In this post we built a simple program to draw a colored rectangle using raw Xlib and OpenGL. I don’t suggest you build your apps with this approach. These posts were intended to build everything from scratch, play around with different approaches and get a feeling of how X Server windows are woring and a bit of OpenGL.

One of the things to try is to move shaders to separate files and load them dynamically on startup. You would need to just fread(…) the shaders into VertexShaderSource and FragmentShaderSource and that is pretty much it. With this you can try getting simple examples from shader toy and try to run them localy on your linux box. Unfortunately described system is not fully ready to start doing that because current version does not pass any time and resolution information to shaders. I will probably write about it a bit later but if you want to try yoursel you should research uniform variable for glsl and easily add them to this solution.

Bear in mind that code in this post is written in a way to be simpler for reading sequentially and for a real project you would need to break it apart for for maintainability.

Here is a sample scene I created using approach described in this post as illustration. If you want source code for these theme I might share it once I do a little cleanup (or just write me and I will send directly).