Skip to content

07. Framebuffer Graphics - Drawing to the Screen

Moving beyond serial console, let's display graphics directly on screen using the Raspberry Pi's framebuffer. This enables rich visual output for games, GUIs, and animations.

What is a Framebuffer?

A framebuffer is a region of video memory that represents the screen. Each pixel's color is stored as a value in this buffer, and the GPU continuously reads from it to display on the monitor.

How it Works

flowchart LR
    CPU["CPU"] -->|"Writes pixels"| FB["Framebuffer<br/>(RAM)"]
    FB -->|"Reads continuously"| GPU["GPU"]
    GPU -->|"HDMI Signal"| Display["Monitor/Display"]

    style FB fill:#ffeb3b
  1. CPU: Writes pixel colors to framebuffer memory
  2. GPU: Reads framebuffer and outputs to display
  3. Display: Shows the pixels on screen

Mailbox Protocol

On Raspberry Pi, the CPU communicates with the GPU via a mailbox interface. This is a message-passing system where:

  • CPU sends requests to GPU
  • GPU processes and responds
  • Used for: allocating framebuffers, setting resolution, getting hardware info

Mailbox Registers (RPi 4)

Register Offset Purpose
MAILBOX_READ 0x00 Read messages from GPU
MAILBOX_STATUS 0x18 Check if mailbox is full/empty
MAILBOX_WRITE 0x20 Write messages to GPU

Base address: 0xFE00B880 (RPi 4)

Mailbox Channels

Channel Use
0 Power management
1 Framebuffer (deprecated)
8 Property tags (we use this)

Property Tag Format

Messages are structured as property tags:

Index Field Description
buffer[0] Total Size Total message size in bytes
buffer[1] Request Code 0 for request
buffer[2] TAG_ID Property tag identifier
buffer[3] Value Size Size of value buffer
buffer[4] Request/Response 0 for request
buffer[5...N-2] Values Tag-specific values
buffer[N-1] End Tag 0 to mark end
1
2
3
4
5
6
7
8
uint32_t buffer[N];
buffer[0] = size;           // Total size in bytes
buffer[1] = 0;              // Request code
buffer[2] = TAG_ID;         // e.g., set resolution
buffer[3] = VALUE_SIZE;     // Size of value buffer
buffer[4] = 0;              // Request/response indicator
buffer[5...] = values;      // Tag-specific values
buffer[N-1] = 0;            // End tag

Implementing the Mailbox Driver

Header: include/mailbox.h

#define MAILBOX_CH_PROP  8

#define MBOX_TAG_FB_ALLOCATE    0x40001
#define MBOX_TAG_FB_SET_PHYS_WH 0x48003
#define MBOX_TAG_FB_SET_VIRT_WH 0x48004
#define MBOX_TAG_FB_SET_DEPTH   0x48005
#define MBOX_TAG_FB_GET_PITCH   0x40008
#define MBOX_TAG_FB_SET_VIRT_OFF 0x48009

int mailbox_call(uint8_t channel);
extern volatile uint32_t mailbox_buffer[36] __attribute__((aligned(16)));

16-Byte Alignment

The mailbox buffer must be 16-byte aligned. Use __attribute__((aligned(16))).

Driver: drivers/mailbox.c

int mailbox_call(uint8_t channel) {
    uint32_t r = ((uint32_t)((uint64_t)&mailbox_buffer) & ~0xF) | (channel & 0xF);

    // Wait until mailbox is not full
    while (*MAILBOX_STATUS & MAILBOX_FULL);

    // Write message address
    *MAILBOX_WRITE = r;

    // Wait for response
    while (1) {
        while (*MAILBOX_STATUS & MAILBOX_EMPTY);

        if (r == *MAILBOX_READ) {
            // Check response code
            return mailbox_buffer[1] == 0x80000000 ? 0 : -1;
        }
    }
}

Framebuffer Initialization

To get a framebuffer, we send multiple property tags to the GPU:

  1. Set physical dimensions (width × height)
  2. Set virtual dimensions (same as physical for now)
  3. Set color depth (32 bits = ARGB)
  4. Get pitch (bytes per line)
  5. Allocate framebuffer

Code: drivers/framebuffer.c

