SRM is a C library that simplifies the development of Linux DRM/KMS applications.
With SRM, you can focus on the OpenGL ES 2.0 logic of your application. For each available display, you can start a rendering thread that triggers common events like initializeGL(), paintGL(), resizeGL(), pageFlipped() and uninitializeGL().
It also ensures textures can be rendered across all screens, even if they are attached to different GPUs.
SRM is the main graphic backend used by the Louvre C++ Wayland Library, as depicted in the image below.
- Support for multi-GPU setups
- Automatic configuration of GPUs and connectors
- Texture allocation from main memory, DMA Buffers, GBM BOs and Wayland DRM Buffers
- Multi-session capability (e.g. can use libseat to open DRM devices)
- Listener for connectors hot-plugging events
- Simple cursor planes control
- V-Sync control (support for the atomic DRM API requires Linux >= 6.8)
- Framebuffer damage (enhances performance in multi-GPU setups where DMA support is lacking)
- Access to screen framebuffers as textures
- Direct Scanout (primary plane)
- Support for double and triple buffering
- Gamma correction
- Intel GPUs (i915 driver)
- NVIDIA GPUs (Nouveau & proprietary)
- AMD GPUs (AMDGPU driver)
- Mali GPUs (Lima driver)
In multi-GPU setups, connectors such as eDP, HDMI, DisplayPort, etc., can be attached to different GPUs. However, not all GPUs may support texture sharing, which could prevent, for example, a compositor from dragging a window across screens. Therefore, SRM assigns one of the following rendering modes to each GPU to ensure textures can always be rendered on all displays:
SELF MODE: If there's a single GPU or one that can handle all buffer formats from the allocator device, it directly renders into its own connectors, which is the best case.
PRIME MODE: When there are multiple GPUs and one cannot support all formats from the allocator, another GPU handles the rendering into a DMA-importable buffer, which is then blitted into the screen framebuffer. This approach is fast but not optimal, as only one GPU performs the "heavy" rendering.
DUMB MODE: In setups with multiple GPUs where one can't import buffers but supports dumb buffers, another GPU handles rendering and copies the result to dumb buffers for direct scanout. This is sub-optimal as only one GPU does the rendering and involves a GPU-to-CPU copy.
CPU MODE: Similar to the previous case, but when dumb buffers are not supported, the rendered content is copied to main memory. Then, a texture supported by the affected GPU is updated from the pixel data and rendered into the screen framebuffer. This is the worst-case scenario as it involves CPU copying for both reading and uploading to GPUs.
Performance in the last three modes can be significantly improved by specifying the damage generated during a paintGL()
event using srmConnectorSetBufferDamage()
.
#include <SRMCore.h>
#include <SRMDevice.h>
#include <SRMConnector.h>
#include <SRMConnectorMode.h>
#include <SRMListener.h>
#include <SRMList.h>
#include <SRMLog.h>
#include <GLES2/gl2.h>
#include <math.h>
#include <fcntl.h>
#include <unistd.h>
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
};
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. */
SRMConnectorMode *mode = srmConnectorGetCurrentMode(connector);
glViewport(0,
0,
srmConnectorModeGetWidth(mode),
srmConnectorModeGetHeight(mode));
// 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
};
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-basic] Failed to initialize connector %s.",
srmConnectorGetModel(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() here is not required. */
}
int main(void)
{
SRMCore *core = srmCoreCreate(&srmInterface, NULL);
if (!core)
{
SRMFatal("[srm-basic] Failed to initialize SRM core.");
return 1;
}
// Subscribe to Udev events
SRMListener *connectorPluggedEventListener = srmCoreAddConnectorPluggedEventListener(core, &connectorPluggedEventHandler, NULL);
SRMListener *connectorUnpluggedEventListener = srmCoreAddConnectorUnpluggedEventListener(core, &connectorUnpluggedEventHandler, NULL);
// Find and initialize avaliable connectors
// Loop each GPU (device)
SRMListForeach (deviceIt, srmCoreGetDevices(core))
{
SRMDevice *device = srmListItemGetData(deviceIt);
// Loop each GPU connector (screen)
SRMListForeach (connectorIt, srmDeviceGetConnectors(device))
{
SRMConnector *connector = srmListItemGetData(connectorIt);
if (srmConnectorIsConnected(connector))
{
if (!srmConnectorInitialize(connector, &connectorInterface, NULL))
SRMError("[srm-basic] Failed to initialize connector %s.",
srmConnectorGetModel(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;
}
/* Unsubscribe to DRM events
*
* These listeners are automatically destroyed when calling srmCoreDestroy()
* so there is no need to free them manually.
* This is here just to show how to unsubscribe to events on the fly. */
srmListenerDestroy(connectorPluggedEventListener);
srmListenerDestroy(connectorUnpluggedEventListener);
// Finish SRM
srmCoreDestroy(core);
return 0;
}