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
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;
static int openRestricted(const char *path, int flags, void *userData)
{
return open(path, flags);
}
static void closeRestricted(int fd, void *userData)
{
close(fd);
}
{
.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:38
int(* openRestricted)(const char *path, int flags, void *data)
Definition SRMCore.h:47
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()
{
if (!core)
{
SRMFatal(
"[srm-example] Failed to create SRMCore.");
return 1;
}
return 0;
}
void srmCoreDestroy(SRMCore *core)
Uninitializes all initialized connectors, removes all resources associated, closes all DRM devices,...
struct SRMCoreStruct SRMCore
Definition SRMTypes.hh:108
SRMCore * srmCoreCreate(SRMInterface *interface, void *userData)
Creates a new SRMCore instance.
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()
{
if (!core)
{
SRMFatal(
"[srm-example] Failed to create SRMCore.");
return 0;
}
{
{
SRMLog(
"[srm-example] - Connector %d %s %s %s.",
}
}
return 0;
}
const char * srmConnectorGetManufacturer(SRMConnector *connector)
Get the manufacturer of the connected display.
const char * srmConnectorGetName(SRMConnector *connector)
Get the name of the connector.
UInt32 srmConnectorGetID(SRMConnector *connector)
Get the DRM connector ID.
const char * srmConnectorGetModel(SRMConnector *connector)
Get the model of the connected display.
struct SRMConnectorStruct SRMConnector
Definition SRMTypes.hh:128
SRMList * srmCoreGetDevices(SRMCore *core)
Get a list of all available devices (SRMDevice).
SRMList * srmDeviceGetConnectors(SRMDevice *device)
Get a list of connectors of this device.
struct SRMDeviceStruct SRMDevice
Definition SRMTypes.hh:112
const char * srmDeviceGetName(SRMDevice *device)
Get the DRM device name (e.g., /dev/dri/card0) associated with this device.
#define SRMListForeach(item, list)
Iterate over items in a linked list from front to back.
Definition SRMList.h:206
void * srmListItemGetData(SRMListItem *item)
Get the data associated with an SRMListItem.
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:
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)
{
glViewport(0,
0,
}
static void paintGL(
SRMConnector *connector,
void *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);
}
static void resizeGL(
SRMConnector *connector,
void *userData)
{
initializeGL(connector, userData);
}
static void pageFlipped(
SRMConnector *connector,
void *userData)
{
}
static void uninitializeGL(
SRMConnector *connector,
void *userData)
{
}
{
.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:132
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()
{
if (!core)
{
SRMFatal(
"[srm-example] Failed to create SRMCore.");
return 1;
}
{
{
SRMLog(
"[srm-example] - Connector %d %s %s %s.",
{
{
SRMError(
"[srm-example] Failed to initialize connector %s",
}
}
}
}
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:
{
SRMError(
"[srm-example] Failed to initialize connector %s",
}
{
}
int main()
{
if (!core)
{
SRMFatal(
"[srm-example] Failed to create SRMCore.");
return 1;
}
{
{
SRMLog(
"[srm-example] - Connector %d %s %s %s.",
{
{
SRMError(
"[srm-example] Failed to initialize connector %s",
}
}
}
}
while (1)
{
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:144
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>
UInt8 pixelsSource[128 * 256 * 4];
core,
NULL,
128,
256,
128 * 4,
pixelsSource,
DRM_FORMAT_ARGB8888);
if (!buffer)
{
SRMError(
"Failed to create a buffer from main memory.");
exit(1);
}
buffer);
if (textureId == 0)
SRMError(
"Failed to get the GL texture ID from SRMBuffer.");
glBindTexture(textureTarget, textureId);
struct SRMBufferStruct SRMBuffer
Definition SRMTypes.hh:148
GLenum srmBufferGetTextureTarget(SRMBuffer *buffer)
Gets the OpenGL texture target associated with an SRMBuffer.
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.
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:56
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.