Atomic modesetting groups every KMS state change — connector routing, CRTC modes, plane sources and destinations, framebuffer assignments — into a single request that the kernel applies all at once or rejects entirely. There are no partial states and no intermediate flicker. This is the correct API for any new compositor, display manager, or application that performs modesetting. The key advantages over the legacyDocumentation 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.
set_crtc API are:
TEST_ONLYvalidation — submit a request and ask the kernel whether it would succeed, without actually changing anything on screen. Invaluable for pre-flight checks.NONBLOCKcommits — return immediately after queuing, rather than blocking until the next vblank.- Multi-output coordination — a single commit can reconfigure multiple CRTCs and connectors atomically.
- Full plane access — overlay and cursor planes are first-class citizens alongside the primary plane.
Both
UniversalPlanes and Atomic capabilities must be enabled before you enumerate planes or issue atomic commits. Do this immediately after opening the device:use drm::Device as BasicDevice;
use drm::control::Device as ControlDevice;
card.set_client_capability(drm::ClientCapability::UniversalPlanes, true)
.expect("Unable to request UniversalPlanes capability");
card.set_client_capability(drm::ClientCapability::Atomic, true)
.expect("Unable to request Atomic capability");
Fetch the global resource list and the full plane list (which is only complete after enabling
UniversalPlanes):use drm::control::{connector, crtc};
let res = card
.resource_handles()
.expect("Could not load resource handles");
let coninfo: Vec<connector::Info> = res
.connectors()
.iter()
.flat_map(|&h| card.get_connector(h, true))
.collect();
let crtcinfo: Vec<crtc::Info> = res
.crtcs()
.iter()
.flat_map(|&h| card.get_crtc(h))
.collect();
let plane_handles = card
.plane_handles()
.expect("Could not list plane handles");
let con = coninfo
.iter()
.find(|i| i.state() == connector::State::Connected)
.expect("No connected connector");
let &mode = con.modes().first().expect("No modes on connector");
let (disp_w, disp_h) = mode.size();
let crtc = crtcinfo.first().expect("No CRTCs found");
Filter the plane list to those compatible with the chosen CRTC, then prefer the one whose
type property equals Primary:use drm::control::{self, PlaneType};
let (primary_planes, other_planes): (Vec<_>, Vec<_>) = plane_handles
.iter()
.filter(|&&plane| {
card.get_plane(plane)
.map(|info| {
let compat = res.filter_crtcs(info.possible_crtcs());
compat.contains(&crtc.handle())
})
.unwrap_or(false)
})
.partition(|&&plane| {
if let Ok(props) = card.get_properties(plane) {
for (&id, &val) in props.iter() {
if let Ok(info) = card.get_property(id) {
if info.name().to_str().map(|x| x == "type").unwrap_or(false) {
return val == (PlaneType::Primary as u32).into();
}
}
}
}
false
});
let plane = *primary_planes
.first()
.unwrap_or_else(|| &other_planes[0]);
Every atomic property —
CRTC_ID, MODE_ID, ACTIVE, FB_ID, SRC_X, etc. — is addressed by a property::Handle. Fetch a HashMap<name, property::Info> for each object:let con_props = card
.get_properties(con.handle())
.expect("Could not get connector properties")
.as_hashmap(&card)
.expect("Could not resolve connector property info");
let crtc_props = card
.get_properties(crtc.handle())
.expect("Could not get CRTC properties")
.as_hashmap(&card)
.expect("Could not resolve CRTC property info");
let plane_props = card
.get_properties(plane)
.expect("Could not get plane properties")
.as_hashmap(&card)
.expect("Could not resolve plane property info");
as_hashmap() resolves each raw property handle into its human-readable property::Info struct so you can look up handles by name string. Property names are stable across kernel versions (e.g. "CRTC_ID", "MODE_ID", "ACTIVE", "FB_ID", "SRC_W", "CRTC_W", etc.).AtomicModeReq is the builder type that accumulates (object, property, value) triples before submission. Create the mode as a property blob — the kernel accepts a raw Mode struct serialised into a blob object:use drm::control::{atomic::AtomicModeReq, property};
use drm::buffer::DrmFourcc;
// Allocate a dumb buffer for the primary plane (see /guides/dumb-buffers).
let mut db = card
.create_dumb_buffer((disp_w.into(), disp_h.into()), DrmFourcc::Xrgb8888, 32)
.expect("Could not create dumb buffer");
{
let mut map = card.map_dumb_buffer(&mut db).expect("Could not map buffer");
for b in map.as_mut() { *b = 128; }
}
let fb = card.add_framebuffer(&db, 24, 32).expect("Could not create framebuffer");
// Serialise the mode into a kernel blob object.
let mode_blob = card
.create_property_blob(&mode)
.expect("Failed to create mode blob");
let mut req = AtomicModeReq::new();
// --- Connector: route to the chosen CRTC ---
req.add_property(
con.handle(),
con_props["CRTC_ID"].handle(),
property::Value::CRTC(Some(crtc.handle())),
);
// --- CRTC: set mode and mark active ---
req.add_property(
crtc.handle(),
crtc_props["MODE_ID"].handle(),
mode_blob,
);
req.add_property(
crtc.handle(),
crtc_props["ACTIVE"].handle(),
property::Value::Boolean(true),
);
// --- Primary plane: bind framebuffer and set source/destination rectangles ---
req.add_property(
plane,
plane_props["FB_ID"].handle(),
property::Value::Framebuffer(Some(fb)),
);
req.add_property(
plane,
plane_props["CRTC_ID"].handle(),
property::Value::CRTC(Some(crtc.handle())),
);
// Source rectangle in 16.16 fixed-point (full buffer).
req.add_property(plane, plane_props["SRC_X"].handle(), property::Value::UnsignedRange(0));
req.add_property(plane, plane_props["SRC_Y"].handle(), property::Value::UnsignedRange(0));
req.add_property(plane, plane_props["SRC_W"].handle(),
property::Value::UnsignedRange((disp_w as u64) << 16));
req.add_property(plane, plane_props["SRC_H"].handle(),
property::Value::UnsignedRange((disp_h as u64) << 16));
// Destination rectangle in pixels (full screen).
req.add_property(plane, plane_props["CRTC_X"].handle(), property::Value::SignedRange(0));
req.add_property(plane, plane_props["CRTC_Y"].handle(), property::Value::SignedRange(0));
req.add_property(plane, plane_props["CRTC_W"].handle(),
property::Value::UnsignedRange(disp_w as u64));
req.add_property(plane, plane_props["CRTC_H"].handle(),
property::Value::UnsignedRange(disp_h as u64));
The source rectangle coordinates (
SRC_X, SRC_Y, SRC_W, SRC_H) use 16.16 fixed-point values, so a width of 1920 pixels is expressed as 1920 << 16 = 125829120. Destination rectangle coordinates (CRTC_X, CRTC_Y, CRTC_W, CRTC_H) are plain integers in pixels.use drm::control::AtomicCommitFlags;
// Validate without touching hardware.
card.atomic_commit(AtomicCommitFlags::TEST_ONLY, req.clone())
.expect("Atomic commit validation failed");
// Apply the modeset.
card.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)
.expect("Atomic commit failed");
TEST_ONLY is invaluable for checking whether a configuration is feasible before committing it. Use it whenever you are building a new configuration dynamically — for instance, when a hotplug event adds or removes a connector — to avoid presenting the user with a black screen due to an unsupported mode combination.AtomicCommitFlags reference
| Flag | Meaning |
|---|---|
PAGE_FLIP_EVENT | Emit a page-flip completion event readable via receive_events() |
PAGE_FLIP_ASYNC | Flip as soon as possible, without waiting for vblank (may tear) |
TEST_ONLY | Validate the request without applying any changes |
NONBLOCK | Return immediately rather than blocking until vblank |
ALLOW_MODESET | Permit a full modeset (required when changing modes or connector routing) |
PAGE_FLIP_EVENT and PAGE_FLIP_ASYNC correspond to the same flags used in the legacy page_flip ioctl. ALLOW_MODESET is required for any commit that changes MODE_ID or CRTC_ID — omitting it will cause the kernel to reject the commit with EINVAL if a modeset is needed.
Subsequent frame updates
After the initial modeset, subsequent frame updates only need to change theFB_ID property on the plane — no ALLOW_MODESET required:
PAGE_FLIP_EVENT with NONBLOCK for a non-blocking frame pipeline: queue the next frame immediately, then wait for the event via receive_events() before queuing another.