EasyDRM — Minimal DRM/KMS Framework
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.
Highlights
- Single-threaded & explicit: you drive the loop, EasyDRM provides blocking & async DRM event polling and render surfaces.
- Multi-monitor aware: every monitor gets an isolated EGL buffer, framebuffer, and fence. (they still all share the same EGL context to avoid unwanted blocking when switching between them)
- 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.
- Robust fences: GPU→DRM synchronization prevents tearing, making the
can_renderflag for each monitor only trip when the EGL fence for that monitor opens
What easydrm does NOT do
- ❌ Handle Input Events (evdev)
- ❌ Handle VT Switching
These are outside of the scope of easydrm because it's meant to only handle graphics which is the most annoying part,
but you can implement those things on top of easydrm using libinput and by not doing swap_buffers when the current tty doesn't match the initial tty.
Also monitor hotplugging is still broken on easydrm, but that will be fixed soon hopefully.
Core Concept
A GLFW application would typically look like this:
int
You have:
- Library initialization and opening a window
- Start a render loop
- Draw something with opengl
- Swap buffers
- Poll OS events
easydrm tries to catch the same spirit as glfw, but not ignoring the nuances like having multiple monitors or explicit synchronization.
This is how a simple application looks like:
use EasyDRM;
let mut easydrm = init_empty.expect;
loop
An important difference here is that easydrm always behaves like glfw with glfwSwapInterval(1), it always has vsync on.
Also instead of blocking on swap buffers for vsync like glfw would do, it blocks on poll_events until it receives some event and only renders the monitors that received a page flip event (page flip meaning the frame is already on screen and the monitor is ready to be updated again).
Architecture Overview
Event & Rendering Flow
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.
Multi-Monitor Grouping
- 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.
Mode System (Default / Requested / Current)
| 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.
Getting Started
Prerequisites
- 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)
- a modern Mesa/GBM/EGL stack.
Build
Run the basic example
⚠️ Run from a VT (outside X/Wayland) to avoid fighting the system compositor.
The example prints detected monitors, animates a color wipe, and keeps running until you Ctrl+C.
API Sketch
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.