int framebuffer_init(uint32_t width, uint32_t height, uint32_t depth) {
    mailbox_buffer[0] = 35 * 4;  // Total size
    mailbox_buffer[1] = 0;       // Request

    // Set physical dimensions
    mailbox_buffer[2] = MBOX_TAG_FB_SET_PHYS_WH;
    mailbox_buffer[3] = 8;       // Value size
    mailbox_buffer[4] = 0;
    mailbox_buffer[5] = width;
    mailbox_buffer[6] = height;

    // Set virtual dimensions
    mailbox_buffer[7] = MBOX_TAG_FB_SET_VIRT_WH;
    mailbox_buffer[8] = 8;
    mailbox_buffer[9] = 0;
    mailbox_buffer[10] = width;
    mailbox_buffer[11] = height;

    // Set depth (32-bit ARGB)
    mailbox_buffer[12] = MBOX_TAG_FB_SET_DEPTH;
    mailbox_buffer[13] = 4;
    mailbox_buffer[14] = 0;
    mailbox_buffer[15] = depth;

    // Set virtual offset (critical for proper initialization!)
    mailbox_buffer[16] = MBOX_TAG_FB_SET_VIRT_OFF;
    mailbox_buffer[17] = 8;
    mailbox_buffer[18] = 0;
    mailbox_buffer[19] = 0;      // X offset
    mailbox_buffer[20] = 0;      // Y offset

    // Get pitch
    mailbox_buffer[21] = MBOX_TAG_FB_GET_PITCH;
    mailbox_buffer[22] = 4;
    mailbox_buffer[23] = 0;
    mailbox_buffer[24] = 0;

    // Allocate buffer
    mailbox_buffer[25] = MBOX_TAG_FB_ALLOCATE;
    mailbox_buffer[26] = 8;
    mailbox_buffer[27] = 0;
    mailbox_buffer[28] = 4096;   // Alignment (4096 required!)
    mailbox_buffer[29] = 0;

    // End tag
    mailbox_buffer[30] = 0;

    if (mailbox_call(MAILBOX_CH_PROP) != 0) {
        return -1;
    }

    // Check if allocation succeeded
    if (mailbox_buffer[28] == 0) {
        return -1;
    }

    // Extract framebuffer address and pitch from response
    fb_info.buffer = (uint32_t*)(uint64_t)(mailbox_buffer[28] & 0x3FFFFFFF);
    fb_info.pitch = mailbox_buffer[24];

    return 0;
}

Virtual Offset Tag Required

The MBOX_TAG_FB_SET_VIRT_OFF tag is essential for proper framebuffer initialization on Raspberry Pi 4. Without it, the screen will remain black even if other tags succeed.

4096-Byte Alignment

Use 4096 for alignment instead of 16. This ensures proper memory alignment for the GPU.

Framebuffer Address

The GPU returns a bus address. We must convert it to a CPU address:

fb_address = returned_address & 0x3FFFFFFF;  // Strip top bits

Drawing Pixels

Pixel Format (32-bit ARGB)

Each pixel is 4 bytes:

Byte 3 Byte 2 Byte 1 Byte 0
Alpha (0xFF) Red (0x00-0xFF) Green (0x00-0xFF) Blue (0x00-0xFF)

32-bit value: 0xAARRGGBB

Combine as: 0xAARRGGBB

Example colors: - Red: 0xFFFF0000 - Green: 0xFF00FF00 - Blue: 0xFF0000FF - White: 0xFFFFFFFF

Framebuffer Memory Layout

graph TB
    subgraph "Screen (Width × Height)"
        Row0["Row 0: pixel[0] pixel[1] ... pixel[width-1]"]
        Row1["Row 1: pixel[0] pixel[1] ... pixel[width-1]"]
        RowN["Row N: pixel[0] pixel[1] ... pixel[width-1]"]

        Row0 -.->|"pitch bytes"| Row1
        Row1 -.->|"pitch bytes"| RowN
    end

    Note["Pitch may include padding\npitch ≥ width × 4 bytes"]

    style Row0 fill:#e3f2fd
    style Row1 fill:#e3f2fd
    style RowN fill:#e3f2fd
    style Note fill:#fff9c4

