Skip to content

Framebuffer Basics (No Window Manager)

Time: 30 min | Prerequisites: SSH Login, Display Setup

This tutorial shows how to draw directly to the framebuffer on an embedded Linux system without X11 or Wayland.


Learning Objectives

By the end of this tutorial you will be able to:

  • Query framebuffer resolution, pixel format, and stride from sysfs
  • Display an image on a bare framebuffer using fbi
  • Write pixels directly to /dev/fb0 using Python and mmap
  • Understand RGB565 vs XRGB8888 pixel formats
The Linux Graphics Stack -- Level 1: Framebuffer

Linux provides three levels of graphics, each trading simplicity for capability:

  • Level 1: fbdev (framebuffer) -- Write pixels directly to /dev/fb0. The display controller reads this memory at 60 Hz and sends the pixels to the monitor. No window manager, no GPU, no synchronisation. This is the simplest path and the one used in this tutorial.
  • Level 2: DRM/KMS -- The modern replacement for fbdev. Provides hardware page flipping synchronised to VBlank (no tearing), mode enumeration from EDID, and hardware overlay planes. Every active display driver in the kernel supports DRM.
  • Level 3: Wayland or X11 -- A full compositor that manages multiple windows, input routing, and GPU-accelerated rendering. Powerful but heavy -- adds seconds to boot time and hundreds of MB of RAM. Unnecessary for single-app embedded products.

A framebuffer is a contiguous block of memory whose size is stride x height bytes. The stride (line length) may be larger than width x bytes_per_pixel due to hardware alignment. The kernel exposes this memory as /dev/fb0 -- every byte you write directly changes a pixel on the display.

For deeper reading see the Graphics Stack reference page.

Course Source Repository

This tutorial references source files from the course repository. If you haven't cloned it yet on your Pi:

cd ~
git clone https://github.com/OE-KVK-H2IoT/embedded-linux.git

Source files for this tutorial are in ~/embedded-linux/solutions/framebuffer-basics/.


How Does This Reach the Display?

When you SSH into a Raspberry Pi and run these scripts, pixels appear on the HDMI monitor — even though you never "selected" that screen or opened a window. This is confusing if you are used to desktop environments where you must open a window first. Here is why it works:

┌─────────────────────────────────────────────────────────┐
│  RAM                                                    │
│  ┌──────────────────────────┐                           │
│  │  Framebuffer memory      │◄── your program writes    │
│  │  (e.g., 1920×1080×4 B)   │    pixels here via mmap   │
│  └──────────┬───────────────┘                           │
│             │  DMA (Direct Memory Access)               │
│             ▼                                           │
│  ┌──────────────────────────┐                           │
│  │  Display controller      │  Reads FB memory 60×/sec  │
│  │  (part of the SoC)       │  line by line, left→right │
│  └──────────┬───────────────┘                           │
│             │  HDMI / DSI signals                       │
└─────────────┼───────────────────────────────────────────┘
       ┌──────────────┐
       │ HDMI Monitor │  Receives pixel stream
       └──────────────┘

Key points:

  • The display controller is a hardware block inside the SoC that continuously scans framebuffer memory at the refresh rate (typically 60 Hz) and converts each pixel value into electrical signals for HDMI (or DSI for ribbon-cable displays).
  • There is no window manager involved. The framebuffer is the entire screen — pixel (0,0) is the top-left corner of the monitor. When the console shows a blinking cursor, that cursor is also being drawn into the framebuffer by a kernel component (fbcon).
  • When you run fb_draw.py, your pixels overwrite whatever was in framebuffer memory — including the text console. The console text is gone because you wrote black pixels over it. When the script exits, the console does not automatically redraw (run clear or switch TTYs with Alt+F1 to get it back).
  • You do not "select" a display — /dev/fb0 is wired to whichever connector the bootloader configured at startup (usually HDMI-1). There is exactly one framebuffer device per display output.
Note

"Where did the terminal go?" — The Linux text console (fbcon) draws characters into the same framebuffer memory your scripts write to. When you clear the framebuffer to black, the terminal text disappears. It is not a different layer — it is the same pixel buffer. To restore the console, press Alt+F1 (on a local keyboard) or reboot. Over SSH, run: sudo chvt 1 to switch back to TTY 1.


1. Check Framebuffer Device

ls -l /dev/fb*

If you see /dev/fb0, framebuffer is available. The device belongs to the video group — you need sudo or membership in that group to write to it.


