Why and how did I make a zero-dependency graphics library from scratch
Current GitHub stargazers of fbgl:
Current GitHub contributors of fbgl:
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:
- Zero external dependencies
- Work directly with the Linux framebuffer
- Provide basic but solid 2D primitives
- Be small enough to understand completely
- Be easily embeddable
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));
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
- More image formats: PNG, BMP
- Hardware acceleration: explore GPU paths while keeping simplicity
- Expanded input: mouse and gamepads
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