Skip to content

DRM/KMS Test Pattern (No Window Manager)

Time: 30 min | Prerequisites: Framebuffer Basics

This tutorial shows how to use the modern DRM/KMS graphics API to display test patterns, inspect display hardware, and understand why DRM replaces the legacy framebuffer.


Learning Objectives

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

  • Explain the difference between legacy framebuffer and DRM/KMS
  • Use modetest to list connectors, CRTCs, planes, and display modes
  • Display SMPTE and other test patterns on screen using DRM/KMS
  • Find the correct connector ID, CRTC ID, and plane ID for your display
  • Understand why DRM/KMS exists and what problems it solves
Tearing, VSync, and Why DRM/KMS Exists

Tearing occurs when your application writes new pixel data to the framebuffer while the display controller is still reading the old frame. The monitor ends up showing the top half of one frame and the bottom half of the next -- a visible horizontal tear line. In the framebuffer tutorial, the bouncing square showed this artifact because /dev/fb0 has no synchronisation mechanism.

VSync (vertical synchronisation) solves tearing by coordinating buffer updates with the display's vertical blanking interval -- the brief pause between finishing one frame and starting the next. During this interval, no pixels are being read, so it is safe to swap buffers. Page flipping implements this: you prepare a complete frame in a back buffer, then atomically swap it with the front buffer during VBlank. The display controller always reads a complete, consistent frame.

DRM (Direct Rendering Manager) and KMS (Kernel Mode Setting) provide this capability at the kernel level. DRM manages GPU and display hardware access; KMS handles display timing (resolution, refresh rate, pixel clock) inside the kernel. Together they give you hardware-reported display modes (from EDID), page flipping synchronised to VBlank, multiple hardware planes for compositing, and a stable API that every modern Linux display driver implements. The pixel math (y * stride + x * pixel_bytes) is identical to framebuffer -- only the plumbing changes.

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/drm-kms-test/.


1. Install DRM Tools

sudo apt-get update
sudo apt-get install -y libdrm-tests

This installs modetest — a command-line tool that talks directly to the DRM/KMS API without any GUI stack.


2. Discover Your Display Hardware

Before you can display anything, you need to find three things: which connector your display is plugged into, which CRTC drives it, and which plane to draw on. The modetest tool queries all of this from the kernel.

2a. Find the DRM Driver

On Raspberry Pi, the DRM driver is vc4 (Broadcom VideoCore). Every SoC has its own driver (e.g., i915 for Intel, amdgpu for AMD). Verify:

ls /dev/dri/

You should see card0 (and possibly card1). If you only see renderD128 but no card*, your kernel may not have KMS enabled.

2b. List Connectors

sudo modetest -M vc4 -c

The output is long — it lists every physical display output with all its properties. Scroll up or pipe to less and look for a connector with status connected:

# Quick way: find only the connected connector(s)
sudo modetest -M vc4 -c 2>/dev/null | grep -A 1 "^[0-9]"

Example output on a Pi 4 with one HDMI cable plugged into the second micro-HDMI port:

id      encoder status          name            size (mm)       modes   encoders
33      0       disconnected    HDMI-A-1        0x0             0       32
--
42      41      connected       HDMI-A-2        520x290         40      41

In this example: - Connector 33 (HDMI-A-1) is disconnected — nothing plugged in there - Connector 42 (HDMI-A-2) is connected with 40 available modes — this is where the display is

Note the id of the connected connector — you need it below.

Warning

Pi 4 has two micro-HDMI ports (HDMI-A-1 and HDMI-A-2, labeled 0 and 1 on the board). Your cable is probably in one of them. If HDMI-A-1 shows disconnected, check HDMI-A-2 — that is where the display is. A connector with 0 modes and disconnected status has nothing attached.

Note

To see the available modes (resolutions) for your connected connector, run sudo modetest -M vc4 -c without grep and scroll to the connected connector. Each mode line shows resolution, refresh rate, and pixel clock. You can also use a mode by its index: -s 42:#0 uses the first listed mode (usually the preferred/native resolution).