2. Query Resolution and Format

Before writing any pixels, you must know the framebuffer's resolution, pixel format, and stride. Getting the format wrong produces garbled colors; getting the stride wrong produces a diagonal smear.

fbset -i

You can also read these values programmatically from sysfs:

cat /sys/class/graphics/fb0/virtual_size    # e.g., 1920,1080
cat /sys/class/graphics/fb0/bits_per_pixel  # e.g., 16 or 32
cat /sys/class/graphics/fb0/stride          # bytes per row
Note

Stride vs width: A 1920-pixel-wide display at 16 bpp uses 1920 × 2 = 3840 bytes per row. But the hardware may round up to 4096 for alignment. Always use stride (from sysfs or fbset), never width × bpp, when calculating pixel offsets.

Checkpoint

fbset -i shows a valid resolution and the LineLength field is non-zero. Note your resolution and bits_per_pixel — you'll need them below.


3. Display a Test Image

Install tools:

sudo apt-get update
sudo apt-get install -y fbi python3-pil
Warning

Use sudo apt-get install python3-pilnot pip3 install pillow. Recent Raspberry Pi OS (Debian Bookworm+) blocks system-wide pip installs with PEP 668. The python3-pil apt package is the correct way to install Pillow system-wide.

Create a test image and display it. First, create the script file:

cat > fb_test_image.py << 'EOF'
#!/usr/bin/env python3
"""Generate a test image matching the framebuffer resolution."""
import sys

# Read actual framebuffer resolution from sysfs
with open("/sys/class/graphics/fb0/virtual_size") as f:
    width, height = [int(x) for x in f.read().strip().split(",")]

print(f"Framebuffer: {width}x{height}")

from PIL import Image, ImageDraw

img = Image.new("RGB", (width, height), (0, 0, 0))
draw = ImageDraw.Draw(img)

# Draw colored bars
bar_h = height // 4
colors = [(255, 0, 0), (0, 255, 0), (0, 128, 255), (255, 255, 0)]
for i, color in enumerate(colors):
    draw.rectangle([0, i * bar_h, width, (i + 1) * bar_h], fill=color)

# Draw a white crosshair at center
cx, cy = width // 2, height // 2
draw.line([(cx, 0), (cx, height)], fill=(255, 255, 255), width=2)
draw.line([(0, cy), (width, cy)], fill=(255, 255, 255), width=2)

img.save("fb_test.png")
print(f"Saved fb_test.png ({width}x{height})")
EOF

Run it:

python3 fb_test_image.py
sudo fbi -T 1 -d /dev/fb0 --noverbose fb_test.png

You should see four colored horizontal bars with a white crosshair at the center. Press q or Esc to exit fbi.

Stuck?
  • Permission denied: Make sure you use sudo for fbi. The framebuffer device requires root or video group membership.
  • fbi shows nothing over SSH: fbi needs a real console (-T 1 selects TTY 1). If the desktop is running, stop it first: sudo systemctl stop lightdm 2>/dev/null
  • "no such file" for fb0: Your display may use DRM/KMS without a legacy framebuffer. Check ls /dev/dri/ — if you see card0, skip to the DRM/KMS tutorial.
Checkpoint

Colored bars are visible on the display. The image fills the screen at the correct resolution.


4. Write Pixels Directly with mmap

Now write pixels directly to framebuffer memory — no image library, no viewer. This is how embedded single-screen UIs work under the hood.

Memory Mapping (mmap)

The mmap system call maps the framebuffer device into your process's address space. Once mapped, you can read and write pixels as if they were entries in an array — no system call per pixel, no copy overhead. The kernel translates your writes directly into display memory updates.

Create the script:

cat > fb_draw.py << 'EOF'
#!/usr/bin/env python3
"""Draw directly to /dev/fb0 using mmap.

Reads resolution, stride, and pixel format from sysfs automatically.
"""
import mmap, struct, sys

# ── Read framebuffer parameters from sysfs ────────────────────
def read_sysfs(name):
    with open(f"/sys/class/graphics/fb0/{name}") as f:
        return f.read().strip()

width, height = [int(x) for x in read_sysfs("virtual_size").split(",")]
bpp = int(read_sysfs("bits_per_pixel"))
stride = int(read_sysfs("stride"))
fb_size = stride * height

print(f"Framebuffer: {width}x{height}, {bpp} bpp, stride={stride}, size={fb_size}")

