Article by Ayman Alheraki on January 11 2026 10:37 AM
With the advancement of GPU technologies, some might wonder: why would anyone be interested in drawing graphics using only the CPU? Yet, there are solid reasons, including:
Developing custom graphical operating systems or emulators.
Creating diagnostic or educational tools.
Learning the core of graphics programming from the ground up.
Working in limited environments without GPU access, such as embedded systems.
This guide explains how to build basic graphics routines relying entirely on the CPU, without OpenGL, DirectX, or any GPU acceleration, and it covers both Linux and Windows systems.
The core idea is that the screen is essentially a linear memory area (called a framebuffer) where each pixel has its place. If you can write into this memory correctly, you can control what appears on the screen.
Steps:
Access the framebuffer through OS services or memory-mapped files.
Calculate the pixel’s position in memory.
Determine the pixel’s color in the expected format.
Write the pixel color directly to the framebuffer.
On Linux systems with framebuffer support, the framebuffer is usually exposed as /dev/fb0, a device that maps directly to the display memory.
Open the framebuffer:
int fbfd = open("/dev/fb0", O_RDWR);Get screen information:
xxxxxxxxxxstruct fb_var_screeninfo vinfo;ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo);Calculate the screen size in bytes:
int screensize = vinfo.yres_virtual * vinfo.xres_virtual * vinfo.bits_per_pixel / 8;Map the framebuffer into your process’s memory:
char* fbp = (char*) mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);Calculate a pixel location:
int x = 100, y = 150;int location = (x + vinfo.xoffset) * (vinfo.bits_per_pixel/8) + (y + vinfo.yoffset) * vinfo.line_length;Set the pixel color to red (in 24-bit color):
fbp[location] = 0x00; // Bluefbp[location+1] = 0x00; // Greenfbp[location+2] = 0xFF; // RedClean up:
munmap(fbp, screensize);close(fbfd);
int main() { int fb = open("/dev/fb0", O_RDWR); if (fb < 0) return 1;
struct fb_var_screeninfo vinfo; ioctl(fb, FBIOGET_VSCREENINFO, &vinfo);
long screensize = vinfo.yres_virtual * vinfo.xres_virtual * vinfo.bits_per_pixel / 8; char* fbp = mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb, 0);
for (int y = 100; y < 200; y++) { for (int x = 100; x < 200; x++) { long location = (x + vinfo.xoffset) * (vinfo.bits_per_pixel/8) + (y + vinfo.yoffset) * vinfo.line_length;
fbp[location] = 0x00; // Blue fbp[location+1] = 0x00; // Green fbp[location+2] = 0xFF; // Red if (vinfo.bits_per_pixel == 32) fbp[location+3] = 0; } }
munmap(fbp, screensize); close(fb); return 0;}
Direct access to video memory is not allowed on modern versions of Windows, but graphics can still be drawn using Windows GDI (Graphics Device Interface) by preparing a memory buffer and displaying it using Windows API.
Create a window using the Windows API.
Prepare a raw pixel buffer in memory.
Draw into the buffer manually.
Display the buffer using StretchDIBits() or similar.
unsigned char buffer[WIDTH * HEIGHT * 4];
void DrawPixel(int x, int y, unsigned char r, unsigned char g, unsigned char b) { int offset = (y * WIDTH + x) * 4; buffer[offset] = b; buffer[offset + 1] = g; buffer[offset + 2] = r; buffer[offset + 3] = 0;}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { if (msg == WM_PAINT) { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps);
BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = WIDTH; bmi.bmiHeader.biHeight = -HEIGHT; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; bmi.bmiHeader.biCompression = BI_RGB;
StretchDIBits(hdc, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT, buffer, &bmi, DIB_RGB_COLORS, SRCCOPY);
EndPaint(hwnd, &ps); return 0; } else if (msg == WM_DESTROY) { PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam);}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPSTR lpCmd, int nCmdShow) { WNDCLASS wc = {0}; wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = "MyWindowClass"; RegisterClass(&wc);
HWND hwnd = CreateWindow("MyWindowClass", "CPU Graphics", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, WIDTH, HEIGHT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, nCmdShow);
for (int y = 200; y < 280; y++) for (int x = 280; x < 360; x++) DrawPixel(x, y, 255, 0, 0);
MSG msg = {0}; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
return 0;}
| Category | Instructions | Purpose |
|---|---|---|
| Memory Access | MOV, STOSB, REP STOSB | Write color values to framebuffer |
| Arithmetic | IMUL, ADD, LEA, SHL | Calculate memory offset for each pixel |
| Looping | LOOP, JNZ, JMP | Iterate over pixel rows and columns |
| Logic | AND, OR, XOR | Pixel blending, masks, or color filters |
Line drawing (e.g., Bresenham's algorithm).
Image viewers (for BMP or raw formats).
Simple 2D games (Tetris, Snake, etc.).
Mathematical visualizations and fractals.
Basic 3D software rendering (for advanced developers).
Building a graphics engine or drawing directly to the screen using the CPU is a rewarding experience. It teaches the fundamentals of how images are represented in memory, how displays work, and what role the GPU actually plays.
Whether on Linux or Windows, low-level graphics programming enhances your understanding of performance, memory, and rendering principles. While not suitable for heavy or commercial applications today, it is still a valuable learning path for advanced programmers and system developers.