ARM64 OS Handbook
🔍

Chapter 40: Graphics

What You Will Learn in This Chapter
  • How framebuffer graphics work at the hardware level
  • How to draw pixels, lines, rectangles, and text
  • How double buffering prevents screen tearing
  • How to expose graphics to user space via an mmap interface
  • How to implement a simple font renderer
  • Our graphics subsystem implementation

40.1 The Framebuffer

A framebuffer is a region of memory that the display hardware reads to produce pixels on the screen. Each pixel is represented by one or more bytes. Writing to the framebuffer memory changes what appears on the display.

QEMU virt provides a framebuffer via the virtio-gpu device or the simple framebuffer described in the device tree. The framebuffer is mapped at a physical address (obtained from the device tree) with known width, height, and pixel format.

40.2 Pixel Formats

The most common format for QEMU virt is BGRA32 (also called XRGB8888): 4 bytes per pixel, with blue, green, red, and unused alpha channels:

/* BGRA32 pixel format: 32 bits per pixel */
#define PIXEL_BLUE   0
#define PIXEL_GREEN  1
#define PIXEL_RED    2
#define PIXEL_ALPHA  3

typedef uint32_t pixel_t;

/* Create a pixel from RGB components */
pixel_t rgb(uint8_t r, uint8_t g, uint8_t b) {
    return (b << 16) | (g << 8) | r;
}

#define WHITE  rgb(255, 255, 255)
#define BLACK  rgb(0, 0, 0)
#define GRAY   rgb(128, 128, 128)

40.3 Drawing Primitives

The framebuffer is a 2D array of pixels. Drawing operations write to specific (x, y) coordinates:

/* Global framebuffer info (filled by kernel init) */
typedef struct {
    void    *base;      /* Virtual address of framebuffer */
    uint32_t width;
    uint32_t height;
    uint32_t pitch;     /* Bytes per row (may be larger than width*4) */
    uint32_t bpp;       /* Bits per pixel (32) */
} fb_info_t;

fb_info_t fb;

/* Set a single pixel. No bounds checking. */
void fb_put_pixel(int x, int y, pixel_t color) {
    uint32_t *pixels = (uint32_t *)fb.base;
    pixels[y * (fb.pitch / 4) + x] = color;
}

/* Fill a rectangle */
void fb_fill_rect(int x, int y, int w, int h, pixel_t color) {
    for (int row = 0; row < h; row++) {
        for (int col = 0; col < w; col++) {
            fb_put_pixel(x + col, y + row, color);
        }
    }
}

/* Draw a horizontal line (faster than one pixel at a time) */
void fb_hline(int x, int y, int len, pixel_t color) {
    uint32_t *pixels = (uint32_t *)fb.base;
    uint32_t *row = &pixels[y * (fb.pitch / 4) + x];
    for (int i = 0; i < len; i++) row[i] = color;
}

/* Draw a vertical line */
void fb_vline(int x, int y, int len, pixel_t color) {
    uint32_t *pixels = (uint32_t *)fb.base;
    int stride = fb.pitch / 4;
    for (int i = 0; i < len; i++) {
        pixels[(y + i) * stride + x] = color;
    }
}

40.4 Double Buffering

If we draw directly to the visible framebuffer, the user sees partial updates (screen tearing). Double buffering solves this by drawing to an off-screen buffer, then copying the result to the visible buffer in one operation:

/* Back buffer for double buffering */
static uint32_t *back_buffer = NULL;
static size_t back_buffer_size = 0;

/* Initialize back buffer */
void fb_init_back_buffer(void) {
    back_buffer_size = fb.width * fb.height * 4;
    back_buffer = (uint32_t *)kmalloc(back_buffer_size);
    fb_clear_back_buffer(BLACK);
}

/* Clear the back buffer */
void fb_clear_back_buffer(pixel_t color) {
    uint32_t *fb32 = (uint32_t *)fb.base;
    for (size_t i = 0; i < fb.width * fb.height; i++) {
        back_buffer[i] = color;
    }
}

