DRI GBM DRM KMS EGL WTF

You can render OpenGL directly to your screen in Linux, without Wayland or X11 - how cool is that?! In order to do so you need a relatively small amount of code. The code below are just the main calls to just demonstrate the core parts. Here is how it works:

Step 1. Open a filehandle to the video card

int fd = open("/dev/dri/card0", O_RDWR);

DRI stands for Direct Rendering Infrastructure. This device represents a video card. That card might have multiple screen connectors (more on that later). In the same directory is a /dev/dri/renderD128 or something like that. This represents a way to run code on a card without a graphical context for doing compute operations. We do not need it for rendering graphics.

Step 2. Get all the physical connectors on the card

drmModeRes *resources = drmModeGetResources(fd);
for (uint8_t i = 0; i < resources->count_connectors; i++) {
    drmModeConnector *connector = drmModeGetConnector(fd, resources->connectors[i]);
    ...
}

Here we are discovering all the physical connectors plugged into the card. These could be VGA, DVI, HDMI or a LVDS (Low-voltage Differential Signaling) connector inside a laptop.

Step 3. Get encoders and match to connectors

for (uint8_t i = 0; i < resources->count_encoders; i++) {
    drmModeEncoder *encoder = drmModeGetEncoder(fd, resources->encoders[i]);
    if (encoder->encoder_id == connector->encoder_id) {
        // A match!
    }
}

An encoder converts pixel data into the format required for that connector. Here we are trying to match the available encoders with their physical connectors.

Step 4. Remember the CRTC for the encoder

drmModeCrtc* crtc = drmModeGetCrtc(fd, encoder->crtc_id);

CRTC stands for CRT Controller. I am not entirely clear on what this abstraction does, but in code examples it gets saved before entering a render loop, and restored when we stop our program.

Step 5. Create a GBM device

struct gbm_device* gbm = gbm_create_device(fd);

GBM stands for Generic Buffer Management. These are render targets, and are chunks of memory where we will render the image to.

Step 6. Initialize an EGL display from the GBM device

EGLDisplay egl_display = eglGetDisplay(gbm);
int ret = eglInitialize(egl_display, &major_v, &minor_v);
const char *version = eglQueryString(context->egl_display, EGL_VERSION);

The name EGL comes from OpenGL ES, and is the glue between OpenGL and GBM. I am pretty sure a "display" here does not mean a physical display.

Step 7. Create a GBM surface

struct gbm_surface* gbm_surface = gbm_surface_create(gbm, width, height, GBM_BO_FORMAT_XRGB8888, GBM_BO_USE_SCANOUT|GBM_BO_USE_RENDERING);

The "width" and "height" here come from the selected mode from the connector, so it has to be done after choosing a connector, and a corresponding "mode" (resolution etc). I believe if you were dealing with multiple physical displays with different sizes, you would need a GBM surface for each one. You are probably able to use the same GBM surface for several displays as long as you are able to use the same mode for each display (mirroring).

Step 8. Choose an EGL config

EGLint attributes[] = {
    EGL_RED_SIZE, 8,
    EGL_GREEN_SIZE, 8,
    EGL_BLUE_SIZE, 8,
    EGL_NONE
};
EGLConfig config;
EGLint num_config;

eglBindAPI(EGL_OPENGL_API);
eglChooseConfig(egl_display, attributes, &config, 1, &num_config);

I haven't explored yet how many possible configs get created or how to choose them.

Step 9. Create an EGL surface from the EGL config and GBM surface

EGLSurface egl_surface = eglCreateWindowSurface(egl_display, config, gbm_surface, NULL);

Here we are creating a new EGL surface bound to a GBM surface.

Step 10. Create an EGL context

EGLContext egl_context = eglCreateContext(egl_display, config, EGL_NO_CONTEXT, NULL);

An EGL context is a state that is manipulated by OpenGL calls. Since OpenGL is statefull this is the context that tracks this state.

Step 11. Bind all the EGL stuff together

eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);

This is telling EGL that every time you make an OpenGL call from now on you are bound to this context.

Step 12. Save the CRTC

drmModeCrtcPtr saved_crtc = drmModeGetCrtc(fd, kms_encoder->crtc_id);

This can be restored after the program is finished.

Step 13. Enter into the render loop

struct gbm_bo* previous_bo;
uint32_t previous_fb;
struct gbm_bo* bo;
uint32_t handle;
uint32_t pitch;
uint32_t fb;
uint8_t quit = 0;

do {
    // Do all GL drawing here
    eglSwapBuffers(egl_display, egl_surface);
    bo = gbm_surface_lock_front_buffer(gbm_surface);
    handle = gbm_bo_get_handle(bo).u32;
    pitch = gbm_bo_get_stride(bo);
    drmModeAddFB(fd, width, height, 24, 32, pitch, handle, &fb);
    drmModeSetCrtc(fd, kms_crtc->crtc_id, fb, 0, 0, &kms_connector->connector_id, 1, &kms_mode);
    if (previous_bo) {
        drmModeRmFB(context->fd, previous_fb);
        gbm_surface_release_buffer(gbm_surface, previous_bo);
    }
    previous_bo = bo;
    previous_fb = fb;
    nanosleep(&ts, NULL);
} while (!quit);