Checkpoint

You have found the connector ID of your connected display. That is all you need to display test patterns. (CRTCs and planes are explored in the tasks at the end.)


3. Display a Test Pattern

Now use modetest to set a mode and show a built-in test pattern. The syntax is:

modetest -M vc4 -s <connector_id>:<mode> -F <pattern>

The -F flag selects the fill pattern. Available patterns: smpte, tiles, plain.

Using the connector ID you found above (replace 46 with your actual connector ID):

# Example: connector 42 (HDMI-A-2), 1920x1080 at 60 Hz, SMPTE color bars
sudo modetest -M vc4 -s 42:1920x1080-60 -F smpte

You should see the classic SMPTE (Society of Motion Picture and Television Engineers) color bar test pattern on your display. Press Enter to exit.

Warning

If the desktop environment is running, it holds the DRM device and modetest will fail with "permission denied" or "resource busy." Stop the desktop first:

sudo systemctl stop lightdm 2>/dev/null    # Raspberry Pi OS with desktop
sudo systemctl stop gdm3 2>/dev/null       # Ubuntu desktop

Try different patterns:

# Tile pattern — checkerboard with metadata text
sudo modetest -M vc4 -s 42:1920x1080-60 -F tiles

# Plain white — useful to check for dead pixels or backlight uniformity
sudo modetest -M vc4 -s 42:1920x1080-60 -F plain

Try a different resolution (if your display supports it):

sudo modetest -M vc4 -s 42:1280x720-60 -F smpte
Checkpoint

SMPTE color bars are visible on the display. modetest exits cleanly when you press Enter.

Stuck?
  • "failed to set mode" — the resolution/refresh combination is not supported by your display. Check available modes with modetest -M vc4 -c.
  • "permission denied" — use sudo. If still failing, another process holds the DRM device (desktop environment, another modetest instance).
  • "cannot find driver vc4" — try without -M: sudo modetest -c to list all available drivers and connectors.
  • Screen flickers then goes black — the mode was set but your monitor does not support it. Wait 10 seconds, modetest will timeout and restore the previous mode.

4. The DRM Display Pipeline

When you ran modetest -s 33:1920x1080-60 -F smpte, it did several things automatically behind the scenes. Understanding this pipeline is the key concept of DRM/KMS:

Connector 33         CRTC 57            Plane 46
(HDMI-A-1)    ←──    (timing engine)    (pixel data)
   │                    │                   │
   │  "which port?"     │  "what timing?"   │  "which pixels?"
   │                    │                   │
   ▼                    ▼                   ▼
 HDMI signal     1920×1080 @ 60 Hz    SMPTE test pattern
 to monitor      pixel clock, sync     in a DRM framebuffer

Why three objects? Because in hardware, these are separate things:

  • A Connector is a physical port — it exists whether or not anything is plugged in
  • A CRTC (CRT Controller — the name is historical, from CRT monitor days) is a timing generator inside the SoC. It does two things: (1) generates the pixel clock and sync signals (HSync, VSync) that tell the monitor when each line and frame starts, and (2) reads pixel data from planes and feeds it to the connector as a serial pixel stream. Each CRTC operates independently with its own timing — this is how the Pi can drive two HDMI monitors at different resolutions and refresh rates simultaneously. Without a free CRTC, a connector cannot display anything, even if it is physically connected.

    CRTC internals (simplified):
    
    Plane memory ──► Pixel fetch ──► Compositor ──► Sync generator ──► Connector
                     (DMA read)     (blend planes)  (HSync, VSync,     (HDMI/DSI
                                                     pixel clock)       encoder)
    
  • A Plane holds the actual pixel data — a buffer in memory. Multiple planes can be stacked (overlays) and the CRTC composites them in hardware. For example, a video player can put the video stream on one plane and the subtitle text on an overlay plane — the CRTC blends them without any CPU work