# ── Pixel packing functions ───────────────────────────────────
def rgb565(r, g, b):
    """Pack RGB into 16-bit RGB565 (2 bytes)."""
    return struct.pack("<H", ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))

def xrgb8888(r, g, b):
    """Pack RGB into 32-bit XRGB8888 (4 bytes)."""
    return struct.pack("<I", (r << 16) | (g << 8) | b)

# Select packer based on detected format
if bpp == 16:
    pack_pixel = rgb565
    pixel_bytes = 2
elif bpp == 32:
    pack_pixel = xrgb8888
    pixel_bytes = 4
else:
    print(f"Unsupported bpp: {bpp}")
    sys.exit(1)

# ── Open and mmap the framebuffer ─────────────────────────────
with open("/dev/fb0", "r+b") as f:
    mm = mmap.mmap(f.fileno(), fb_size)

    # Clear to black
    mm[:fb_size] = b'\x00' * fb_size

    # Draw a red rectangle (100x100) at center
    rx, ry, rw, rh = width // 2 - 50, height // 2 - 50, 100, 100
    red = pack_pixel(255, 0, 0)

    for y in range(ry, ry + rh):
        for x in range(rx, rx + rw):
            offset = y * stride + x * pixel_bytes
            mm[offset:offset + pixel_bytes] = red

    # Draw a green horizontal line across the middle
    green = pack_pixel(0, 255, 0)
    y = height // 2
    for x in range(width):
        offset = y * stride + x * pixel_bytes
        mm[offset:offset + pixel_bytes] = green

    mm.close()

print("Done — check the display!")
EOF

Run it:

sudo python3 fb_draw.py

You should see a red square at the center of a black screen, with a green horizontal line through the middle.

What's Happening

 Your Python program
 mmap(/dev/fb0)  →  maps framebuffer memory into your address space
 mm[offset] = pixel_bytes  →  write goes directly to display memory
 Display controller reads memory continuously at refresh rate (60 Hz)
 Pixels appear on screen — no window manager, no compositor, no GPU

Each pixel's position in memory is: offset = y * stride + x * pixel_bytes

The stride (not width * pixel_bytes) accounts for any padding the hardware adds at the end of each row.

Checkpoint

A red rectangle and green line are visible on the display. fb_draw.py detected the correct resolution and pixel format automatically.


5. Animate: Bouncing Square

Create a simple animation to see the framebuffer update in real time:

cat > fb_bounce.py << 'EOF'
#!/usr/bin/env python3
"""Bouncing square on the framebuffer."""
import mmap, struct, time, signal, sys

# ── Read FB params ────────────────────────────────────────────
def read_sysfs(name):
    with open(f"/sys/class/graphics/fb0/{name}") as f:
        return f.read().strip()

width, height = [int(x) for x in read_sysfs("virtual_size").split(",")]
bpp = int(read_sysfs("bits_per_pixel"))
stride = int(read_sysfs("stride"))
fb_size = stride * height

if bpp == 16:
    def pack(r, g, b):
        return struct.pack("<H", ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3))
    pixel_bytes = 2
else:
    def pack(r, g, b):
        return struct.pack("<I", (r << 16) | (g << 8) | b)
    pixel_bytes = 4

BLACK = pack(0, 0, 0)
WHITE = pack(255, 255, 255)

running = True
def stop(sig, frame): global running; running = False
signal.signal(signal.SIGINT, stop)

# ── Animation loop ────────────────────────────────────────────
SQ = 60  # square size
x, y = width // 4, height // 4
dx, dy = 3, 2

with open("/dev/fb0", "r+b") as f:
    mm = mmap.mmap(f.fileno(), fb_size)
    mm[:fb_size] = b'\x00' * fb_size   # clear

    print(f"Bouncing {SQ}x{SQ} square on {width}x{height} — Ctrl+C to stop")

    while running:
        # Erase old position
        for row in range(y, min(y + SQ, height)):
            off = row * stride + x * pixel_bytes
            mm[off:off + SQ * pixel_bytes] = BLACK * SQ

        # Move
        x += dx
        y += dy
        if x <= 0 or x + SQ >= width:  dx = -dx
        if y <= 0 or y + SQ >= height: dy = -dy

        # Draw new position
        for row in range(y, min(y + SQ, height)):
            off = row * stride + x * pixel_bytes
            mm[off:off + SQ * pixel_bytes] = WHITE * SQ

        time.sleep(0.016)   # ~60 fps

    mm[:fb_size] = b'\x00' * fb_size   # clear on exit
    mm.close()

