SRM  v0.5.6-1
Simple Rendering Manager
🎓 Tutorial

In this tutorial, you will learn the basics of SRM to kickstart your journey into creating DRM/KMS applications with OpenGL ES 2.0.

Let's begin by creating an empty project directory, with a main.c and meson.build file inside. In this example, we will use Meson as our build system.

main.c

int main()
{
return 0;
}

meson.build

project('srm-example',
'c',
version : '0.1.0')
c = meson.get_compiler('c')
pkg = import('pkgconfig')
glesv2_dep = dependency('glesv2')
srm_dep = dependency('SRM')
m_dep = c.find_library('m')
sources = ['main.c']
executable(
'srm-example',
sources,
dependencies: [glesv2_dep, srm_dep, m_dep])

Now, let's configure the project by running the following commands in your project directory:

cd your_project_dir
meson setup builddir

You should observe output confirming that the GLESv2, SRM and Math libraries have been found, and a new builddir directory should be created.

...
Found pkg-config: /usr/bin/pkg-config (0.29.2)
Run-time dependency glesv2 found: YES 3.2
Run-time dependency srm found: YES 0.3.2
Library m found: YES
Build targets in project: 1

If this is not the case, and the libraries are not found, please double-check that you have installed the GLESv2 and SRM libraries correctly, or investigate if any environment configuration adjustments are necessary.

Please refer to the Downloads section for detailed installation instructions for SRM.

Now, in main.c let's set up an interface that allows SRM to open and close DRM devices.

#include <SRM/SRMCore.h>
#include <SRM/SRMDevice.h>
#include <SRM/SRMConnector.h>
#include <SRM/SRMConnectorMode.h>
#include <SRM/SRMList.h>
#include <SRM/SRMLog.h>
#include <GLES2/gl2.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>
static float color = 0.f;
/* Opens a DRM device */
static int openRestricted(const char *path, int flags, void *userData)
{
SRM_UNUSED(userData);
// Here something like libseat could be used instead
return open(path, flags);
}
/* Closes a DRM device */
static void closeRestricted(int fd, void *userData)
{
SRM_UNUSED(userData);
close(fd);
}
static SRMInterface srmInterface =
{
.openRestricted = &openRestricted,
.closeRestricted = &closeRestricted
};
#define SRM_UNUSED(var)
Macro to suppress "unused parameter" warnings.
Definition: SRMTypes.hh:42
Interface for managing DRM devices (/dev/dri/card*).
Definition: SRMCore.h:59
int(* openRestricted)(const char *path, int flags, void *data)
Definition: SRMCore.h:68

This interface handles the management of DRM file descriptors during SRMCore's device scanning process and when you call srmCoreDestroy().

Instead of relying solely on the open() and close() functions, you might consider incorporating a library like libseat to enhance your program's compatibility with multi-session setups, enabling seamless TTY switching (like in the srm-multi-session example).

Let's proceed by creating an SRMCore instance using this interface. If any errors arise during the SRMCore creation process, we will ensure a graceful program exit.

// ...
int main()
{
SRMCore *core = srmCoreCreate(&srmInterface, NULL);
if (!core)
{
SRMFatal("[srm-example] Failed to create SRMCore.");
return 1;
}
return 0;
}
SRMCore * srmCoreCreate(SRMInterface *interface, void *userData)
Creates a new SRMCore instance.
void srmCoreDestroy(SRMCore *core)
Uninitializes all initialized connectors, removes all resources associated, closes all DRM devices,...
struct SRMCoreStruct SRMCore
Definition: SRMTypes.hh:106
void SRMFatal(const char *format,...)
Report an unrecoverable error. SRM_DEBUG >= 1.

Devices and Connectors

Now lets enumerate all avaliable devices (GPUs) and their respective connectors (displays).

