EasyDRM is a GLFW-inspired abstraction over DRM/KMS, GBM, and EGL/OpenGL that lets you build fullscreen Linux applications without a compositor (no X11, no Wayland). It owns the low-level plumbing—monitor discovery, events, page flips, fences, and atomic commits—so you can focus on your render loop while staying in total control of timing.
- Single-threaded & explicit: you drive the loop, EasyDRM provides blocking events and render surfaces.
- Multi-monitor aware: every monitor gets an isolated GL/EGL/GBM context, framebuffer, and fence.
- Deterministic swap orchestration:
swap_buffers()walks the monitors you rendered and issues atomic commits in a predictable order. - Refresh-rate aware: monitors are grouped by refresh rate for introspection and future scheduling tweaks.
- 3-state mode management:
default_mode,requested_mode, andcurrent_modeminimize expensive modesets and handle TTY focus loss. - Robust fences: GPU→DRM synchronization prevents tearing and leaks by cleaning sync objects every frame.
use easydrm::EasyDRM;
let mut easydrm = EasyDRM::init_empty().expect("GPU available");
loop {
for monitor in easydrm.monitors_mut() {
if monitor.can_render() { // 1) Draw only to ready monitors
monitor.make_current().unwrap();
render_frame(monitor);
}
}
easydrm.swap_buffers().unwrap(); // 2) Commit each monitor that was drawn
easydrm.poll_events().unwrap(); // 3) Block on DRM/input events
if easydrm.should_update() { // 4) Global logic tied to fastest refresh group
update_simulation();
}
}EasyDRM feels like a game engine main loop: you control the cadence, EasyDRM keeps hardware state in sync.
poll_events()blocks on DRM, vblank, hotplug, and optional input fds viapoll().can_render()turns true when the previous page flip + fence finished.make_current()activates the monitor’s GL/EGL context so you can issue draw calls.swap_buffers()iterates the monitors that were drawn and issues their atomic commits with the right fences.
- Monitors are grouped by refresh rate; the map is exposed so you can choose a cadence or diagnostics strategy.
- The helper
should_update()fires once every time the fastest refresh-rate group has committed, letting you run simulation at that cadence. - Every monitor tracks its own fence + framebuffer pair to keep scan-out safe.
| Field | Meaning | When it changes |
|---|---|---|
default_mode |
Optimal mode detected at init | Never after initialization |
requested_mode |
What the app wants (None = default) |
monitor.set_mode(...) |
current_mode |
What DRM is actually using | After successful atomic commit or clear_mode_state() |
A modeset runs when requested_mode != current_mode, covering first boot, TTY focus loss, and user-driven mode switches.
- Linux environment with a DRM/KMS-capable GPU (running on a VT/TTY, not under X11/Wayland).
- Permissions to open
/dev/dri/card*(run as root or add the user to thevideogroup). - Rust 1.84+ (edition 2024) and a modern Mesa/GBM/EGL stack.
cargo build --release
⚠️ Run from a VT (outside X/Wayland) to avoid fighting the system compositor.
cargo run --example basicThe example prints detected monitors, animates a color wipe, and keeps running until you Ctrl+C.
EasyDRM::init_empty()– initialize without a custom per-monitor context.EasyDRM::init(|req| { /* create custom context using req.gl / req.get_proc_address */ })– attach your own data per monitor.EasyDRM::monitors()/monitors_mut()– iterate over monitor handles.Monitor::make_current()– bind this monitor’s GL context and mark it as drawn.Monitor::gl()– access generated GLES2 bindings.Monitor::set_mode(Some(mode))– request a specific DRM mode;Nonereverts todefault_mode.EasyDRM::swap_buffers()– walks monitors that were drawn and calls their atomic swap path.EasyDRM::poll_events()– wait for page flips, hotplug, and optional input events.EasyDRM::should_update()– returns true once per cycle when the fastest refresh-rate group has committed.
See examples/basic.rs and examples/custom_context.rs for end-to-end loops.
- No automatic rendering—users decide exactly when and how to render.
- No UI toolkit logic—just buffers, events, and explicit synchronization.
- Works with any renderer (OpenGL, Skia, Clay, custom software) as long as it can target the provided GL context.
- Deterministic timing: a single
swap_buffers()call per loop orchestrates every commit that needs to happen.
- ✅ Global render loop & commit model
- ✅ Fence strategy plus refresh-rate grouping metadata
- ✅ 3-state display mode system
- ✅ Complete
Monitor::swap_buffers()implementation - 🚧 Cursor plane API