print("Stopped.")
EOF
sudo python3 fb_bounce.py

A white square bounces around the screen. Press Ctrl+C to stop. This is tearing-prone (no VSync) — that's by design. The DRM/KMS tutorial shows how page flipping solves tearing.


Pixel Format Reference

Format Bytes/Pixel Channel Packing Notes
RGB565 2 RRRRRGGGGGGBBBBB (16 bits) Common on small embedded LCDs
RGB888 3 RRRRRRRRGGGGGGGGBBBBBBBB (24 bits) No alpha, tight packing, rare
XRGB8888 4 xxxxxxxxRRRRRRRRGGGGGGGGBBBBBBBB (32 bits) DRM default; x = padding

The scripts above auto-detect the format from /sys/class/graphics/fb0/bits_per_pixel and select the correct packing function.


6. Your Turn: Investigate and Modify

These tasks require you to read sysfs, calculate values, and modify code — not just copy-paste.

Task A: Framebuffer Math

Answer these questions by reading sysfs values and calculating by hand. Write your answers in a comment at the top of your script.

  1. What is the total size of your framebuffer in bytes? Calculate stride × height, then verify it matches reality.
  2. How many wasted bytes per row does your display have? Calculate: stride - (width × bytes_per_pixel). If non-zero, explain why the hardware pads rows.
  3. If your display is 32 bpp, how much memory would you save switching to 16 bpp? What color precision do you lose?

How to read the values:

cat /sys/class/graphics/fb0/virtual_size     # width,height
cat /sys/class/graphics/fb0/bits_per_pixel   # 16 or 32
cat /sys/class/graphics/fb0/stride           # bytes per row

How to validate your calculation — compare your stride × height result against the actual memory size:

# Method 1: read the framebuffer size from the kernel
ls -l /dev/fb0                               # may show 0 (device files often do)
# Method 2: more reliable — use Python to check the mapped size
python3 -c "
import os
with open('/sys/class/graphics/fb0/stride') as f: stride = int(f.read())
with open('/sys/class/graphics/fb0/virtual_size') as f: w, h = f.read().split(',')
print(f'Calculated: {stride} × {int(h)} = {stride * int(h)} bytes')
print(f'That is {stride * int(h) / 1024 / 1024:.2f} MB')
"
Example Answer (1920×1080, 32 bpp)
  • stride = 7680 (= 1920 × 4, no padding in this case)
  • total = 7680 × 1080 = 8,294,400 bytes ≈ 7.9 MB
  • wasted per row = 7680 - (1920 × 4) = 0 (no alignment padding)
  • Switching to 16 bpp: 1920 × 2 × 1080 = 4,147,200 bytes ≈ 3.9 MB — saves ~50%, but blue goes from 256 levels (8 bit) to 32 levels (5 bit)

Task B: Draw a Gradient

Modify fb_draw.py to fill the entire screen with a horizontal gradient — black on the left edge, bright blue on the right edge. Each column x should have a blue intensity proportional to its position:

blue_value = int(255 * x / (width - 1))

You need to figure out: which packing function argument controls blue? What happens to red and green values?

Step-by-step approach:

  1. Start from fb_draw.py — you already have the sysfs reading and pixel packing functions
  2. Replace the rectangle-drawing loop with a nested loop over all pixels: for y in range(height): for x in range(width):
  3. For each pixel, compute blue_value from x, then pack with pack_pixel(0, 0, blue_value)
  4. Run it — if you see vertical banding (staircase steps instead of smooth gradient), count how many distinct bands you see
Why the Banding?

The gradient looks striped because of quantization — your 8-bit blue value (0–255) gets truncated when packed into the pixel format. At 16 bpp (RGB565), blue has only 5 bits = 32 levels. Across 1920 pixels, each level spans ~60 pixels — a visible band. Even at 32 bpp (8-bit blue = 256 levels), you may see faint bands because 256 steps across 1920 pixels means ~7.5 pixels per step.

The fix is dithering — adding a tiny random offset to each pixel's value so neighboring pixels alternate between two adjacent levels, and the eye perceives a smooth average. Replace the blue calculation with:

import random

