Xlib 02: Software rendering window in X11



Introduction

This is continuation of the previous post about creating windows using Xlib/X11. In this post I will show how to start basic software rendering inside linux from scratch usin Xlib.

Full Code

05_window_software_renderer.c

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define STATUS_ERROR 0

typedef struct {
    int X;
    int Y;
    int Width;
    int Height;
} entity;

typedef struct {
    uint8_t *Memory;
    uint64_t Size;
    uint32_t Width;
    uint32_t Height;
    uint32_t Pitch;
} buffer ;


float 
Clamp(float Min, float Value, float Max) {
    if(Value < Min)      { Value = Min; }
    else if(Value > Max) { Value = Max; }
    return Value; 
}

void DrawRect(buffer *Buffer, int32_t X, int32_t Y, int32_t Width, int32_t Height, uint32_t Color) {
    int32_t StartX = X;
    int32_t EndX = X + Width;
    int32_t StartY = Y;
    int32_t EndY = Y + Height;

    StartX = Clamp(0, StartX, Buffer->Width);
    EndX   = Clamp(0, EndX  , Buffer->Width);

    StartY = Clamp(0, StartY, Buffer->Height);
    EndY   = Clamp(0, EndY  , Buffer->Height);

    for(int Y = StartY; Y < EndY; Y++) {
        for(int X = StartX; X < EndX; X++) {
            uint32_t *Pixel = (uint32_t *)((uint32_t *)Buffer->Memory + Y*Buffer->Width + X);
            *Pixel = Color;
        }
    }
}



void DoRender(buffer* Buffer, entity Box)
{
    DrawRect(Buffer, 0, 0, Buffer->Width, Buffer->Height, 0xff87de87);
    /* DrawRect(Buffer, 10, 10, 50, 80, 0xff00aa44); */
    DrawRect(Buffer, Box.X, Box.Y, Box.Width, Box.Height, 0xff00aa44);
}

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

    int DefaultScreen = DefaultScreen(MainDisplay);
    GC Context = XDefaultGC(MainDisplay, DefaultScreen);

    int ScreenDepth = 24;
    XVisualInfo VisualInfo = {};
    if(STATUS_ERROR == XMatchVisualInfo(MainDisplay, DefaultScreen, ScreenDepth, TrueColor, &VisualInfo)) {
        printf("ERROR: No matching visual info\n");
    }
    
    int WindowX = 0;
    int WindowY = 0;
    int WindowWidth = 800;
    int WindowHeight = 600;
    int BorderWidth = 0;
    int WindowDepth = VisualInfo.depth;
    int WindowClass = InputOutput;
    Visual* WindowVisual = VisualInfo.visual;

    int AttributeValueMask = CWBackPixel | CWEventMask;
    XSetWindowAttributes WindowAttributes = {};
    WindowAttributes.background_pixel = 0xffffccaa;
    WindowAttributes.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | ExposureMask;

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

    XMapWindow(MainDisplay, MainWindow);

    XStoreName(MainDisplay, MainWindow, "Moving rectangle. Use arrow keys to move.");

    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 property \n");
    }


    int BitsPerPixel = 32;
    int BytesPerPixel = BitsPerPixel / 8;
    int WindowBufferSize = WindowWidth*WindowHeight*BytesPerPixel;

    buffer Buffer = {}; 
    Buffer.Width = WindowWidth;
    Buffer.Height = WindowHeight;
    Buffer.Pitch = Buffer.Width * BytesPerPixel;
    Buffer.Size = Buffer.Pitch * Buffer.Height;
    Buffer.Size = Buffer.Pitch * Buffer.Height;
    Buffer.Memory = (uint8_t *)malloc(Buffer.Size); 

    entity Box = {};
    Box.Width = 50;
    Box.Height = 80;
    Box.X = WindowWidth/2 - Box.Width/2;
    Box.Y = WindowHeight/2 - Box.Height/2;
    int StepSize = 5;

    int Offset = 0;
    int BytesBetweenScanlines = 0;
    XImage *WindowBuffer = XCreateImage(MainDisplay, VisualInfo.visual, VisualInfo.depth, ZPixmap, 
            Offset, (char *)Buffer.Memory, WindowWidth, WindowHeight, BitsPerPixel, BytesBetweenScanlines);

    int IsWindowOpen = 1;
    while(IsWindowOpen) {

        while(XPending(MainDisplay) > 0) {
            XEvent GeneralEvent = {};

            XNextEvent(MainDisplay, &GeneralEvent);

            switch(GeneralEvent.type) {
                case KeyPress:
                case KeyRelease:
                {
                    XKeyPressedEvent *Event = (XKeyPressedEvent *)&GeneralEvent;
                    if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Escape))
                    {
                        IsWindowOpen = 0;
                    }

                    if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Down))
                    {
                        Box.Y += StepSize;
                    }
                    else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Up))
                    {
                        Box.Y -= StepSize;
                    }
                    else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Right))
                    {
                        Box.X += StepSize;
                    }
                    else if(Event->keycode == XKeysymToKeycode(MainDisplay, XK_Left))
                    {
                        Box.X -= StepSize;
                    }

                } break;

                case ClientMessage:
                {
                    XClientMessageEvent *Event = (XClientMessageEvent *) &GeneralEvent;
                    if((Atom)Event->data.l[0] == WM_DELETE_WINDOW) {
                        XDestroyWindow(MainDisplay, MainWindow);
                        IsWindowOpen = 0;
                    }
                } break;

                case ConfigureNotify:
                {
                    XConfigureEvent *Event = (XConfigureEvent *)&GeneralEvent;
                    WindowWidth = Event->width;
                    WindowHeight = Event->height;

                    // NOTE: XDestroyImage also frees Memory, so no need to free from our side
                    XDestroyImage(WindowBuffer);

                    Buffer.Width = WindowWidth;
                    Buffer.Height = WindowHeight;
                    Buffer.Pitch = Buffer.Width * BytesPerPixel;
                    Buffer.Size = Buffer.Pitch * Buffer.Height;
                    Buffer.Memory = (uint8_t *)malloc(Buffer.Size); 

                    WindowBuffer = XCreateImage(MainDisplay, VisualInfo.visual, VisualInfo.depth, ZPixmap, 
                            Offset, (char *)Buffer.Memory, WindowWidth, WindowHeight, BitsPerPixel, BytesBetweenScanlines);
                } break;

            }
        }

        DoRender(&Buffer, Box);

        int SourceX = 0;
        int SourceY = 0;
        int DestX = 0;
        int DestY = 0;
        XPutImage(MainDisplay, MainWindow, Context, WindowBuffer, SourceX, SourceY, DestX, DestY, WindowWidth, WindowHeight);
    }
}