// ...
int main()
{
SRMCore *core = srmCoreCreate(&srmInterface, NULL);
if (!core)
{
SRMFatal("[srm-example] Failed to create SRMCore.");
return 0;
}
// Loop each GPU (device)
{
SRMDevice *device = srmListItemGetData(deviceIt);
SRMLog("[srm-example] Device %s connectors:", srmDeviceGetName(device));
// Loop each GPU connector (screen)
SRMListForeach (connectorIt, srmDeviceGetConnectors(device))
{
SRMConnector *connector = srmListItemGetData(connectorIt);
SRMLog("[srm-example] - Connector %d %s %s %s.",
srmConnectorGetID(connector),
srmConnectorGetName(connector),
}
}
return 0;
}
UInt32 srmConnectorGetID(SRMConnector *connector)
Get the DRM connector ID.
const char * srmConnectorGetModel(SRMConnector *connector)
Get the model of the connector.
const char * srmConnectorGetManufacturer(SRMConnector *connector)
Get the manufacturer of the connector.
const char * srmConnectorGetName(SRMConnector *connector)
Get the name of the connector.
struct SRMConnectorStruct SRMConnector
Definition: SRMTypes.hh:126
SRMList * srmCoreGetDevices(SRMCore *core)
Get a list of all available devices (SRMDevice).
struct SRMDeviceStruct SRMDevice
Definition: SRMTypes.hh:110
SRMList * srmDeviceGetConnectors(SRMDevice *device)
Get a list of connectors of this device.
const char * srmDeviceGetName(SRMDevice *device)
Get the DRM device name (e.g., /dev/dri/card0) associated with this device.
void * srmListItemGetData(SRMListItem *item)
Get the data associated with an SRMListItem.
#define SRMListForeach(item, list)
Iterate over items in a linked list from front to back.
Definition: SRMList.h:206
void SRMLog(const char *format,...)
Print a general message independent of the SRM_DEBUG value.

Here, we are simply iterating over each SRMDevice (GPU/DRM device) and its associated SRMConnectors (screens/displays), printing the DRM id, name, model, and manufacturer of each. Afterward, we conclude the program.

Lets compile the program by running:

cd builddir
meson compile

If there are no errors during the build process, you should find a new executable file in the builddir directory named srm-example. You can run it with the following command:

./srm-example

The output should display one or more devices along with their respective connectors information. For example, on my machine, which has a single GPU, the output appears as follows:

[srm-example] Device /dev/dri/card0 connectors:
[srm-example] - Connector 77 eDP-0 Color LCD Apple Computer Inc.
[srm-example] - Connector 84 DisplayPort-1 Unknown Unknown.
[srm-example] - Connector 92 HDMI-A-1 Unknown Unknown.
[srm-example] - Connector 98 DisplayPort-0 Unknown Unknown.
[srm-example] - Connector 104 HDMI-A-2 Unknown Unknown.
[srm-example] - Connector 108 HDMI-A-0 Unknown Unknown.

Please note that in the output, connectors may appear as "Unknown" for model and manufacturer if no display is attached to those connectors. This is the expected behavior.

In my case, you can observe that there is only one connected, which corresponds to my laptop screen (eDP-0). You can check the connectivity status of any connector with the srmConnectorIsConnected() function, which we will demonstrate in the upcoming sections.

Rendering

Now, let's delve into the process of rendering to the available connectors. Our approach involves setting up a unified interface for managing OpenGL rendering events, which will be shared across all connectors. While it's possible to employ distinct interfaces for each connector, for the sake of simplicity, we'll use a single one here.

It's of utmost importance to underscore that these events are initiated by SRM itself and should not be manually triggered by you. Additionally, it's essential to recognize that all these events are executed within the rendering thread of each connector, operating independently from the main thread.

// ...
static void initializeGL(SRMConnector *connector, void *userData)
{
SRM_UNUSED(userData);
/* You must not do any drawing here as it won't make it to
* the screen. */
glViewport(0,
0,
// Schedule a repaint (this eventually calls paintGL() later, not directly)
srmConnectorRepaint(connector);
}
static void paintGL(SRMConnector *connector, void *userData)
{
SRM_UNUSED(userData);
glClearColor((sinf(color) + 1.f) / 2.f,
(sinf(color * 0.5f) + 1.f) / 2.f,
(sinf(color * 0.25f) + 1.f) / 2.f,
1.f);
color += 0.01f;
if (color > M_PI*4.f)
color = 0.f;
glClear(GL_COLOR_BUFFER_BIT);
srmConnectorRepaint(connector);
}
static void resizeGL(SRMConnector *connector, void *userData)
{
/* You must not do any drawing here as it won't make it to
* the screen.
* This is called when the connector changes its current mode,
* set with srmConnectorSetMode() */
// Reuse initializeGL() as it only sets the viewport
initializeGL(connector, userData);
}
static void pageFlipped(SRMConnector *connector, void *userData)
{
SRM_UNUSED(connector);
SRM_UNUSED(userData);
/* You must not do any drawing here as it won't make it to
* the screen.
* This is called when the last rendered frame is now being
* displayed on screen.
* Google v-sync for more info. */
}
static void uninitializeGL(SRMConnector *connector, void *userData)
{
SRM_UNUSED(connector);
SRM_UNUSED(userData);
/* You must not do any drawing here as it won't make it to
* the screen.
* Here you should free any resource created on initializeGL()
* like shaders, programs, textures, etc. */
}
static SRMConnectorInterface connectorInterface =
{
.initializeGL = &initializeGL,
.paintGL = &paintGL,
.resizeGL = &resizeGL,
.pageFlipped = &pageFlipped,
.uninitializeGL = &uninitializeGL
};
// ...
UInt8 srmConnectorRepaint(SRMConnector *connector)
Schedules a new rendering frame.
SRMConnectorMode * srmConnectorGetCurrentMode(SRMConnector *connector)
Get the current connector mode.
struct SRMConnectorModeStruct SRMConnectorMode
Definition: SRMTypes.hh:130
UInt32 srmConnectorModeGetHeight(SRMConnectorMode *connectorMode)
Get the height of the connector mode.
UInt32 srmConnectorModeGetWidth(SRMConnectorMode *connectorMode)
Get the width of the connector mode.
Interface for OpenGL events handling.
Definition: SRMConnector.h:42
void(* initializeGL)(SRMConnector *connector, void *data)
Notifies that the connector has been initialized.
Definition: SRMConnector.h:51

Lets see what each event does:

  • initializeGL: This event is called once after a connector is initialized with srmConnectorInitialize(). Here you should set up all your necessary OpenGL resources, such as shaders, texture loading, etc. In this specific case, it configures the viewport using the dimensions of the current connector mode (SRMConnectorMode). A connector can have multiple modes, each defining resolution and refresh rate. Additionally, it calls srmConnectorRepaint(), which schedules a new rendering frame (paintGL() call) asynchronously.
  • resizeGL: This event is triggered when the current connector mode changes (set with srmConnectorSetMode()). Here, the main task is to update the viewport to match the new dimensions.
  • paintGL: Inside this event handler, you should perform all the OpenGL rendering operations required for the current frame. In the provided example, the screen is simply cleared with a random color and a new frame is scheduled with srmConnectorRepaint().
  • pageFlipped: This event is triggered when the last rendered frame (in paintGL()) is now being displayed on the screen (check Multiple Buffering).
  • uninitializeGL: This event is triggered just before the connector is uninitialized. Here you should free the resources created in initializeGL().

Important Note: It is imperative that you avoid initializing, uninitializing, or changing a connector's mode within its rendering thread, that is, from any of the event handlers. Doing so could lead to a deadlock or even cause your program to crash. Please be aware that this behavior is slated for correction in the upcoming SRM release.

Now lets use this interface to initialize all connected connectors.

// ...
int main()
{
SRMCore *core = srmCoreCreate(&srmInterface, NULL);
if (!core)
{
SRMFatal("[srm-example] Failed to create SRMCore.");
return 1;
}
// Loop each GPU (device)
{
SRMDevice *device = srmListItemGetData(deviceIt);
SRMLog("[srm-example] Device %s connectors:", srmDeviceGetName(device));
// Loop each GPU connector (screen)
SRMListForeach (connectorIt, srmDeviceGetConnectors(device))
{
SRMConnector *connector = srmListItemGetData(connectorIt);
SRMLog("[srm-example] - Connector %d %s %s %s.",
srmConnectorGetID(connector),
srmConnectorGetName(connector),
// Check if there is a display attached
if (srmConnectorIsConnected(connector))
{
// Initialize the connector
if (!srmConnectorInitialize(connector, &connectorInterface, NULL))
{
SRMError("[srm-example] Failed to initialize connector %s",
srmConnectorGetName(connector));
}
}
}
}
// Sleep 10 secs
usleep(10000000);
return 0;
}
UInt8 srmConnectorInitialize(SRMConnector *connector, SRMConnectorInterface *interface, void *userData)
Initializes a connector, creating its rendering thread and invoking initializeGL() once initialized.
UInt8 srmConnectorIsConnected(SRMConnector *connector)
Check if the connector is connected.
void SRMError(const char *format,...)
Report a nonfatal error. SRM_DEBUG >= 2.

Now, we're checking each connector's display attachment status using srmConnectorIsConnected() and initializing them with srmConnectorInitialize().

Additionally, note that we've included a usleep() call at the end to wait for 10 seconds. This delay is necessary because, as said before, each connector performs its rendering in its own thread. Blocking the main thread ensures that the program doesn't exit immediately.

Re-compile with meson compile and before running the program, switch to a free virtual terminal (TTY) by pressing CTRL + ALT + F[1, 2, 3 ..., 10] or with the chvt N command and launch it from there. You should observe your displays changing colors rapidly for 10 seconds.

If you encounter issues, please attempt to run the program with superuser privileges or by adding your user to the video group. This may resolve any potential permission-related problems.

Additionally, you have the option to set the SRM_DEBUG environment variable to 3 in order to enable fatal, error and warning logging messages.

Hotplugging Events

Thus far, we've discussed the process of identifying available connectors and initializing them at program startup. However, a critical consideration is what happens if one of these connectors becomes disconnected while the program is running, such as unplugging an HDMI display.

In such scenarios, the connectors are programmed to undergo automatic uninitialization when they become disconnected, triggering their corresponding uninitializeGL() event. However, you do have the flexibility to include listeners to detect and respond to connectors plugging and unplugging events, as exemplified below:

// ...
static void connectorPluggedEventHandler(SRMListener *listener, SRMConnector *connector)
{
SRM_UNUSED(listener);
/* This is called when a new connector is avaliable (E.g. Plugging an HDMI display). */
/* Got a new connector, let's render on it */
if (!srmConnectorInitialize(connector, &connectorInterface, NULL))
SRMError("[srm-example] Failed to initialize connector %s",
srmConnectorGetName(connector));
}
static void connectorUnpluggedEventHandler(SRMListener *listener, SRMConnector *connector)
{
SRM_UNUSED(listener);
SRM_UNUSED(connector);
/* This is called when a connector is no longer avaliable (E.g. Unplugging an HDMI display). */
/* The connnector is automatically uninitialized after this event (if initialized)
* so calling srmConnectorUninitialize() is not necessary. */
}
int main()
{
SRMCore *core = srmCoreCreate(&srmInterface, NULL);
if (!core)
{
SRMFatal("[srm-example] Failed to create SRMCore.");
return 1;
}
// Subscribe to udev events
srmCoreAddConnectorPluggedEventListener(core, &connectorPluggedEventHandler, NULL);
srmCoreAddConnectorUnpluggedEventListener(core, &connectorUnpluggedEventHandler, NULL);
// Loop each GPU (device)
{
SRMDevice *device = srmListItemGetData(deviceIt);
SRMLog("[srm-example] Device %s connectors:", srmDeviceGetName(device));
// Loop each GPU connector (screen)
SRMListForeach (connectorIt, srmDeviceGetConnectors(device))
{
SRMConnector *connector = srmListItemGetData(connectorIt);
SRMLog("[srm-example] - Connector %d %s %s %s.",
srmConnectorGetID(connector),
srmConnectorGetName(connector),
if (srmConnectorIsConnected(connector))
{
if (!srmConnectorInitialize(connector, &connectorInterface, NULL))
{
SRMError("[srm-example] Failed to initialize connector %s",
srmConnectorGetName(connector));
}
}
}
}
while (1)
{
/* Udev monitor poll DRM devices/connectors hotplugging events (-1 disables timeout).
* To get a pollable FD use srmCoreGetMonitorFD() */
if (srmCoreProcessMonitor(core, -1) < 0)
break;
}
return 0;
}
SRMListener * srmCoreAddConnectorPluggedEventListener(SRMCore *core, void(*callback)(SRMListener *, SRMConnector *), void *userData)
Registers a new listener to be invoked when a new connector is plugged.
SRMListener * srmCoreAddConnectorUnpluggedEventListener(SRMCore *core, void(*callback)(SRMListener *, SRMConnector *), void *userData)
Registers a new listener to be invoked when an already plugged connector is unplugged.
Int32 srmCoreProcessMonitor(SRMCore *core, Int32 msTimeout)
Dispatch pending udev monitor events or block until an event occurs or a timeout is reached.
struct SRMListenerStruct SRMListener
Definition: SRMTypes.hh:142

Now, each time a new connector becomes available, connectorPluggedEventHandler() will be invoked, allowing us to initialize the new connector. Similarly, we can detect when a connector is disconnected using connectorUnpluggedEventHandler(). If an initialized connector gets disconnected, it is automatically uninitialized, triggering the uninitializeGL() function.

One notable change is the replacement of the usleep() function with an infinite while loop. Within this loop, we poll a udev monitor file descriptor using the srmCoreProcessMonitor() function. This change is necessary to allow SRM to check and invoke the hotplugging events.

To test these changes, recompile the program and try connecting and disconnecting an external display on the fly. You should observe that it is automatically initialized and uninitialized each time, reflecting the hotplugging events.

Buffers

SRM lets you create buffers (OpenGL textures) from various sources, including DMA planes, GBM buffer objects, Wayland DRM buffers, and main memory buffers. These buffers can be used for rendering on all connectors, even if they belong to different devices.

Let's see how to create a buffer from main memory:

#include <SRM/SRMBuffer.h>
// ...
// 128 x 256 ARGB8 image in main memory
UInt8 pixelsSource[128 * 256 * 4];
// Pass NULL as the allocator device to share the buffer across all devices
core, // SRM core
NULL, // allocator device
128, // src width
256, // src height
128 * 4, // src stride
pixelsSource,
DRM_FORMAT_ARGB8888);
if (!buffer)
{
SRMError("Failed to create a buffer from main memory.");
exit(1);
}
// ...
// Use the buffer in a connector rendering thread (paintGL() event)
// Retrieve the OpenGL texture ID
GLuint textureId = srmBufferGetTextureID(
buffer);
if (textureId == 0)
SRMError("Failed to get the GL texture ID from SRMBuffer.");
GLenum textureTarget = srmBufferGetTextureTarget(buffer);
glBindTexture(textureTarget, textureId);
// ... glDrawArrays()
SRMBuffer * srmBufferCreateFromCPU(SRMCore *core, SRMDevice *allocator, UInt32 width, UInt32 height, UInt32 stride, const void *pixels, SRM_BUFFER_FORMAT format)
Creates an SRMBuffer from main memory buffer.
struct SRMBufferStruct SRMBuffer
Definition: SRMTypes.hh:146
GLenum srmBufferGetTextureTarget(SRMBuffer *buffer)
Get the OpenGL texture target associated with an SRMBuffer.
GLuint srmBufferGetTextureID(SRMDevice *device, SRMBuffer *buffer)
Retrieves an OpenGL texture ID associated with an SRMBuffer for a specific device (GPU).
SRMDevice * srmConnectorGetRendererDevice(SRMConnector *connector)
Retrieve the renderer device associated with the connector.
uint8_t UInt8
Alias for an unsigned 8-bit integer (uint8_t).
Definition: SRMTypes.hh:54

It's essential to acknowledge that all buffers are shared across all devices, with the exception of those created from GBM buffers or Wayland DRM buffers, which may not always be supported by all devices.

Furthermore, you have the option to read from and write to these buffers, and they are automatically synchronized across all devices. For more in-depth information, please refer to the SRMBuffer documentation.