modetest -s connects all three for you. A real DRM application (compositor, media player, kiosk UI) must set up this pipeline explicitly using ioctl() calls — that is the DRM/KMS API.


5. VSync Page Flipping

The main reason DRM/KMS replaces framebuffer is synchronized display updates. In the framebuffer tutorial, the bouncing square tore because you wrote pixels while the display controller was reading them. DRM solves this with page flipping — you prepare a complete frame in a back buffer, then ask the kernel to swap it at the next VBlank.

Test VSync'd page flipping:

sudo modetest -M vc4 -s 42:1920x1080-60 -F smpte -v

The -v flag enables VSync'd page flippingmodetest allocates two buffers and flips between them synchronized to the display's vertical blanking interval. You will see the pattern appear without tearing. The terminal prints flip timing information.

freq: 60.01Hz

Compare this to the framebuffer bouncing square — that had visible tearing because /dev/fb0 has no flip mechanism. DRM page flipping guarantees the display controller always reads a complete, consistent frame.


6. Programmatic Drawing: Framebuffer vs DRM

In the Framebuffer Basics tutorial, you already wrote pixels programmatically — fb_draw.py opens /dev/fb0, mmaps it, and writes pixels. That works on any Pi regardless of KMS configuration. Here is how the same task differs when done through DRM:

Step Framebuffer (fb_draw.py) DRM/KMS
Open device open("/dev/fb0", "r+b") open("/dev/dri/card0") + ioctl to find connector
Get resolution Read from sysfs (virtual_size) Query connector modes (EDID)
Get buffer mmap the device file directly Create a "dumb buffer" via ioctl, then mmap it
Draw pixels mm[offset] = packed_pixel Same — identical pixel math
Display it Instant — writes go to active scanout Call set_mode / page flip ioctl
VSync Not available Page flip waits for VBlank
Requires /dev/fb0 (always present) /dev/dri/card0 (needs KMS enabled)

The pixel packing and offset math (y * stride + x * pixel_bytes) is identical in both cases. The difference is plumbing — how you get the buffer and how you tell the hardware to display it.

What is a "Dumb Buffer"?

A dumb buffer is a plain block of GPU-accessible memory with no hardware acceleration — you write pixels to it with the CPU, just like a framebuffer. It is called "dumb" because the GPU does not render into it (as opposed to a "smart" buffer used by OpenGL/Vulkan). For embedded UIs without 3D graphics, dumb buffers are all you need.

Enabling DRM/KMS on the Raspberry Pi

If ls /dev/dri/ shows "No such file or directory", DRM/KMS is not enabled. The display is using the legacy firmware-driven framebuffer. To enable KMS:

# Check current config
grep dtoverlay /boot/firmware/config.txt

# Enable KMS (add or uncomment this line)
sudo nano /boot/firmware/config.txt
# dtoverlay=vc4-kms-v3d

sudo reboot

# After reboot, verify
ls /dev/dri/     # should show card0, card1, renderD128

For this course, KMS is not required — the framebuffer path works for all tutorials. KMS becomes important when you need VSync, multiple outputs, or hardware planes. The modetest tool can talk to the vc4 driver even without full KMS device nodes.


7. SPI and DSI Displays

So far, everything used the HDMI connector. But many embedded products use smaller displays connected via SPI or DSI instead of HDMI:

HDMI DSI (ribbon cable) SPI (GPIO wires)
Typical size 7"–27" 3.5"–10" 1.3"–3.5"
Resolution 1920×1080+ 800×480 typical 320×240 typical
Bandwidth 5.9 Gbps (HDMI 1.4) 1.5 Gbps (2 lanes) 50 Mbps (SPI @ 50 MHz)
Rendering path GPU scan-out (DRM/KMS) GPU scan-out (DRM/KMS) CPU pushes pixels over SPI
Max practical FPS 60+ 60 10–20 (bandwidth-limited)
DRM device /dev/dri/card0 /dev/dri/card0 /dev/fb1 (legacy) or /dev/dri/card1