Compile code with: gcc 05_window_software_renderer.c -o 05_window_software_renderer -lX11

Explanation

This code is direct continuation of the previous post and specifically 4th program in that post. In that post we drew and moved rectangle using Xlib functions. The issue with that approach is that it is a lot slower to draw if you want to draw a lot of rectangles, circles, text and etc, which is a normal case for an app or game.

In the above program from this post we do the same thing but do all the rendering ourself and just send the result back just once per frame or once when app requires some drawing.

Even though the code grew quite a bit the difference in actual Xlib change is not that much. Basically we need to get something like a canvas where we can draw locally and ‘upload’ results back to x11 server once we are done. This cavans in our case is just a regular bitmap/image. Later using regular software rendering we can render different objects onto this image. In this example we implement only rectangle rendering but it is enought to show general concepts.

Initially we create XImage buffer with the help of XCreateImage(…) function. Even though in this example we used custom visual info it could easily be replaced with ‘CopyFromParent’ as we don’t need a extra control (used just to show how it could be done). We need to create XImage ‘object’ so that don’t have to pass large memory blob on each frame and rather just pass it’s identifier.

XImage *WindowBuffer = XCreateImage(MainDisplay, VisualInfo.visual, VisualInfo.depth, ZPixmap, Offset, (char *)Buffer.Memory, WindowWidth, WindowHeight, BitsPerPixel, BytesBetweenScanlines);

At the same time we started using ConfigureNotify event which fires every time window configuration changes or put simply when the window is resized. With the event comes information about new window dimensions which we use create new XImage object. XDestroyImage is used to destroy image to not leak memory.

In this example we slightly changed our approach to event handling. Previously we used to process one event, then do rendering, then process one more event and then do rendering. In new example we first process all available events and then do rendering. If we don’t have any events we just continously render until we get events. This is very wasteful but good enough for this example. If it were a game the we could sleep some time that is left in the frame. For retained mode normal apps we could just not do anything untill we get events.

The rendering is done with a just putting pixels by hand onto the image. In this example we first clear all pixels to a specific color. Then we render a simple square onto the same image based on the dynamic offset which changes on up, down, left, right arrow keys. This create a feeling of moving object.

And once we done with rendering objects onto the image we just send the image to the server with XPutImage(…). This tells X server to get this image and blit it onto a Drawable. In this case our drawable is whole window.

XPutImage(MainDisplay, MainWindow, Context, WindowBuffer, SourceX, SourceY, DestX, DestY, WindowWidth, WindowHeight);

Conclusion

Moving from general Xlib rendering was actually quite easy if we don’t count that we have to actually implement software rendering functions ourself (or we could inculde some libraries to simplify the task). The difference with the previous approach is that we can do all the rendering in “locally” and is faster compared to previous approach. Of course in current day and age doing software rendering is not needed in 99% of the cases. Better approach is to utilise GPU and delegate all the rendering to hardware acceleration. It will be explained in the next section.

Here is example build with this method described in this post.