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:
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.
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.
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.
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.
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.
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.
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).
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.
EGLSurface egl_surface = eglCreateWindowSurface(egl_display, config, gbm_surface, NULL);
Here we are creating a new EGL surface bound to a GBM surface.
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.
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.
drmModeCrtcPtr saved_crtc = drmModeGetCrtc(fd, kms_encoder->crtc_id);
This can be restored after the program is finished.
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);