Setting a Pixel

1
2
3
4
5
void fb_set_pixel(uint32_t x, uint32_t y, uint32_t color) {
    if (x >= fb_info.width || y >= fb_info.height) return;

    fb_info.buffer[y * (fb_info.pitch / 4) + x] = color;
}

Why pitch / 4? - Pitch is in bytes (total bytes per row) - Each pixel is 4 bytes (32 bits) - So one row has pitch / 4 pixels - Pitch may be larger than width × 4 due to alignment padding

Filling a Rectangle

1
2
3
4
5
6
7
void fb_fill_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint32_t color) {
    for (uint32_t row = y; row < y + h && row < fb_info.height; row++) {
        for (uint32_t col = x; col < x + w && col < fb_info.width; col++) {
            fb_set_pixel(col, row, color);
        }
    }
}

Drawing Lines (Bresenham's Algorithm)

Bresenham's algorithm draws lines efficiently without floating-point math:

void fb_draw_line(uint32_t x0, uint32_t y0, uint32_t x1, uint32_t y1, uint32_t color) {
    int dx = abs(x1 - x0);
    int dy = abs(y1 - y0);
    int sx = x0 < x1 ? 1 : -1;
    int sy = y0 < y1 ? 1 : -1;
    int err = dx - dy;

    while (1) {
        fb_set_pixel(x0, y0, color);
        if (x0 == x1 && y0 == y1) break;

        int e2 = 2 * err;
        if (e2 > -dy) { err -= dy; x0 += sx; }
        if (e2 < dx)  { err += dx; y0 += sy; }
    }
}

Color Helpers

Define colors as macros:

1
2
3
4
5
6
7
#define RGB(r, g, b) (0xFF000000 | ((r) << 16) | ((g) << 8) | (b))

#define COLOR_BLACK   RGB(0, 0, 0)
#define COLOR_WHITE   RGB(255, 255, 255)
#define COLOR_RED     RGB(255, 0, 0)
#define COLOR_GREEN   RGB(0, 255, 0)
#define COLOR_BLUE    RGB(0, 0, 255)

Building and Testing

1. Build

1
2
3
cd simpian-os/build
cmake -DBUILD_EXAMPLES=ON ..
make

2. Deploy

Copy framebuffer_demo.img to SD card (rename to kernel8.img).

Important: Add the following to config.txt on the SD card:

# Required for framebuffer resolution to be applied correctly
disable_overscan=1

Without this setting, the resolution requested in framebuffer_init() may not be applied correctly, and the display may use default or unexpected dimensions.

3. Connect Display

Connect HDMI cable to monitor.

4. Expected Output

On screen: - 3 large rectangles (red, green, blue) at top - 2 rectangles (yellow, cyan) at bottom - White diagonal lines - Bouncing white square animation

On serial:

Framebuffer Graphics Demo
==========================

Initializing framebuffer...
Framebuffer initialized:
  Resolution: 1920x1080
  Depth: 32 bpp

Drawing rectangles...
Drawing lines...
Graphics demo complete!
Starting animation (bouncing square)...

Performance Considerations

Double Buffering

For smooth animation without flicker, use double buffering:

  1. Draw to back buffer
  2. Swap back and front buffers
  3. Repeat

Implement by setting virtual height = 2 × physical height.

DMA for Fast Copy

For large fills, use DMA instead of CPU loops:

// Future improvement: Use DMA controller
dma_fill(fb_buffer, color, size);

Complete Source Code

Troubleshooting

Problem Solution
Black screen Check HDMI connection, verify mailbox response
Wrong colors Check byte order (ARGB vs RGBA)
Garbled display Incorrect pitch calculation
Slow drawing Use DMA, reduce resolution, or optimize loops

Important Notes

Framebuffer Address

The framebuffer address from GPU is a bus address, not a CPU address. Mask with 0x3FFFFFFF to convert.

Resolution Limits

Maximum resolution depends on GPU memory. 1920×1080 @ 32bpp works reliably.

What's Next?

With graphics working, we can now: - Implement text rendering (fonts and console) - Create a graphical shell or desktop environment - Build games with sprites and animations

The next article covers Text Rendering, which will allow us to print text on screen instead of just colored rectangles!