Digital Rights Management? No, we’re talking about Direct Rendering Manager (DRM), the core subsystem in the Linux kernel responsible for managing graphics memory and display hardware.
If you’re an engineer working on display controllers, embedded graphics, or GPU drivers, chances are you’ll need to interface with DRM at some point. Unfortunately, quality documentation is sparse, and kernel internals can be daunting. This tutorial aims to demystify DRM by walking through the basics of allocating and displaying a framebuffer.
What Does DRM Actually Do?
From the userspace perspective, DRM serves to make it easy to configure and communicate with video and graphics hardware. For, mixers & video processing units (VPUs), DRM enables managing video overlays, composition, and color conversion. For GPUs, DRM is typically responsible for allocating memory buffers and shuttling commands between userspace and hardware.
While DRM can be used to configure some GPU settings, it’s not meant to be a replacement for full-fledged userspace graphics APIs like OpenGL, Vulkan, or Direct3D. Higher level tools form the command streams responsible for performing the actual rendering and shader execution. DRM merely acts as the highway/traffic controller to get these command streams to their actual destination.
Furthermore, not all GPUs are graphical. Traditionally, GPUs were designed around the rasterization pipeline, which takes vertex data, processes it through programmable shaders, and ultimately produces pixels on screen. Modern GPUs can also perform general-purpose computation and are more appropiately called(GPGPU). Such GPUs may opt to use their own proprietary kernel modules(NVIDIA does this).
DRM Pipeline
To display an image to the screen, we’ll need to perform the following steps:
- open drm device in /dev/card
- check for dumb buffer support
- find first available connector
- find first available CRTC
- find first available mode
- malloc a GPU compatible framebuffer
- map dumb buffer into userspace
- Fill the framebuffer with a pattern of interest
The connector is the physical connector on a logic board. The connector can have various states such as plugged-in or un-plugged. Furthermore, the connector can have certain EDID information.
The CRTC is the actual IP block that scans out a framebuffer. Typically, under the hood, a CRTC driver that implements the bindings for libdrm will write to the CRTC’s offset register as well as various registers instructing the CRTC about the scanout dimensions and possibly buffer format. The scanout IP will then access the the pixel values from the framebuffer in memory, convert pixels(necessary for formats like YUV), and drive/transmit them to the display.
For SOC-integrated CRTCs, DMA accesses will often be necessary, whereas for discrete GPUs, the CRTC likely has access to the framebuffer in the GPUs VRAM.
A mode represents a point in the (resolution, bit-depth, format, refresh-rate) configuration space supported by a connector.
While it is true that the kernel provides various memory allocation objects, however, we cannot use these with DRM. This is because DRM allocator takes special care to allocate framebuffers that play nice with the alignment and other requirements of IP that accesses said allocated buffers. Furthermore, when allocating memory, DRM has to be conscious of where the memory is being stored such as in CPU RAM or VRAM.
If we want our user-space process to be able to draw to the framebuffer, said framebuffer must be mapped as writeable into our user-space process’s page-table.
Running The Code
At the end of the day, in the modern(non fb-dev) Linux graphics stack, DRM is what is responsible for arbiting what gets shown on the screen. In a typical graphics capable Linux system, a windows manager effectively takes possession of a display surface usually exposed by DRM through /dev/dri/card*
. We can force a windows manager to relinquish control by selecting a virtual console by pressing CTRL+ALT+FUNCTION_KEY
.
In recent versions of Ubuntu, I’ve found CTRL+ALT+F3
to work well enough. You’ll be prompted by logind
for your credential.
Once you’ve logged-in, compile and run the following code with: gcc drm_checkerboard.c -o out
pkg-config –cflags –libs libdrm&& ./out
.
The following code will should draw a checker board pattern.
Code
// $gcc drm_checkerboard.c -o out `pkg-config --cflags --libs libdrm` && ./out #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <drm.h> #include <drm_mode.h> #include <sys/mman.h> #include <xf86drm.h> #include <xf86drmMode.h> #include <errno.h> #include <string.h> #define XR24 0x34325258 drmModeRes *openDRMCard(const char *device_path, int *fd_out) { if (!device_path || !fd_out) { errno = EINVAL; return NULL; } int fd = open(device_path, O_RDWR | O_CLOEXEC); if (fd < 0) { return NULL; } drmModeRes *resources = drmModeGetResources(fd); if (!resources) { close(fd); errno = ENODEV; return NULL; } *fd_out = fd; return resources; } int main(int argc, char **argv) { int fd; drmModeRes *drm_device = openDRMCard("/dev/dri/card0", &fd); if (!drm_device) { perror("openDRMCard failed"); return EXIT_FAILURE; } // check for dumb buffer support printf("Checking for dumb buffer support.\n"); uint64_t cap = 0; int ret = drmGetCap(fd, DRM_CAP_DUMB_BUFFER, &cap); if (ret || cap == 0) { fprintf(stderr, "driver doesn't support the dumb buffer API\n"); return 1; } // find first available connector printf("Checking for first available plugged-in connector.\n"); drmModeConnector *first_available_connector; bool found_connector = false; for (int i = 0; i < drm_device->count_connectors; i++) { first_available_connector = drmModeGetConnector(fd, drm_device->connectors[i]); printf("%d\n", i); if (first_available_connector->connection == DRM_MODE_CONNECTED) { found_connector = true; break; } else { drmModeFreeConnector(first_available_connector); } } if(!found_connector) { fprintf(stderr, "Couldn't find plugged-in connector!\n"); return -1; } // find first available CRTC printf("Finding first available crtc\n"); if(!drm_device->count_crtcs) { fprintf(stderr, "No available CRTCs on first connector!"); return -1; } // find first available mode printf("Checking for preferred mode.\n"); drmModeModeInfo *mode; if(!first_available_connector->count_modes) { fprintf(stderr, "No available modes on first connector!"); return -1; } bool found_preferred_mode = false; for (int i = 0; i < first_available_connector->count_modes; i++) { mode = &first_available_connector->modes[i]; if (mode->type & DRM_MODE_TYPE_PREFERRED) { found_preferred_mode = true; break; } } if (!found_preferred_mode) { printf("Did not find preferred mode!"); } // effectively performs malloc for a GPU compatible framebuffer printf("Creating dumb buffer for resolution : %dx%d\n", mode->hdisplay, mode->vdisplay); uint32_t dumb_buffer_handle; uint32_t dumb_buffer_pitch; uint64_t dumb_buffer_size; uint32_t bits_per_pixel = 32; ret = drmModeCreateDumbBuffer( fd, (uint32_t) mode->hdisplay, (uint32_t) mode->vdisplay, bits_per_pixel, 0, &dumb_buffer_handle, &dumb_buffer_pitch, &dumb_buffer_size); if (ret) { fprintf(stderr, "failed to create dumb buffer: %s\n", strerror(errno)); return -1; } // map dumb buffer into userspace. We store the offset into the file descriptor // of where the dumbuffer will be located in the variable `uint64_t offset` void *map; uint64_t offset; ret = drmModeMapDumbBuffer(fd, dumb_buffer_handle, &offset); if (ret) { fprintf(stderr, "drmModeMapDumbBuffer failed: %s\n", strerror(errno)); return ret; } map = mmap(0, dumb_buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); if (map == MAP_FAILED) { fprintf(stderr, "mmap failed with MAP_FAILED\n"); return -EINVAL; } // Fill the framebuffer with a checkerboard pattern printf("Filling framebuffer with checkerboard pattern.\n"); uint32_t *pixel_buffer = (uint32_t *)map; int width = mode->hdisplay; int height = mode->vdisplay; int tile_size = 128; int bytes_per_pixel = bits_per_pixel >> 3; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Determine whether the current tile should be black or white int tile_x = x / tile_size; int tile_y = y / tile_size; bool tile_x_even = tile_x == 0; uint32_t color = ((tile_x + tile_y) % 2 == 0) ? 0xFFFFFFFF : 0xFF000000; // White or Black // We can't simply do `y * mode->hdisplay` because the actual buffer may be padded. // Since `dumb_buffer_pitch` gives us the number of bytes per per buffer row, we divide by // `bytes_per_pixel` to get the number of pixels in a row. pixel_buffer[y * (dumb_buffer_pitch / bytes_per_pixel) + x] = color; } } // register GPU compatible buffer as something that can be used with // a CRTC printf("Creating frambuffer.\n"); unsigned int fb_id; uint32_t handles[4] = {0}; uint32_t pitches[4] = {0}; uint32_t offsets[4] = {0}; handles[0] = dumb_buffer_handle; pitches[0] = dumb_buffer_pitch; ret = drmModeAddFB2( fd, (uint32_t) mode->hdisplay, (uint32_t) mode->vdisplay, XR24, handles, pitches, offsets, &fb_id, 0); if (ret) { fprintf(stderr, "failed to add framebuffer: %s\n", strerror(errno)); return 0; } // set CRTC to selected mode printf("Setting mode\n"); uint32_t connectors[1] = {drm_device->connectors[0]}; ret = drmModeSetCrtc( fd, drm_device->crtcs[0], fb_id, 0, // x offset (we can peek into a frambuffer that could be larger than mode resolution) 0, // y offset connectors, 1, mode); if (ret) { fprintf(stderr, "failed to set mode: %s\n", strerror(errno)); return 0; } sleep(5); drmModeRmFB(fd, fb_id); drmModeDestroyDumbBuffer(fd, dumb_buffer_handle); drmModeFreeResources(drm_device); close(fd); return 0; }