Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Smithay/drm-rs/llms.txt

Use this file to discover all available pages before exploring further.

Legacy modesetting uses the set_crtc() ioctl to bind a connector, a CRTC, and a framebuffer together in a single call. It is the original KMS API and is the simplest way to put pixels on screen: pick a mode, allocate a buffer, call set_crtc, done. The trade-off is that each call is applied immediately without validation, can cause brief flicker, and only configures one CRTC at a time.
Legacy modesetting is effectively deprecated for new display-server code. The kernel still supports it, but the atomic API (see Atomic Modesetting) is preferred for any new work because it provides validation, atomicity across multiple outputs, and better plane management.
1
Open the device and implement traits
2
Follow the steps in Opening a Device to create your Card wrapper, implement AsFd, drm::Device, and drm::control::Device:
3
use std::fs::{File, OpenOptions};
use std::os::unix::io::{AsFd, BorrowedFd};
use drm::control::Device as ControlDevice;

pub struct Card(File);

impl AsFd for Card {
    fn as_fd(&self) -> BorrowedFd<'_> { self.0.as_fd() }
}

impl drm::Device for Card {}
impl ControlDevice for Card {}

impl Card {
    pub fn open(path: &str) -> Self {
        let mut opts = OpenOptions::new();
        opts.read(true).write(true);
        Card(opts.open(path).unwrap())
    }
}
4
Load resource handles
5
resource_handles() returns handles for every framebuffer, CRTC, connector, and encoder the device controls:
6
let res = card
    .resource_handles()
    .expect("Could not load resource handles");
7
Enumerate connector information
8
Fetch detailed info for each connector handle. Pass force_probe: true on startup so the kernel performs a fresh hardware probe and updates EDID, modes, and connection state:
9
use drm::control::connector;

let coninfo: Vec<connector::Info> = res
    .connectors()
    .iter()
    .flat_map(|&handle| card.get_connector(handle, true))
    .collect();
10
get_connector() blocks while the kernel probes the hardware when force_probe is true. Only request forced probes at startup or after a hotplug event — repeated forced probes can cause brief display disruption.
11
Find a connected connector
12
Filter the connector list for one that has a physical cable attached:
13
let con = coninfo
    .iter()
    .find(|info| info.state() == connector::State::Connected)
    .expect("No connected connector found");
14
Pick a display mode
15
The mode list returned by the connector is already sorted with the preferred (native) resolution first:
16
let &mode = con
    .modes()
    .first()
    .expect("No modes available on connector");

let (width, height) = mode.size();
println!("Using mode: {}x{}", width, height);
17
Get a CRTC
18
Retrieve the first available CRTC from the resource list. In a more complete implementation you would match the CRTC to the connector via the encoder’s possible_crtcs bitmask:
19
use drm::control::crtc;

let crtcinfo: Vec<crtc::Info> = res
    .crtcs()
    .iter()
    .flat_map(|&handle| card.get_crtc(handle))
    .collect();

let crtc = crtcinfo.first().expect("No CRTCs found");
20
Create a dumb buffer and framebuffer
21
Allocate a CPU-writable dumb buffer at the connector’s native resolution, fill it with a solid colour, then wrap it in a framebuffer object. See Dumb Buffers for a full explanation of this step:
22
use drm::buffer::DrmFourcc;

// Allocate the dumb buffer.
let mut db = card
    .create_dumb_buffer(
        (width.into(), height.into()),
        DrmFourcc::Xrgb8888,
        32, // bits per pixel
    )
    .expect("Could not create dumb buffer");

// Map it and fill with mid-grey.
{
    let mut map = card
        .map_dumb_buffer(&mut db)
        .expect("Could not map dumb buffer");
    for b in map.as_mut() {
        *b = 128;
    }
} // mapping is dropped here, unmapping the buffer

// Wrap in a framebuffer object (depth=24, bpp=32 for XRGB8888).
let fb = card
    .add_framebuffer(&db, 24, 32)
    .expect("Could not create framebuffer");
23
Call set_crtc to display the image
24
set_crtc takes the CRTC handle, an optional framebuffer, a scanout offset within the framebuffer, a slice of connector handles to drive, and the mode to apply:
25
card.set_crtc(
    crtc.handle(),
    Some(fb),
    (0, 0),            // (x, y) offset into the framebuffer
    &[con.handle()],   // connectors driven by this CRTC
    Some(mode),        // mode to set; None clears the display
)
.expect("Could not set CRTC");
26
The call blocks until the kernel applies the new mode. When it returns, the display should show the framebuffer contents.
27
Clean up
28
Always destroy framebuffer and dumb buffer objects before the device file descriptor is closed:
29
card.destroy_framebuffer(fb).unwrap();
card.destroy_dumb_buffer(db).unwrap();

Page flipping for double-buffering

Calling set_crtc repeatedly is fine for static images but causes tearing for animation because it updates the scanout pointer mid-frame. Page flipping schedules the buffer swap to occur exactly at the next vertical blanking interval. Allocate a second framebuffer, render into it, then queue the flip:
use drm::control::PageFlipFlags;

card.page_flip(
    crtc.handle(),
    new_fb,              // the next framebuffer to display
    PageFlipFlags::EVENT, // request a completion event
    None,                // no specific target sequence
)
.expect("Could not queue page flip");
After queuing, block on receive_events() to learn when the flip completed and it is safe to render into the old buffer again:
use drm::control::Event;

for event in card.receive_events().expect("Could not receive events") {
    match event {
        Event::PageFlip(e) => {
            println!(
                "Flip complete on CRTC {:?}, frame {}",
                e.crtc, e.frame
            );
            // Now safe to reuse the old framebuffer for the next frame.
        }
        Event::Vblank(e) => {
            println!("Vblank on CRTC {:?}, frame {}", e.crtc, e.frame);
        }
        Event::Unknown(_) => {}
    }
}
receive_events() reads from the DRM device file descriptor. To integrate it with an event loop (epoll, mio, Tokio, etc.), monitor the device fd for readability and call receive_events() when data is available.
For modern display servers and compositors, move to Atomic Modesetting. The atomic API supports TEST_ONLY validation, non-blocking commits, and coordinated multi-CRTC updates — all impossible with the legacy set_crtc path.

Build docs developers (and LLMs) love