/* Flip: copy back buffer to visible framebuffer */
void fb_flip(void) {
    memcpy(fb.base, back_buffer, back_buffer_size);
}

/* Draw to back buffer instead */
void fb_put_pixel_buffered(int x, int y, pixel_t color) {
    back_buffer[y * fb.width + x] = color;
}

40.5 Font Rendering

Text is drawn by copying glyph bitmaps from a font. A minimal bitmap font stores each character as an 8x16 or 8x8 grid of bits. A 1 bit means draw the foreground color; a 0 bit means leave unchanged:

/* 8x16 bitmap font: 16 bytes per glyph, one bit per pixel */
extern const uint8_t font_8x16[256][16];

/* Draw a character at (x, y). Foreground = fg, background = bg. */
void fb_put_char(int x, int y, char c, pixel_t fg, pixel_t bg) {
    const uint8_t *glyph = font_8x16[(unsigned char)c];
    for (int row = 0; row < 16; row++) {
        uint8_t bits = glyph[row];
        for (int col = 0; col < 8; col++) {
            if (bits & (0x80 >> col)) {
                fb_put_pixel_buffered(x + col, y + row, fg);
            } else if (bg != 0xFFFFFFFF) {
                /* Transparent background if bg is special value */
                fb_put_pixel_buffered(x + col, y + row, bg);
            }
        }
    }
}

/* Draw a null-terminated string starting at (x, y) */
void fb_put_string(int x, int y, const char *str, pixel_t fg, pixel_t bg) {
    while (*str) {
        fb_put_char(x, y, *str++, fg, bg);
        x += 8;
    }
}

40.6 User-Space Access

User-space programs access the framebuffer through an mmap() syscall on the framebuffer device. The kernel maps the framebuffer physical memory into the process's address space:

/* Framebuffer device: /dev/fb0 */
/* Kernel-side mmap handler for framebuffer */
void *fb_mmap(struct proc *proc, size_t offset, size_t size) {
    /* Map framebuffer physical memory into user address space */
    uint64_t user_addr = find_free_pages(proc, size);
    uint64_t fb_phys = (uint64_t)fb.base_phys + offset;
    for (size_t i = 0; i < size; i += PAGE_SIZE) {
        map_page(proc->page_table, user_addr + i,
                 fb_phys + i, PAGE_USER | PAGE_WRITE);
    }
    return (void *)user_addr;
}

/* User-space usage */
int fb_fd = open("/dev/fb0", O_RDWR);
uint32_t *fb_ptr = mmap(NULL, fb_size, PROT_READ | PROT_WRITE,
                         MAP_SHARED, fb_fd, 0);
fb_ptr[y * fb_width + x] = rgb(255, 255, 255);  /* Draw a white pixel */

40.7 Our Implementation

Our graphics subsystem provides:

  • Framebuffer initialization: reads resolution from the device tree FDT
  • Drawing primitives: put_pixel, fill_rect, hline, vline, circle
  • Double buffering: off-screen back buffer with flip()
  • Bitmap font: 8x16 ASCII font with 95 printable glyphs
  • Text rendering: put_char, put_string with foreground/background colors
  • User-space API: /dev/fb0 with mmap support

The framebuffer driver is initialized during kernel startup after the MMU is enabled. By default it uses 1024x768 resolution at 32 bits per pixel.

40.8 Exercises

Exercise 1: Draw a Circle

Implement the Bresenham circle algorithm to draw outlined and filled circles on the framebuffer.

Exercise 2: Scrolling Terminal

Build a simple text terminal on the framebuffer that scrolls when the cursor reaches the bottom of the screen.

Exercise 3: Bitmap Font Generator

Write a tool that converts a .bdf font file into the C array format used by our font renderer.

40.9 Summary

The framebuffer is a simple memory-mapped display: write pixel values to the right address and they appear on screen. Double buffering prevents tearing. A bitmap font turns glyph bit patterns into visible text. By exposing the framebuffer through mmap, user-space programs can draw directly to the screen. This graphics layer forms the foundation for terminal emulators, window managers, and graphical applications.