ARM64 OS Handbook
🔍

Chapter 34: Framebuffer

What You Will Learn in This Chapter
  • What a framebuffer is and how it drives a display
  • How to query and set display resolution from the device tree
  • How to draw pixels, rectangles, and text to the framebuffer
  • Font rendering with a simple bitmap font
  • Double buffering for flicker-free updates
  • Our framebuffer driver implementation

34.1 What is a Framebuffer?

A framebuffer is a region of memory that contains pixel data for a display. The display controller reads this memory and continuously sends it to the screen. Writing pixel values to the framebuffer memory changes what appears on the screen.

On QEMU virt with the virtio-gpu device (or the simple framebuffer from the device tree), the kernel receives a framebuffer address, width, height, and pixel format from the firmware or device tree.

34.2 Framebuffer Initialization

/* Framebuffer info from device tree or bootloader */
struct framebuffer {
    uint64_t phys_addr;       /* Physical address of framebuffer memory */
    uint64_t virt_addr;       /* Mapped virtual address */
    int width;                /* Width in pixels */
    int height;               /* Height in pixels */
    int pitch;                /* Bytes per row */
    int bpp;                  /* Bits per pixel (32 for RGBA32) */
    int size;                 /* Total size in bytes */
};

struct framebuffer fb;

/* Initialize framebuffer from device tree */
int fb_init(void) {
    /* Find the "simple-framebuffer" node in the device tree */
    struct device_node *node = of_find_compatible(NULL, "simple-framebuffer");
    if (!node) {
        /* No framebuffer: fall back to UART-only output */
        return -1;
    }

    fb.phys_addr = of_read_reg(node, 0);
    fb.width = of_read_u32(node, "width");
    fb.height = of_read_u32(node, "height");
    fb.bpp = of_read_u32(node, "stride") * 8 / fb.width;
    fb.pitch = of_read_u32(node, "stride");
    fb.size = fb.pitch * fb.height;

    /* Map framebuffer into kernel address space */
    fb.virt_addr = (uint64_t)mmap_phys(fb.phys_addr, fb.size,
                                        MMIO_FLAGS);

    return 0;
}

/* Colors (grayscale, per our theme) */
#define COLOR_BLACK   0xFF000000
#define COLOR_WHITE   0xFFFFFFFF
#define COLOR_GRAY1   0xFF333333
#define COLOR_GRAY2   0xFF666666
#define COLOR_GRAY3   0xFF999999
#define COLOR_GRAY4   0xFFCCCCCC

34.3 Pixel Drawing Functions

/* Plot a single pixel */
void fb_plot_pixel(int x, int y, uint32_t color) {
    if (x < 0 || x >= fb.width || y < 0 || y >= fb.height) return;
    uint32_t *pixel = (uint32_t *)(fb.virt_addr + y * fb.pitch + x * 4);
    *pixel = color;
}

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

/* Clear entire screen */
void fb_clear(uint32_t color) {
    fb_fill_rect(0, 0, fb.width, fb.height, color);
}

/* Draw a horizontal line */
void fb_draw_hline(int x, int y, int length, uint32_t color) {
    uint32_t *start = (uint32_t *)(fb.virt_addr + y * fb.pitch + x * 4);
    for (int i = 0; i < length; i++) start[i] = color;
}

/* Draw a vertical line */
void fb_draw_vline(int x, int y, int length, uint32_t color) {
    for (int i = 0; i < length; i++)
        fb_plot_pixel(x, y + i, color);
}

/* Draw a rectangle outline */
void fb_draw_rect(int x, int y, int w, int h, uint32_t color) {
    fb_draw_hline(x, y, w, color);
    fb_draw_hline(x, y + h - 1, w, color);
    fb_draw_vline(x, y, h, color);
    fb_draw_vline(x + w - 1, y, h, color);
}

34.4 Font Rendering

/* Simple 8x16 bitmap font (each character is 16 bytes, 8 pixels wide) */
extern const uint8_t font8x16[256][16];

/* Draw a character at position (x, y) */
void fb_putchar(int x, int y, char c, uint32_t fg, uint32_t bg) {
    const uint8_t *glyph = font8x16[(unsigned char)c];

    for (int row = 0; row < 16; row++) {
        uint8_t bits = glyph[row];
        for (int col = 0; col < 8; col++) {
            uint32_t color = (bits & (1 << (7 - col))) ? fg : bg;
            fb_plot_pixel(x + col, y + row, color);
        }
    }
}

/* Draw a string at position (x, y) */
void fb_puts(int x, int y, const char *s, uint32_t fg, uint32_t bg) {
    int cursor_x = x;
    int cursor_y = y;

    while (*s) {
        if (*s == '\n') {
            cursor_x = x;
            cursor_y += 16 + 2;  /* Line feed with 2-pixel spacing */
        } else {
            fb_putchar(cursor_x, cursor_y, *s, fg, bg);
            cursor_x += 8 + 1;   /* Character width + 1 pixel spacing */
        }
        s++;
    }
}

/* Scroll the entire framebuffer up by one line */
void fb_scroll(void) {
    int line_bytes = 16 * fb.pitch;  /* 16 pixel rows per character line */
    memmove((void *)fb.virt_addr,
            (void *)(fb.virt_addr + line_bytes),
            fb.size - line_bytes);
    /* Clear the bottom line */
    fb_fill_rect(0, fb.height - 16, fb.width, 16, COLOR_BLACK);
}

34.5 Double Buffering

Double buffering prevents flicker by drawing to an off-screen buffer, then copying the result to the visible framebuffer:

/* Double buffer */
static uint32_t *back_buffer = NULL;

void fb_init_double_buffer(void) {
    back_buffer = kmalloc(fb.size);
}

void fb_clear_back(uint32_t color) {
    memset(back_buffer, color, fb.size);
}

void fb_putchar_back(int x, int y, char c, uint32_t fg, uint32_t bg) {
    /* Same as fb_putchar but writes to back_buffer */
    const uint8_t *glyph = font8x16[(unsigned char)c];
    for (int row = 0; row < 16; row++) {
        uint8_t bits = glyph[row];
        for (int col = 0; col < 8; col++) {
            uint32_t color = (bits & (1 << (7 - col))) ? fg : bg;
            back_buffer[(y + row) * fb.width + (x + col)] = color;
        }
    }
}

/* Swap buffers: copy back buffer to visible framebuffer */
void fb_swap(void) {
    memcpy((void *)fb.virt_addr, back_buffer, fb.size);
}

34.6 Our Implementation

Our framebuffer driver (drivers/video/fb.c) provides:

  • Simple framebuffer: uses the device tree "simple-framebuffer" node
  • Virtual framebuffer: for QEMU without a display device
  • Terminal emulator: fb_terminal provides a scrollable text console on the framebuffer
  • Console switch: the kernel can output to both UART and framebuffer terminal
  • Double buffering: for smooth animation in graphics applications

34.7 Exercises

Exercise 1: Draw a Circle

Implement a circle-drawing function using the midpoint circle algorithm. Test by drawing concentric circles.

Exercise 2: Terminal Emulator

Implement a simple terminal that displays text, supports cursor positioning, and scrolling. Test by printing kernel log messages to the framebuffer.

34.8 Summary

The framebuffer provides pixel-level access to the display. Our kernel initializes it from the device tree, then provides functions for drawing pixels, lines, rectangles, and text with a bitmap font. Double buffering eliminates flicker for dynamic content. The framebuffer is the foundation for the graphics subsystem (Chapter 40) and window manager (Chapter 41).