def dithered_blue(x, width):
    exact = 255.0 * x / (width - 1)    # e.g., 127.4
    noise = random.random() - 0.5       # range [-0.5, +0.5)
    return max(0, min(255, int(exact + noise)))
Try it with and without dithering on the same screen (e.g., top half dithered, bottom half not) to see the difference. This is the same technique GPUs use internally when converting between color depths.

Hint: Speeding It Up

Approach 1 — row at a time (slow but obvious):

for y in range(height):
    row = b''.join(pack_pixel(0, 0, int(255 * x / (width - 1))) for x in range(width))
    off = y * stride
    mm[off:off + width * pixel_bytes] = row
This is still slow because it rebuilds the same row 1080 times. Time it and see.

Approach 2 — precompute once (fast): A horizontal gradient is the same row repeated for every y. Build it once, write it many times:

row = b''.join(pack_pixel(0, 0, int(255 * x / (width - 1))) for x in range(width))
row += b'\x00' * (stride - width * pixel_bytes)  # pad to stride

for y in range(height):
    off = y * stride
    mm[off:off + stride] = row

Approach 3 — one single write (fastest): Build the entire framebuffer as one bytes object and write it in a single operation:

row = b''.join(pack_pixel(0, 0, int(255 * x / (width - 1))) for x in range(width))
row += b'\x00' * (stride - width * pixel_bytes)
mm[:fb_size] = row * height

Compare all three with time.monotonic() — the difference is dramatic and shows why minimizing Python loop iterations matters in embedded.

Task C: Measure Frame Rate

Modify fb_bounce.py to measure and print the actual frame rate every second. You will need to:

  1. Count frames in a 1-second window
  2. Print the FPS value
  3. Try removing the time.sleep(0.016) line — what happens to FPS? Is the CPU usage acceptable? (Check with htop in another SSH session.)
Hint

Add these variables before the while loop:

frame_count = 0
last_time = time.monotonic()
Then inside the loop, after drawing:
frame_count += 1
now = time.monotonic()
if now - last_time >= 1.0:
    print(f"FPS: {frame_count}")
    frame_count = 0
    last_time = now

What You Will See in htop

With time.sleep(0.016), the process uses ~1–3% CPU — it sleeps most of the time and only wakes up to draw.

Without sleep, the process becomes a busy loop — it consumes 100% of one CPU core. In htop you will see one core pinned at 100%. You may also notice the process jumping between CPU cores — the Linux scheduler migrates threads to balance load across cores, so the green bar flickers between CPU 0, 1, 2, 3. This is normal behavior and shows that your single-threaded program has no CPU affinity set.

This matters in embedded: a busy loop on a single-core system (like Pi Zero) leaves zero CPU for anything else — SSH becomes laggy, other services stall. Even on a quad-core Pi 4, wasting an entire core on drawing a 60 Hz animation that the display can only show 60 times per second is pointless work. The display controller reads framebuffer memory at a fixed 60 Hz regardless of how fast you write — frames between refreshes are never seen.

Rule of thumb for embedded animation loops: sleep to yield CPU, and never render faster than the display refresh rate. On framebuffer (no VSync signal available), time.sleep(1/60) is a reasonable approximation. The DRM/KMS tutorial shows how to use page-flip events to sleep until the exact VBlank moment.

Task D: Understand VTs and the Display

After running fb_draw.py, the terminal text is gone. Without rebooting, investigate what controls the screen:

  1. Find out which virtual terminal (VT) is active: cat /sys/class/tty/tty0/active
  2. Check if fbi is still running: ps aux | grep fbi
  3. Try switching VTs and observe the HDMI display:
sudo chvt 2    # switch to VT 2 — what do you see?
sudo chvt 1    # switch back to VT 1 — what do you see now?

Answer these questions:

  • Did chvt 1 show the text console or the colored bars from fbi? Why?
  • What happens if you kill fbi first (sudo killall fbi) and then chvt 2 + chvt 1?
Hint: What's Actually Happening

Try this sequence and watch the HDMI display after each command:

sudo python3 fb_draw.py    # red rectangle — terminal text overwritten
sudo chvt 2                # VT 2: kernel tells fbcon to redraw → login prompt appears
sudo chvt 1                # VT 1: but what redraws here?

If fbi is still running on VT 1 (from Section 3), it hooks into VT switch events. When VT 1 gets focus, fbi redraws its image — you see the colored bars again, not the text console. This is why it looks like chvt 1 "brought back the squares." The fbi process is a VT-aware application — it registered itself to receive focus events and redraws when activated.