SPI displays are the interesting case for embedded: they are cheap (under $10), tiny, and need only a few wires — but the CPU must copy every pixel through the SPI bus. At 320×240×16 bpp = 153,600 bytes per frame, and SPI running at 50 MHz = 6.25 MB/s, the theoretical maximum is ~40 fps. In practice, overhead reduces this to 10–20 fps.

The kernel driver (fbtft or panel-mipi-dbi) handles the SPI transfer — you write to the framebuffer or DRM buffer, and the driver pushes changed pixels over SPI in the background. From your application's perspective, drawing to an SPI display is identical to drawing to HDMI — same mmap, same pixel math, different device path.

Hands-On

If you have a 3.5" SPI TFT HAT, try the SPI Display tutorial — it uses the same framebuffer techniques from the previous tutorial on a small SPI LCD, and shows the bandwidth limitations in practice.


8. Your Turn

Task A: Find All Modes

List every mode your display supports and answer:

  1. What is the highest resolution your display reports?
  2. What is the lowest resolution available?
  3. How many different refresh rates are listed for 1920x1080 (if any)?
sudo modetest -M vc4 -c

Task B: Framebuffer vs DRM Side-by-Side

Run both approaches and compare:

  1. Draw something with fb_draw.py (from the previous tutorial)
  2. Then run modetest with SMPTE bars

Answer: What happens to your framebuffer drawing when modetest takes over? Why? (Hint: DRM/KMS takes exclusive control of the display pipeline — it does not share with /dev/fb0.)

Task C: Explore the Pipeline

Run the full dump and investigate your display hardware:

sudo modetest -M vc4

The output is hundreds of lines. Scroll through it (or pipe to less) and answer:

  1. How many CRTCs does your Pi have? (Look for the CRTCs: section and count the rows with numeric IDs.)
  2. How many Planes does it have? For each plane, what is its type — Primary, Overlay, or Cursor? (Search for type: under each plane's properties.)
  3. If a CRTC has 1 primary plane and 2 overlay planes, how many independent image layers can it composite in hardware?
Hint

The type property is buried in each plane's property list. Look for lines like:

type:
    flags: immutable enum
    enums: Overlay=0 Primary=1 Cursor=2
    value: 1
value: 1 = Primary, value: 0 = Overlay, value: 2 = Cursor.

Solution Code

A helper script for Task C is available at src/embedded-linux/solutions/drm-kms-test/task_c_pipeline.sh — it parses modetest output and summarizes the pipeline. Try to explore the output yourself first.


What Just Happened?

You used the DRM/KMS API to directly control the display hardware — the same interface that modern compositors (Weston, KWin, Sway) use internally.

Concept What You Did
Connector Found the physical HDMI port and its supported modes from EDID
Pipeline Understood the Connector → CRTC → Plane chain that every DRM app must set up
Mode setting Set resolution and refresh rate via -s
Test patterns Used -F smpte/tiles/plain to generate test images
Page flipping Used -v to see tear-free synchronized updates
FB vs DRM Same pixel math — DRM adds mode discovery, VSync, and hardware planes
Display types Compared HDMI, DSI, and SPI — same API, different bandwidth and rendering path
Approach API VSync Mode Discovery Status
Framebuffer /dev/fb0 No Manual (fbset/sysfs) Deprecated
DRM/KMS /dev/dri/card0 Yes (page flip) Hardware-reported (EDID) Current standard
Wayland Compositor protocol Yes (compositor) Automatic Full desktop

Deliverable

  • [ ] modetest -M vc4 -c output — identified your connector ID, status, and available modes
  • [ ] SMPTE test pattern displayed on screen via modetest -s ... -F smpte
  • [ ] Tested at least two different resolutions or patterns
  • [ ] Understand the framebuffer vs DRM drawing comparison (same pixel math, different plumbing)
  • [ ] Task A — listed highest/lowest resolution and refresh rates
  • [ ] Task C — counted planes and identified their types

Back to Course Overview | Previous: Framebuffer Basics | Next: SDL2 Display →