Felipe Tavares' Avatar

Learning Userland DRM

Part I

January 23, '21

DRM is the bridge between the Linux kernel and actual software running atop Linux, for most operations which involve a graphics card. For example, X11 and Wayland actually send drawing commands to the graphics card using DRM.

In this post I’ll be talking about how to write software which communicates with the graphics card directly, completely bypassing X. Do mind that I am also learning while I write, so some information might be inaccurate and also I’m jumping through unnecessary hoops, from the perspective of just getting things done.

I’ll be using C++.

libdrm

The kernel DRM API is file based, which means you can open a file representing the graphics card (usually /dev/dri/card0) and use that file to send actual commands to the card, using the ioctl() syscall with the opened file representing the card.

Fortunately, there is also a library conveniently named libdrm that simplifies all that. Keep in mind that all that it does is what we mentioned above: opening a file and sending commands through ioctl() syscalls.

In C++ a simple program that interacts with the video card through DRM might look like this at the top level:

#include <cassert>
#include <unistd.h>
#include <fcntl.h>

// The file which represents the card
static const char gpu_dev[] = "/dev/dri/card0";

int open_gpu() {
  return open(gpu_dev, O_RDWR);
}

int close_gpu(int gpu) {
  return close(gpu);
}

int main(int arg_count, char** arg_vector) {
  const int gpu = open_gpu();
  assert(gpu >= 0);

  // some ioctl() magic here...

  assert(close_gpu(gpu) >= 0);

  return 0;
}

And as a sanity check we can try to run it in a Linux machine:

❯ clang++ drm-part-1.cpp -o drm-part-1
❯ ./drm-part-1
❯

Surely enough, it runs! But it doesn’t do anything yet. It needs that ioctl() magic, but at least no assertions failed and we know we can open the video card file now!

Lets include another library which has some very handy structs and also definitions of parameters for ioctl(): libdrm/drm.h. While we are at it, lets also include sys/ioctl.h and iostream for syscalls and printing.

#include <libdrm/drm.h>
#include <sys/ioctl.h>
#include <iostream>

Driver Version

The first interaction that we are going to have with the card is not going to be with the card itself but just with the driver (bummer, I know). Lets try to retrieve the driver version using a ioctl() call.

There is a definition for a struct which holds exactly this data in libdrm/drm.h, drm_version_t:

struct drm_version {
	int version_major;	  /**< Major version */
	int version_minor;	  /**< Minor version */
	int version_patchlevel;	  /**< Patch level */
	__kernel_size_t name_len;	  /**< Length of name buffer */
	char *name;	  /**< Name of driver */
	__kernel_size_t date_len;	  /**< Length of date buffer */
	char *date;	  /**< User-space buffer to hold date */
	__kernel_size_t desc_len;	  /**< Length of desc buffer */
	char *desc;	  /**< User-space buffer to hold desc */
};

To get the version from the driver we need to do three things:

  1. Initialize a drm_version_t, notice there are some char * fields for which we need to allocate memory.
  2. Call ioctl() with the appropriate parameters and a pointer to our drm_version_t.
  3. Print out the information we get and free everything.

Seems easy enough right? Lets do it!

drm_version_t create_version() {
  drm_version_t version;

  version.name = new char[256];
  version.date = new char[256];
  version.desc = new char[256];

  return version;
}

And:

void delete_version(const drm_version_t &version) {
  delete version.name;
  delete version.date;
  delete version.desc;
}

To print it out:

void print_version(const drm_version_t &version) {
  std::cout << std::string(version.name, version.name_len) << std::endl
            << std::string(version.desc, version.desc_len) << std::endl
            << version.version_major << "."
            << version.version_minor << "."
            << version.version_patchlevel << std::endl;
}

And finally, we can call all that in our main(), notice the ioctl() call using the DRM_IOCTL_VERSION value, which is defined in libdrm so we don’t have to worry about the specific values passed:

int main(int arg_count, char** arg_vector) {
  const int gpu = open_gpu();
  assert(gpu >= 0);

  drm_version_t version = create_version();
  assert(ioctl(gpu, DRM_IOCTL_VERSION, &version) == 0);
  print_version(version);
  delete_version(version);

  assert(close_gpu(gpu) >= 0);

  return 0;
}

After a little compiling, behold the power!

❯ ./drm-part-1
i915
Intel Graphics
1.6.0

We are talking to the DRM driver of my Intel graphics card! No X11 involved, this runs even directly in the tty!

Keep in mind that libdrm does a lot more than just provide the definitions and we do not need to be calling ioctl() directly, but for the purposes of learning I found important know exactly how the syscalls are made to the graphics driver.

drm-part-1.cpp


Part II Part III