If you kill fbi first (sudo killall fbi), then chvt 1 gives control back to fbcon, which redraws VT 1's text buffer — you see the terminal again.

This reveals an important difference:

Who redraws? On VT switch?
fb_draw.py Nobody — the script exited, pixels are "fire and forget" Content is gone (overwritten by whatever redraws next)
fbi fbi process stays running, listens for VT events Redraws its image when VT gets focus
fbcon Kernel console driver Redraws text when VT gets focus (if no app claimed the VT)

Takeaway: raw mmap writes have no persistence — once your process exits, nothing will restore those pixels. A production framebuffer app must keep running and handle redraw events, just like fbi does.

Task E: Wrong Stride Experiment

Deliberately break fb_draw.py by replacing the line:

stride = int(read_sysfs("stride"))

with a hardcoded wrong value:

stride = width * pixel_bytes   # WRONG — ignoring hardware alignment

Run it and observe the output. If your display has alignment padding (stride > width × pixel_bytes), you will see a diagonal smear instead of a rectangle — each row starts a few pixels too early because you are not accounting for the padding bytes at the end of each row.

What If It Looks Normal?

If your display has no padding (stride == width × pixel_bytes), the output will look correct. In that case, try a different wrong value to see the effect:

stride = width * pixel_bytes + 64   # artificially add 64 bytes of fake padding
This shifts every row to the right, producing a visible slant. The principle is the same — wrong stride = wrong row start = diagonal distortion.

Checkpoint

You have at least completed tasks A, B, and D. Your gradient fills the full screen, and you can explain why the terminal disappeared and how to get it back.

Solution Code

If you are stuck, reference solutions are available in the course repository under src/embedded-linux/solutions/framebuffer-basics/ — contains task_a_fb_math.py, task_b_gradient.py, task_c_fps_counter.py, and task_e_wrong_stride.py. Try to solve each task yourself first — the learning happens in the debugging, not in reading the answer.


What Just Happened?

You wrote pixels directly to video memory — the same mechanism used by embedded kiosks, factory HMIs, and boot splash screens. No window manager, no compositor, no GPU driver needed.

Concept What You Did
sysfs Read resolution, bpp, and stride from /sys/class/graphics/fb0/
mmap Mapped device memory into user space — zero-copy pixel access
stride Used y * stride (not y * width * bpp) for correct row addressing
pixel format Packed RGB values into RGB565 or XRGB8888 depending on hardware
fbcon Understood that console text and your pixels share the same buffer

Why DRM/KMS Next?

The framebuffer API is simple and effective for static images, but it hits real limitations once you need smooth animation or production-quality output:

Problem Framebuffer (fbdev) DRM/KMS
Tearing You write while the display reads — no synchronization. The bouncing square tears visibly. Page flipping: you prepare a new frame in a back buffer, then atomically swap it during VBlank. The display never reads a half-written frame.
Resolution changes Changing mode requires fbset or rebooting. No standard way to enumerate supported modes. You query connectors, modes, and timing from the kernel. The hardware tells you exactly what it supports.
Multiple outputs One /dev/fb0 per display, no coordination. One /dev/dri/card0 manages all outputs (HDMI + DSI) with shared resources.
Hardware planes Not exposed — everything is one flat buffer. Hardware overlay planes can composite layers (e.g., video + UI) without CPU cost.
Future-proof Deprecated by the kernel — no new fbdev drivers are accepted. Existing drivers are maintained but receive no new features. Active development. All new display drivers are DRM-only.

The framebuffer is the right place to understand how pixel memory works. DRM/KMS is where you build products.

The next tutorial uses modetest and DRM dumb buffers to display tear-free graphics — same pixel math you learned here, but with proper synchronization.


Deliverable

  • [ ] fbset -i output shows your resolution and pixel format
  • [ ] fb_test.png displayed via fbi — colored bars visible on screen
  • [ ] fb_draw.py — red rectangle and green line drawn directly to /dev/fb0
  • [ ] fb_bounce.py — bouncing square animation runs at ~60 fps
  • [ ] Task A — framebuffer math answers (total size, wasted bytes, bpp comparison)
  • [ ] Task B — horizontal gradient fills the screen, smooth color transition
  • [ ] Task D — restored the console after framebuffer overwrite, explanation of why chvt works

Back to Course Overview | Next: DRM/KMS Test →