LEVENT KAYA'S WEBSITE

CONTACT    ARCHIVE    RSS     DONATE


Why and how did I make a zero-dependency graphics library from scratch

· tags: c, linux, graphics, framebuffer, and low-level

Current GitHub stargazers of fbgl:
GitHub Repo stars

Current GitHub contributors of fbgl:
GitHub contributors

fbgl texture

Hello everyone, today I will tell you why and how I programmed my graphics library fbgl for Linux from scratch, with no dependencies other than Linux headers.


How Did It Start?

I can’t play games as much as I used to, but as a player and a programmer, there is one game series that I really love: Doom

I think I was 11 or 12 when I first played Doom (the first I played was Doom 3), but the one that amazed me the most was the original Doom from 1993. As time passed, I read about id Software and John Carmack; when the sources became public I dove into them. Eventually I wanted to write my own ray casting engine.


The Pursuit

If you want to display graphics on the screen, the basic question is: how do I draw on the screen?

I encountered dozens of libraries: SDL, SFML, raylib, and bigger stacks like OpenGL and Vulkan. For my tiny needs (2D textures + a bit of math), pulling in huge dependencies felt wrong. So I decided to write my own simple library.


A Revolt Against Bloatware

I love reinventing things from scratch. This time it wasn’t just curiosity; it was frustration. I wanted a user-space Linux library with zero external deps — just Linux headers and /dev/fb0. After all, Carmack didn’t have Vulkan.


The Technical Challenge

When I decided to create fbgl (Framebuffer Graphics Library), I knew I was signing up for a challenge. The Linux framebuffer is a direct, low-level way to draw: memory you write maps to pixels.

Goals:


Understanding the Framebuffer

In Linux, the framebuffer (/dev/fb0) is a device that represents your display as a memory-mapped region. Writing to it directly manipulates the screen.

The initialization in fbgl_init is intentionally simple:

int fbgl_init(const char *device, fbgl_t *fb)
{
    fb->fd = device == NULL ? open(DEFAULT_FB, O_RDWR) :
                              open(device, O_RDWR);
    
    // Retrieve screen information
    ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->finfo);
    ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vinfo);

    // Memory map the framebuffer
    fb->pixels = (uint32_t *)mmap(NULL, fb->screen_size,
                                  PROT_READ | PROT_WRITE, 
                                  MAP_SHARED, fb->fd, 0);
    
    return 0;
}

Key Design Decisions

Minimal API Surface

fbgl has a minimal, intuitive API. Want to draw a line? fbgl_draw_line(). Circle? fbgl_draw_circle_filled().

Texture Loading

TGA textures are supported because the format is simple. The loader handles 24/32‑bit textures, orientation, and color conversion.

fbgl_tga_texture_t *fbgl_load_tga_texture(const char *path)
{
    // Supports 24/32 bit, handles orientation, converts formats
}

Keyboard Input

Simple, non-blocking keyboard input using terminal raw mode:

fbgl_key_t fbgl_get_key(void)
{
    char c;
    ssize_t bytes_read = read(STDIN_FILENO, &c, 1);

    // Translate raw input to meaningful key events
}

Text Rendering

fbgl can load and render PSF1 bitmap fonts:

fbgl_render_psf1_text(fb, font, "Hello, World!", 10, 10, FBGL_RGB(255, 255, 255));

text


Challenges and Solutions

Hardware variability — query with ioctl() and abstract to a stable API.
Debugging low-level code — log everything; test on multiple devices.
Performance — dirty rectangles, efficient memory mapping.


Example Application: A Simple Game

#define FBGL_IMPLEMENTATION
#include "fbgl.h"

int main() {
    fbgl_t fb;
    if (fbgl_init(NULL, &fb) != 0) {
        return -1;
    }

    fbgl_clear(FBGL_RGB(0, 0, 0)); // Clear screen to black

    fbgl_point_t ball_pos = {50, 50};
    fbgl_point_t ball_size = {10, 10};
    fbgl_point_t direction = {1, 1};

    while (1) {
        fbgl_clear(FBGL_RGB(0, 0, 0)); // Clear screen

        // Update ball position
        ball_pos.x += direction.x;
        ball_pos.y += direction.y;

        // Bounce off edges
        if (ball_pos.x <= 0 || ball_pos.x + ball_size.x >= fb.width) {
            direction.x = -direction.x;
        }
        if (ball_pos.y <= 0 || ball_pos.y + ball_size.y >= fb.height) {
            direction.y = -direction.y;
        }

        // Draw ball
        fbgl_draw_rectangle_filled(
            ball_pos,
            (fbgl_point_t){ball_pos.x + ball_size.x, ball_pos.y + ball_size.y},
            FBGL_RGB(255, 0, 0), &fb
        );

        usleep(10000); // Sleep for 10ms
    }

    fbgl_destroy(&fb);
    return 0;
}

The Road Ahead


Conclusion

Building fbgl started as a personal challenge against bloat. It’s not the fastest or the most feature‑rich, but I understand every line. If you like tinkering, try stripping away layers of abstraction and dive into the fundamentals.

Repo: https://github.com/lvntky/fbgl


← Back to Articles