I. Contexte
Direct Rendering Manager (DRM)
Direct Rendering Manager ou DRM est un sous-système du noyau Linux servant d'interface avec le GPU qui fournit une API accessible en mode user.[1] Il permet d'échanger des données et des commandes ; en effet, la carte vidéo maintient dans sa mémoire une zone comprenant une file de commandes : la command queue.[2]
Le DRM se place comme le successeur de fbdev, un sous-système Linux plus ancien fournissant également une API permettant de modifier l'affichage.[3] Cette dernière permettait de gérer directement la zone mémoire du GPU ayant le contenu à afficher : le framebuffer. Le principal inconvénient de fbdev est l'impossibilité pour plusieurs programmes d'utiliser simultanément la mémoire vidéo : chaque programme considère qu'il est le seul à utiliser la VRAM.
Dans le cas de DRM, DRM dispose d'un accès exclusif au périphérique vidéo, il joue donc le rôle d'intermédiaire incontournable entre les programmes et le périphérique vidéo, permettant ainsi un accès en parallèle aux ressources vidéo par différents programmes.
Le DRM est accessible en espace utilisateur via un ensemble d'ioctls.[4]
![]() |
---|
Figure 1 - Communication avec la carte vidéo par deux programmes sans DRM[1] |
![]() |
---|
Figure 2 - Communication avec la carte vidéo par deux programmes avec DRM[1] |
Bibliothèque libdrm
Libdrm est une bibliothèque bas niveau en espace utilisateur se présentant sous la forme d'un wrapper encapsulant les ioctls de DRM.[5] Elle est utilisée par de nombreux pilotes graphiques (driver Mesa DRI, driver X, libva, etc.).
Kernel Mode Setting (KMS)
Un mode d'affichage est un ensemble de caractéristiques sur la manière dont l'affichage est formaté :
- résolution d'écran (nombre de pixels horizontaux * nombre de pixels verticaux) ;
- profondeur de couleurs (taille d'un pixel en bits) ;
- taux de rafraîchissement (fréquence d'actualisation de l'affichage en Hz).
Un dispositif d'affichage peut supporter plusieurs modes, nous devons donc en sélectionner un et l'appliquer : cette opération est appelée le mode-setting (ou modeset). Il est absolument nécessaire d'opérer un mode-setting avant d'utiliser un framebuffer car ce dernier voit ses dimensions et son empreinte mémoire fluctuer en fonction du mode.
Il existe deux manières d'opérer un mode-setting.
Premièrement, un mode-setting peut être appliqué en mode utilisateur (Userspace Mode Setting - UMS). Historiquement, une application pouvait changer le mode d'affichage d'elle-même. Cependant cette approche n'est pas idéale en raison de la porosité de l'isolation logiciel / matériel qu'elle entraîne, risquant ainsi des problèmes de sécurité et de stabilité. De plus, deux programmes effectuant un mode-setting simultanément peuvent laisser l'affichage dans un état incohérent.
D'autre part, un mode-setting peut être appliqué en mode système (Kernel Mode Setting - KMS). C'est cette approche qui est adoptée par Linux de nos jours, réglant les problèmes soulevés ci-dessus. Il s'agit en effet d'une composante essentielle de DRM. Des progrès en terme de stabilité ont été observés : la mise en veille de l'appareil pose moins de problèmes et on conserve une interface graphique en cas de plantage du serveur X. Mais aussi en terme de sécurité : grâce à KMS, il est possible de faire tourner le serveur X sans les privilèges root (No-Root X - NRX).[6]
II. Concepts (les objets DRM-KMS)
Pour mieux comprendre le fonctionnement de DRM-KMS, nous allons suivre le cheminement d'un signal vidéo : de la donnée en mémoire vers le pixel à l'écran. Cette donnée va être traitée par plusieurs objets durant son parcours.
Les structures présentées ci-dessous sont des abstractions d'objets noyau, qui sont référencés par un handle, un identifiant unique attribué par le noyau.
Framebuffer
Un framebuffer est une zone mémoire contenant une frame, sous forme de bitmap, à afficher à l'écran. Cette frame contient tous les pixels constituant une image à un instant précis.[7] Chaque pixel est défini par des composantes correspondant aux couleurs (ou au canal alpha pour la transparence, si le format de l'image en dispose).
Dans libdrm, un framebuffer est représenté par la structure drmModeFB
.
typedef struct _drmModeFB {
uint32_t fb_id;
uint32_t width, height;
uint32_t pitch;
uint32_t bpp;
uint32_t depth;
/* driver specific handle */
uint32_t handle;
} drmModeFB, *drmModeFBPtr;
Membres de _drmModeFB
:
fb_id
: ID du framebuffer, attribué par le noyau ;width, height
: Dimensions du framebuffer en pixels ;pitch
: Taille d'une ligne en octets + potentiel padding (aussi appelé « stride ») ;bpp
: Bits par pixel (souvent 32) ;depth
: Bits par composante (couleur / canal alpha) ;handle
: Identifiant unique du côté du pilote graphique.
Dans les faits, nous ne manipulons pas cette structure directement, on utilise simplement le handle pour discriminer un framebuffer.
Plane
Un plane représente une image qui peut être superposée ou incorporée à d'autres. Ces images peuvent optionnellement être rognées et / ou mises à l'échelle. Chaque plane est donc associé à un framebuffer comprenant l'image en question.
La recomposition de l'image finale est effectuée matériellement, sans utiliser de puissance de calcul CPU ou GPU. Cela permet à la fois une économie de ressources de calcul et d'opérations mémoire.
Il existe trois types de plane : [8], [9]
- Primary : Planes « principales », opérés par le mode-setting du CRTC et les opérations de flipping ;
- Cursor: Planes représentant un curseur, opérés par les ioctls du curseur.
- Overlay : Autres planes (aussi appelées « sprites ») ;
Les planes ne sont pas disponibles sur tous les périphériques vidéo et sont en nombre limité, en fonction du matériel.[10]
Dans libdrm, un plane est représenté par la structure drmModePlane
.
typedef struct _drmModePlane {
uint32_t count_formats;
uint32_t *formats;
uint32_t plane_id;
uint32_t crtc_id;
uint32_t fb_id;
uint32_t crtc_x, crtc_y;
uint32_t x, y;
uint32_t possible_crtcs;
uint32_t gamma_size;
} drmModePlane, *drmModePlanePtr;
Membres de drmModePlane
:
count_formats
: Nombre de formats de pixels ;formats
: Tableau des formats supportés (cf. libdrm/drm_fourcc.h) ;plane_id
: ID du plane, attribué par le noyau ;crtc_id
: ID du CRTC associé ;fb_id
: ID du framebuffer associé ;crtc_x, crtc_y
: Coordonnées du plane sur le CRTC ;x, y
: Offset sur le framebuffer ;possible_crtcs
: Bitmask des CRTC compatibles (un bit par CRTC) ;gamma_size
: Taille de la table de correction gamma.
Dans cette prise en main, nous n'utiliserons pas de correction gamma.
CRTC
Le nom « CRTC » (Cathode-Ray Tube Controller) fait référence au contrôleur matériel que l'on retrouvait dans les écrans cathodiques. Dans le contexte de KMS, le CRTC n'a rien à voir avec ce type de moniteurs et ce nom n'est qu'une relique du passé.[10]
Ici, un CRTC est un objet abstrait central dans toute communication avec le DRM.
Il permet d'abord de maintenir un flux de pixels. Ce flux est alimenté par les données issues d'un framebuffer et est ensuite transmis à l'encodeur.[9]
Ensuite, il occupe une place centrale dans le pipeline vidéo puisqu'il permet également de récupérer des informations depuis le connecteur comme les modes disponibles.[5]
Dans libdrm, un CRTC est représenté par la structure drmModeCrtc
.
typedef struct _drmModeCrtc {
uint32_t crtc_id;
uint32_t buffer_id; /**< FB id to connect to 0 = disconnect */
uint32_t x, y; /**< Position on the framebuffer */
uint32_t width, height;
int mode_valid;
drmModeModeInfo mode;
int gamma_size; /**< Number of gamma stops */
} drmModeCrtc, *drmModeCrtcPtr;
Membres de drmModeCrtc
:
crtc_id
: ID du CRTC, attribué par le noyau ;buffer_id
: ID du framebuffer associé ;x, y
: Position du framebuffer sur l'écran ;width, height
: Dimensions du CRTC en pixels ;mode_valid
: Booléen indiquant la validité du mode courant (1) ou non (0) ;mode
: Structure contenant des informations sur le mode ;gamma_size
: Taille de la table de correction gamma
Encodeur
Le rôle de l'encodeur est de convertir des données issues du CRTC en un format adéquat pour le connecteur.[10]
Dans libdrm, un encodeur est représenté par la structure drmModeEncoder
.
typedef struct _drmModeEncoder {
uint32_t encoder_id;
uint32_t encoder_type;
uint32_t crtc_id;
uint32_t possible_crtcs;
uint32_t possible_clones;
} drmModeEncoder, *drmModeEncoderPtr;
Membres de _drmModeEncoder
:
encoder_id
: ID de l'encodeur, attribué par le noyau ;encoder_type
: Type du connecteur associé (HDMI, VGA, DP, etc.) ;crtc_id
: ID du CRTC associé ;possible_crtcs
: Bitmask des CRTC compatibles ;possible_clones
: Bitmask des encodeurs compatibles pour le clonage.
Astuce : clonage
Un CRTC peut se connecter à plusieurs encodeurs en sortie, cela résultera en un clonage de l'affichage sur les moniteurs connectés à ces encodeurs.[10]
Connecteur
Le connecteur est l'objet représentant un connecteur physique de sortie vidéo (HDMI, VGA, etc.). Il détient des informations sur le moniteur qui y est connecté, le cas échéant.[10]
Dans libdrm, un connecteur est représenté par la structure drmModeConnector
.
typedef struct _drmModeConnector {
uint32_t connector_id;
uint32_t encoder_id; /**< Encoder currently connected to */
uint32_t connector_type;
uint32_t connector_type_id;
drmModeConnection connection;
uint32_t mmWidth, mmHeight; /**< HxW in millimeters */
drmModeSubPixel subpixel;
int count_modes;
drmModeModeInfoPtr modes;
int count_props;
uint32_t *props; /**< List of property ids */
uint64_t *prop_values; /**< List of property values */
int count_encoders;
uint32_t *encoders; /**< List of encoder ids */
} drmModeConnector, *drmModeConnectorPtr;
Membres de drmModeConnector
:
connector_id
: ID du connecteur, attribué par le noyau ;encoder_id
: ID de l'encodeur associé ;connector_type
: Type du port physique (HDMI, VGA, etc.) ;connector_type_id
: ID du port physique ;connection
: État du connecteur (connecté / déconnecté) ;mmWidth, mmHeight
: Dimensions en millimètres ;subpixel
: Disposition des sous-pixels (arrangement physique des composantes d'un pixel sur le moniteur) ;count_modes
: Nombre de modes supportés par le moniteur ;modes
: Tableau de modes d'affichage ;count_props
: Nombre de propriétés associées au connecteur ;props
: Tableau des identifiants des propriétés ;prop_values
: Tableau des valeurs des propriétés ;count_encoders
: Nombre d'encodeurs compatibles ;encoders
: Tableau des encodeurs compatibles.
Nous nous intéressons seulement aux bases fondamentales sur cette prise en main, nous n'utiliserons pas les propriétés.
III. Le pipeline vidéo
![]() |
---|
Figure 3 - Schéma du pipeline vidéo |
Les flèches pleines représentent le flux de données, les flèches discontinues représentent les informations de configuration.[5],[9]
IV. Implémentation naïve
Dans cette section, nous apporterons des briques de bases pour démarrer un premier programme simple avec un affichage basique en utilisant libdrm.[5]
Récupération des ressources
Ouverture du descripteur de fichier
Nous aurons besoin d'utiliser les objets DRM-KMS présentés auparavant, ceux-ci sont spécifiques à une carte vidéo. Nous devons par conséquent communiquer avec la carte vidéo en question. Sur Linux, tout est fichier[11] et cela inclut les appareils physiques qui se situent dans le répertoire /dev
.
Nous devons donc commencer avant par récupérer le descripteur de fichier de l'appareil graphique, celui-ci est récupérable au chemin /dev/dri/card*
où l'astérisque est à remplacer par un numéro parmi les périphériques disponibles. [12]
// Exemple pour /dev/card0
int drm_fd = open("/dev/card0", O_RDWR | O_NONBLOCK);
if(!drm_fd) {
/* Gérer l'erreur */
}
Cette ouverture doit être faite avec les droits en lecture et écriture O_RDWR
. Le flag O_NONBLOCK
permet d'éviter de mettre le processus en attente dans le cas où une opération d'entrée / sortie ne peut pas être effectuée au moment de l'appel en avortant l'opération avec l'erreur EAGAIN
. Ce flag n'est pas obligatoire mais est très utile lorsqu'on applique la synchronisation verticale.
Structure des ressources DRM
À l'aide de notre descripteur de fichier, nous pouvons récupérer une structure contenant des pointeurs vers des objets parmi ceux présentés dans la section (II).
Cette structure de libdrm est drmModeRes
.
typedef struct _drmModeRes {
int count_fbs;
uint32_t *fbs;
int count_crtcs;
uint32_t *crtcs;
int count_connectors;
uint32_t *connectors;
int count_encoders;
uint32_t *encoders;
uint32_t min_width, max_width;
uint32_t min_height, max_height;
} drmModeRes, *drmModeResPtr;
Membres de drmModeRes
:
count_fbs
: Nombre de framebuffers disponibles ;fbs
: Tableau d'ID de framebuffers disponibles ;count_crtcs
: Nombre de CRTC disponibles ;crtcs
: Tableau d'ID de CRTC disponibles ;count_connectors
: Nombre de connecteurs disponibles ;connectors
: Tableau d'ID de connecteurs disponibles ;count_encoders
: Nombre d'encodeurs disponibles ;encoders
: Tableau d'ID d'encodeurs disponibles ;min_width, max_width
: Résolution minimale supportée par le matériel ;min_height, max_height
: Résolution maximale supportée par le matériel.
Pour récupérer cette structure, il faut appeler la fonction drmModeGetResources()
qui prend pour unique paramètre le descripteur de fichier de l'appareil vidéo et qui retourne un pointeur vers un drmModeRes
.[13]
drmModeRes *res = drmModeGetResources(drm_fd);
if(!res) {
/* Gérer l'erreur */
}
Récupération des connecteurs
Après avoir récupéré nos ressources, nous devons récupérer les connecteurs pour communiquer avec les moniteurs de notre choix. Pour ce faire, nous devons parcourir le tableau de connecteurs pointé par la structure de type drmModeRes
.
Il faut veiller à seulement garder les connecteurs qui pourraient nous être utiles. Autrement dit, nous ne gardons que les connecteurs actuellement connectés à un moniteur.
Pour chaque élément du tableau de connecteurs, il faut appeler la fonction drmModeConnector()
qui prend en paramètre le descripteur de fichier de la carte vidéo et l'ID d'un connecteur (un élement du tableau connectors
dans drmModeRes
).
for(uint32_t i = 0; i < res->count_connectors; ++i) {
drmModeConnector *drm_conn = drmModeGetConnector(drm_fd, res->connectors[i]);
/* Le i-ème connecteur n'existe pas */
if(!drm_conn)
continue;
/* Le i-ème connecteur n'est pas connecté */
if(drm_conn->connection != DRM_MODE_CONNECTED || drm_conn->count_modes < 1) {
drmModeFreeConnector(drm_conn);
continue;
}
/* Connecteur trouvé... */
}
Association d'un CRTC avec chaque connecteur
L'association d'un connecteur avec un encodeur est assez triviale : on peut simplement utiliser le premier encodeur compatible (champ encoders
de drmModeConnector
).
Une fois l'encodeur sélectionné, nous pouvons commencer à chercher un CRTC compatible avec cet encodeur. En revanche, si cet encodeur n'est compatible avec aucun CRTC alors on utilisera le prochain encodeur.
L'association présentée ici est naïve : on va associer à un connecteur le premier CRTC compatible. Cette manière de faire ne nous garantit pas des associations optimales en nombre mais elle est très simple à implémenter.[5]
Pour rappel, la structure drmModeEncoder
dispose d'un champ possible_crtcs
qui liste, sous la forme d'un bitmask, les CRTC compatibles avec cet encodeur. Par exemple, supposons que possible_crtcs == 0b0110001
, alors l'encodeur est compatible avec les CRTC se trouvant aux indices 0 ; 5 et 6 du tableau de CRTC (champ crtcs
de drmModeRes
).
Voici un exemple de fonction permettant de récupérer l'ID d'un CRTC pour un connecteur.
/**
* find_crtc() - Trouve un CRTC compatible pour un connecteur donné
* @drm_fd: Descripteur de fichier de la carte vidéo (/dev/dri/cardN)
* @res: Structure contenant les pointeurs vers les objets KMS
* @conn: Connecteur pour lequel on cherche un CRTC
* @taken_crtcs: Bitset des CRTC déjà utilisés
*
* Retourne :
* * 0: Aucun CRTC trouvé
* * Sinon : ID d'un CRTC compatible
*/
uint32_t find_crtc(int drm_fd, drmModeRes *res,
drmModeConnector *conn, uint32_t *taken_crtcs)
{
/* Boucle sur les connecteurs */
for(int i=0; i<drm_conn->count_encoders; ++i){
drmModeEncoder *enc = drmModeGetEncoder(drm_fd, drm_conn->encoders[i]);
/* Le i-ème encodeur n'existe pas */
if(!enc)
continue;
for(int j=0; j<res->count_crtcs; ++j){
/* Le j-ème CRTC correspond au i-ème bit d'un entier de 32 bits */
uint32_t bit = 1 << j;
/* Le j-ème CRTC n'est pas compatible avec le i-ème connecteur */
if((enc->possible_crtcs & bit) == 0)
continue;
/* Le j-ème CRTC est déjà pris */
if(*taken_crtcs & bit)
continue;
/* CRTC adéquat trouvé */
drmModeFreeEncoder(enc);
*taken_crtcs |= bit;
return res->crtcs[j];
}
drmModeFreeEncoder(enc);
}
/* Aucun CRTC trouvé */
return 0;
}
À partir de l'ID retourné par cette fonction, on peut trivialement récupérer l'objet CRTC correspondant grâce à la fonction drmModeGetCrtc()
.
drmModeCrtc *crtc = drmModeGetCrtc(drm_fd, crtc_id);
if(!crtc) {
/* Gérer l'erreur */
}
Initialisation du framebuffer
L'implémentation du framebuffer pose problème du fait de la présence de beaucoup de code spécifique au pilote ; elle est très dépendante du matériel.
Cependant, KMS fournit ce qu'on appelle un « dumb framebuffer ». Il s'agit d'un framebuffer réduit au strict minimum, sans accélération matérielle, mais qui ne nécessite rien de dépendant au pilote. On peut néanmoins faire un mmap en espace utilisateur et faire du rendu logiciel.[14]
Il n'existe pas de structure permettant de décrire un dumb framebuffer, on peut donc créer notre propre structure.
/**
* struct dumb_framebuffer - Décrit un dumb framebuffer
* @id: Objet de l'objet KMS
* @width: Largeur du framebuffer
* @height: Hauteur du framebuffer
* @stride: Stride (ou pitch) => taille d'une ligne + padding en octets
* @handle: Handle pilote (ID unique niveau pilote)
* @size: Taille du mapping mémoire
* @data: Zone mémoire mappée où l'écriture est possible
*/
struct dumb_framebuffer {
uint32_t id; /* Issue de drmModeAddFB2() */
uint32_t width; /* Définie par le développeur */
uint32_t height; /* Définie par le développeur */
uint32_t stride; /* Issue de drmModeCreateDumbBuffer() */
uint32_t handle; /* Issue de drmModeCreateDumbBudder() */
uint64_t size; /* Issue de drmModeCreateDumbBuffer() */
uint8_t *data; /* Issue du mmap */
};
Bien que libdrm ne fournisse pas de structure décrivant un dumb framebuffer, la bibliothèque fournit des wrappers autour des ioctls pour ce type de framebuffer.
Pour débuter la création de notre framebuffer, on doit commencer par enregistrer le dumb buffer avec ses dimensions auprès du kernel qui nous retournera un handle pour manipuler le buffer à l'aide de l'API DRM ainsi que le stride et la taille totale en octets. Pour cela, nous pouvons utiliser la fonction de libdrm drmModeCreateDumbBuffer()
.
Déclaration de drmModeCreateDumbBuffer()
dans xf86drmMode.h
:
/**
* Create a dumb buffer.
*
* Given a width, height and bits-per-pixel, the kernel will return a buffer
* handle, pitch and size. The flags must be zero.
*
* Returns 0 on success, negative errno on error.
*/
extern int
drmModeCreateDumbBuffer(int fd, uint32_t width, uint32_t height, uint32_t bpp,
uint32_t flags, uint32_t *handle, uint32_t *pitch,
uint64_t *size);
Voici un exemple d'utilisation :
struct dumb_framebuffer *fb = malloc(sizeof(struct dumb_framebuffer));
/* Entrées de l'appel à drmModeCreateDumbBuffer() */
uint32_t width = crtc->width; // Par exemple, la même dimension que le CRTC
uint32_t height = crtc->height;
uint32_t bpp = 32; // Dépend du format de pixels souhaité
/* Sorties de l'appel à drmModeCreateDumbBuffer() */
uint32_t handle, pitch;
uint64_t size;
if(drmModeCreateDumbBuffer(drm_fd, width, height, bpp, 0,
&handle, &pitch, &size)) {
perror("drmModeCreateDumbBuffer error");
free(fb);
/* Gérer l'erreur */
}
fb->width = width;
fb->height = height;
fb->stride = *pitch;
fb->handle = *handle;
fb->size = *size;
Après avoir enregistré notre dumb buffer auprès du noyau, nous pouvons créer un objet framebuffer, comme présenté dans la section du framebuffer.
Pour ce faire, nous pouvons soit utiliser la fonction drmModeAddFB()
, soit drmModeAddFB2()
. La seconde permet le choix d'un format de pixels et prend en charge les formats multiplanaires. On utilisera ici la seconde fonction mais sans traiter des formats multiplanaires. Le choix du format de pixels se fait parmi ceux présents dans libdrm/drm_fourcc.h
.
Cette fonction retourne le nouvel ID du framebuffer via un paramètre de sortie.
uint32_t handles[4] = { fb->handle };
uint32_t strides[4] = { fb->stride };
uint32_t offsets[4] = { 0 };
if(drmModeAddFB2(drm_fd, fb->width, fb_height, DRM_FORMAT_ARGB8888,
handles, strides, offsets, &fb->id, 0)) {
perror("drmModeAddFB2 error");
/* Gérer l'erreur */
}
On peut remarquer que certains paramètres sont des tableau de taille 4, ces tableaux sont utiles lorsque l'on utilise un format multiplanaire. Dans notre cas, nous n'utilisons que le premier élément du tableau.
Ensuite, la prochaine étape consiste à préparer le framebuffer pour un mmap. Cela consiste à appeler la fonction drmModeMapDumbBuffer
qui prend en paramètres d'entrée : le descripteur de fichier DRM et le handle du framebuffer. Cette fonction renvoie un offset dans un paramètre de sortie : l'offset qui sera utilisé pour le mmap.[14]
uint64_t mmap_offset;
if(drmModeMapDumbBuffer(drm_fd, *handle, &mmap_offset)){
perror("Error during dumb buffer mapping preparation");
/* Gérer l'erreur */
}
Puis, on peut effectuer le mmap sur le descripteur de fichier de la carte vidéo à l'aide de la taille du framebuffer et de l'offset que nous avons récupérés. Le pointeur vers cette zone mémoire sera accessible depuis le champ data
de la structure dumb framebuffer
.
fb->data = mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED,
drm_fd, mmap_offset);
if(fb->data == MAP_FAILED) {
perror("fb mmap error");
/* Gérer l'erreur */
}
À partir de maintenant, nous pouvons écrire dans notre framebuffer comme dans n'importe quelle zone mémoire.
À titre d'exemple, voici comment remplir notre framebuffer avec des pixels bleus en utilisant le format FourCC DRM_FORMAT_XRGB8888
[15]. Comme précisé dans le fichier libdrm/drm_fourcc.h
, c'est un format en little endian[16].
/**
* Format de pixels : DRM_FORMAT_XRGB8888
*
* { B G R X }
*/
const int blue[4] = { 0xFF, 0x00, 0x00, 0xFF };
for(size_t y=0; y<fb->height; ++y) {
uint8_t *row = fb->data + fb->_stride * y;
for(size_t x=0; x<fb->width; ++x) {
row[x*4+0] = blue[0]; // B
row[x*4+1] = blue[1]; // G
row[x*4+2] = blue[2]; // R
row[x*4+3] = blue[3]; // X
}
}
Récupération du mode
Avant d'afficher le framebuffer sur le moniteur, nous devons sélectionner un mode adéquat pour le modeset. On peut sélectionner le meilleur mode disponible de deux manières :
- en sélectionnant le mode ayant pour flag
DRM_MODE_TYPE_PREFERRED
; - ou en sélectionnant le premier mode du tableau.
Le premier mode du tableau est généralement le préféré (on économise une boucle dans ce cas).[5]
drmModeInfo mode = drm_conn->modes[0];
La structure décrivant un mode est drmModeModeInfo
:
typedef struct _drmModeModeInfo {
uint32_t clock;
uint16_t hdisplay, hsync_start, hsync_end, htotal, hskew;
uint16_t vdisplay, vsync_start, vsync_end, vtotal, vscan;
uint32_t vrefresh;
uint32_t flags;
uint32_t type;
char name[DRM_DISPLAY_MODE_LEN];
} drmModeModeInfo, *drmModeModeInfoPtr;
On y retrouve des informations intéressantes comme la largeur (hdisplay
), la hauteur (vdisplay
), le taux de rafraîchissement (vrefresh
) et les flags (flags
) qui nous permettent de déterminer précisément le mode préféré / natif du dispositif d'affichage.
Modeset
Maintenant, on peut afficher le contenu de notre framebuffer à l'écran. Pour cela, nous pouvons associer un framebuffer à un CRTC grâce à la fonction drmModeSetCrtc()
.
En plus de cette association, la fonction va permettre d'effectuer le modeset en suivant le mode passé en paramètre.
drmModeSetCrtc(drm_fd, crtc_id, fb->id, 0, 0, drm_conn->connector_id, 1, mode);
Cet ioctl n'aboutira pas s'il est effectué depuis un programme exécuté dans une session X.org ou un compositeur Wayland.[5]
Astuce : sauvegarde et restauration du CRTC
Il est judicieux de sauvegarder l'état courant du CRTC avant d'effectuer l'association entre notre CRTC et notre framebuffer.
Pour cela, avant d'effectuer modeset, on peut utiliser
drmModeGetCrtc()
pour garder l'état du CRTC de côté.drmModeCrtc *saved_crtc = drmModeGetCrtc(drm_id, crtc_id);
L'ID
crtc_id
est celui récupéré plus tôt avec notre fonctionfind_crtc
. Bien qu'on modifie le CRTCcrtc_id
après coup, la restauration fonctionnera parce qu'on récupère un pointeur vers une structure contenant l'état du CRTC à un instant précis.Pour restaurer le CRTC à cet état antérieur, il suffit simplement d'appeler
drmModeSetCrtc()
.uint32_t crtc_id = saved_crtc->id; uint32_t buffer_id = saved_crtc->buffer_id; uint32_t x = saved_crtc->x; uint32_t y = saved_crtc->y; uint32_t conn_id = drm_conn->connector_id; uint32_t mode = saved_crtc->mode; drmModeSetCrtc(drm_fd, crtc_id, buffer_id, x, y, conn_id, 1, mode);
V. Mise en place des planes
Nous avons vu jusqu'ici comment écrire des pixels bruts sur un framebuffer. Mais comment faire pour superposer des éléments qui pourraient se déplacer au dessus de notre image ? On pourrait réécrire le framebuffer à chaque fois mais cela demanderait énormément de ressources. La solution réside dans les planes.
Récupération des planes
Nous pouvons imaginer utiliser un plane pour le fond et un plane avec un élément qui bouge sur l'écran.
Pour pouvoir utiliser des planes, il faut d'abord appeler drmModeGetPlaneResources()
pour récupérer le tableau des ID de planes.
drmModePlaneRes *plane_res = drmModeGetPlaneResources(drm_fd);
if(plane_res) {
/* Gérer l'erreur */
}
Association plane - CRTC
Une fois ce tableau accessible, on peut associer un plane avec notre CRTC. Cette association est assez similaire à celle entre un CRTC et un encodeur.
On commence par parcourir le tableau contenant les ID des planes. Pour chaque ID, on récupère la structure drmModePlane
correspondante en appelant la fonction drmModeGetPlane()
.
for(size_t i=0; i<plane_res->count_planes; ++i){
// ...
uint32_t plane_id = plane_res->planes[i];
drmModePlane *plane = drmModeGetPlane(drm_fd, plane_id);
// ...
}
Ensuite, on doit vérifier que le plane soit compatible avec notre CRTC. Le champ possible_crtcs
de la structure drmModePlane
est un bitset représentant les CRTC compatibles.
Dans un premier temps, on doit alors récupérer le bit représentant notre CRTC.
for(size_t i=0; i<plane_res->count_planes; ++i){
// ...
// Récupérer le bit représentant le CRTC courant
int crtc_index = -1;
for(size_t j=0; j<res->count_crtcs; ++j){
if(res->crtcs[j] == crtc->crtc_id){
crtc_index = 1 << j;
break;
}
}
// Ne devrait pas se produire
if(crtc_index < 0){
fprintf(stderr, "Couldn't get CRTC index\n");
return 0;
}
// ...
}
On peut ensuite vérifier la compatibilité.
for(size_t i=0; i<plane_res->count_planes; ++i){
// ...
// Le i-ème plane est incompatible avec le CRTC
if(!(plane->possible_crtcs & crtc_index)){
drmModeFreePlane(plane);
continue;
}
// ...
}
Voici une fonction retournant l'ID d'un plane pour un CRTC donné.
/**
* find_plane - Trouve un plane compatible pour un CRTC donné
*
* @drm_fd: Descripteur de fichier DRM (/dev/dri/cardN)
* @res: Structure contenant les pointeurs vers les objets KMS
* @plane_res: Structure référençant tous les planes
* @crtc: Pointeur vers la structure DRM-KMS décrivant le CRTC
* @taken_planes: Bitset des planes déjà pris
*
* Returns:
* * 0: Aucun plane trouvé (erreur)
* * Sinon : ID du plane
*/
uint32_t find_plane(int drm_fd, drmModeRes *res,
drmModePlaneRes *plane_res, drmModeCrtc *crtc, uint32_t *taken_planes)
{
for(size_t i=0; i<plane_res->count_planes; ++i){
// Le i-ème plane est représenté par le i-ème bit d'un entier de 32 bits
uint32_t i_bit = 1 << i;
// Récupérer le bit représentant le CRTC courant
int crtc_index = -1;
for(size_t j=0; j<res->count_crtcs; ++j){
if(res->crtcs[j] == crtc->crtc_id){
crtc_index = 1 << j;
break;
}
}
// Ne devrait pas se produire
if(crtc_index < 0){
fprintf(stderr, "Couldn't get CRTC index (find_plane)\n");
return 0;
}
uint32_t plane_id = plane_res->planes[i];
drmModePlane *plane = drmModeGetPlane(drm_fd, plane_id);
// Le plane n'existe pas
if(!plane)
continue;
// Le i-ème plane est incompatible avec le CRTC
if(!(plane->possible_crtcs & crtc_index)){
drmModeFreePlane(plane);
continue;
}
// Le i-ème plane est déjà pris par un autre CRTC
if(*taken_planes & i_bit){
drmModeFreePlane(plane);
continue;
}
// Plane adéquat trouvé
*taken_planes |= i_bit;
drmModeFreePlane(plane);
return plane_id;
}
// Aucun plane trouvé
return 0;
}
Association framebuffer - plane
Maintenant que nous avons un plane à associer au CRTC, nous devons faire cette association et lier le plane au framebuffer afin d'alimenter le plane en données. Cette association est possible grâce à la fonction drmModeSetPlane()
.
int drmModeSetPlane(int fd, uint32_t plane_id, uint32_t crtc_id,
uint32_t fb_id, uint32_t flags,
int32_t crtc_x, int32_t crtc_y,
uint32_t crtc_w, uint32_t crtc_h,
uint32_t src_x, uint32_t src_y,
uint32_t src_w, uint32_t src_h);
Cette fonction prend les paramètres suivants :
drm_fd
: Descripteur de fichier de l'appareil DRM ;plane_id
: ID du plane à configurer ;crtc_id
: ID du CRTC à associer au plane ;fb_id
: ID du framebuffer à lier au plane ;flags
: Doit être 0 ;crtc_x, crtc_y
: Offset du plane sur le CRTC ;crtc_w, crtc_h
: Dimensions du CRTC ;src_x, src_y
: Offset depuis la source (rognage de l'image source) ;src_w, src_h
: Dimensions de l'image source.
Attention : les variables src_*
sont au format à virgule fixe [17] Q16.16 : les 16 bits de poids fort correspondent à la partie entière et les 16 bits de poids faibles correspondent à la partie décimale.
Les opérations exécutées par cette fonction sont synchronisées au vblank et sont bloquantes.
Déplacement d'un plane
Le déplacement d'un plane, sans modification de l'image, ne nécessite pas de modification sur son framebuffer.
Ce déplacement ne nécessite qu'un appel supplémentaire à drmModeSetPlane()
en changeant les coordonnées sur le CRTC (paramètres crtc_x
et crtc_y
).
VI. Libération des ressources
Les accesseurs de libdrm (fonctions drmModeGet*
) allouent de la mémoire sur le tas. Il faut par conséquent libérer les zones mémoires allouées par ces appels. Pour chaque accesseur, on a une fonction associée qui permet de libérer cette zone mémoire (fonctions drmModeFree*
).
Exemples :
drmModeGetConnector()
→drmModeFreeConnector()
;drmModeGetPlane()
→drmModeFreePlane()
;- etc.
VII. Déchirement d'écran : problème et solutions
Le déchirement d'écran
En exécutant notre programme, on peut voir que l'affichage est très imparfait : on observe le phénomène de déchirement d'écran (screen tearing).
![]() |
---|
Figure 3 - Exemple de déchirement d'écran[18] |
Dans ce cas, le framebuffer a été affiché en plein milieu de son écriture. On a donc une ou plusieurs déchirures (tear points). Entre chaque déchirure, on retrouve une portion de l'image à l'instant présent ou à un instant antérieur.
Différents facteurs peuvent provoquer ces déchirements. Dans notre cas, la cause est le manque de synchronisation entre l'écriture du framebuffer et son affichage à l'écran.[18]
![]() |
---|
Figure 4 - Schéma du problème de lecture / écriture sur le framebuffer[5] |
Sur ce schéma, les zones hachurées correspondent aux instants où le déchirement se produit par lecture d'un framebuffer incohérent.
Double mémoire tampon
Pour pallier ce problème, nous pouvons appliquer la technique de double mémoire tampon (double buffering). Cela consiste à utiliser deux framebuffers : l'un pour l'écriture et l'autre pour la lecture. Le moniteur affiche les données d'un framebuffer sur lequel l'appareil vidéo n'écrit pas quand la carte écrit sur un autre framebuffer, non affiché.[19]
Dès lors que le framebuffer en cours d'écriture est entièrement écrit et cohérent, alors on peut échanger les pointeurs de ces deux framebuffers. Ainsi, le buffer d'écriture devient celui de lecture pour l'affichage et le buffer de lecture devient le buffer d'écriture pour la prochaine frame.
struct dumb_framebuffer *fb = fb_back
// Écriture sur le framebuffer d'écriture (fb_back)
// ...
drmModeSetCrtc(drm_fd, crtc_id, fb, x, y, conn_id, 1, mode);
// Échange de pointeurs entre les deux buffers
fb_back = fb_front;
fb_front = fb;
Cette solution n'est pas parfaite car si l'échange des buffers se produit en pleine lecture, nous pourrons observer une déchirure.
Synchronisation verticale
Une alternative au double buffering est la synchronisation verticale. Ici, la carte vidéo attend que le moniteur soit prêt à afficher une image pour ensuite lui envoyer. On dit que le moniteur est « prêt » lorsqu'il a fini la lecture d'un framebuffer ; lorsqu'une frame vient d'être affichée dans son intégralité. [18]
La synchronisation verticale permet d'améliorer la qualité de l'image en réduisant considérablement les déchirures, au prix d'une latence plus élevée.
Nous avons donc besoin de réagir à cet évènement « prêt ». Par chance, DRM dispose d'un système d'évènements pouvant envoyer des notifications à certains moments précis du cycle d'affichage. L'élément qui nous intéresse est le « page flip » qui se produit à la fin d'une lecture.
Libdrm met à disposition une structure drmEventContext
, utile pour définir des fonctions de rappel (callbacks) sur les évènements DRM.[19]
Définition de drmEventContext
dans xf86drm.h
:
#define DRM_EVENT_CONTEXT_VERSION 4
typedef struct _drmEventContext {
/* This struct is versioned so we can add more pointers if we
* add more events. */
int version;
void (*vblank_handler)(int fd,
unsigned int sequence,
unsigned int tv_sec,
unsigned int tv_usec,
void *user_data);
void (*page_flip_handler)(int fd,
unsigned int sequence,
unsigned int tv_sec,
unsigned int tv_usec,
void *user_data);
void (*page_flip_handler2)(int fd,
unsigned int sequence,
unsigned int tv_sec,
unsigned int tv_usec,
unsigned int crtc_id,
void *user_data);
void (*sequence_handler)(int fd,
uint64_t sequence,
uint64_t ns,
uint64_t user_data);
} drmEventContext, *drmEventContextPtr;
Notons que pour récupérer les évènements d'un appareil DRM, il faut faire un poll(2)
sur son descripteur de fichier avant de pouvoir gérer un évènement. Ce descripteur de fichier sera marqué comme lisible (flag POLLIN
) dès qu'un évènement DRM survient. Lorsqu'aucun évènement n'est survenu et qu'on est simplement en attente (pas d'erreur), le poll(2)
renvoie le code erreur EGAGAIN
.[19]
i = 0;
time_t start = time(NULL);
end = time(NULL) + 5; // durée du programme : 5 secondes, imprécis.
while(time(NULL) <= end){
struct pollfd poll_fd = {
.fd = drm_fd,
.events = POLLIN
};
int poll_timeout = 5000;
int poll_status = poll(&poll_fd, 1, poll_timeout);
if(poll_status < 0 && errno != EAGAIN){
perror("error drm fd poll");
break;
}
if(poll_fd.revents & POLLIN){
drmEventContext evctx = {
.version = DRM_EVENT_CONTEXT_VERSION,
.page_flip_handler = page_flip_handler // pointeur de fn : callback
};
if(drmHandleEvent(drm_fd, &evctx) < 0){
perror("drmHandleEvent error a");
break;
}
}
++i;
}
time_t duration = time(NULL) - start;
printf("Program ended.\n\tDuration: %ld seconds\n\tFrames: %ld\n\tThroughput: %.2f FPS\n",
duration, i, (float)i/duration);
Si on exécute cette portion de code telle quelle, nous ne recevrons aucun évènement. C'est tout à fait normal parce qu'on ne s'est pas abonné au type d'évènement que l'on souhaite récupérer. Nous devons nous abonner à DRM_MODE_PAGE_FLIP_EVENT
. L'abonnement à l'évènement page flip est réalisé au moment de la demande d'un page flip (fonction drmModePageFlip
). Nous devons donc initier le cycle des page flips en appelant cette fonction une fois avant la boucle while ci-dessus.[20]
if(drmModePageFlip(drm_fd, crtc_id, fb->id, DRM_MODE_PAGE_FLIP_EVENT, data) < 0){
perror("drmModePageFLip");
}
Le dernier argument est passé comme user_data
à notre callback. Dans notre cas, nous pouvons passer un pointeur vers une structure personnalisée contenant toutes les informations nécessaires sur les objets KMS propres à un moniteur (CRTC, framebuffers, etc.).
Nous pouvons maintenant définir notre fonction de retour (ici page_flip_handler()
).
void page_flip_handler(int drm_fd, unsigned sequence, unsigned tv_seq,
unsigned tv_used, void *data)
{
struct connector *c = data;
struct dumb_framebuffer *fb = c->fb;
// Écriture sur le framebuffer
// ...
if(drmModePageFlip(drm_fd, c->crtc_id, fb->id, DRM_MODE_PAGE_FLIP_EVENT, c) < 0){
perror("drmModePageFLip");
}
}
La synchronisation verticale permet d'empêcher de démarrer une écriture au milieu d'une lecture. En revanche, il est possible d'avoir un déchirement d'écran dans le cas d'une écriture lente qui se termine après l'échéance (vblank). On peut observer ce phénomène en ralentissant notre programme, avec Valgrind par exemple.[5]
Pour aller plus loin : les timings
Pour plus d'informations en détail sur les timings d'affichage, consultez les sources : Understanding the Linux Graphics Stack training, Bootlin[9] et Understanding Linux LCD display timings, Bhuvan[21]
Synchronisation verticale avec double mémoire tampon
Pour éviter tout déchirement, on peut coupler la synchronisation verticale et le double buffering.
En effet, l'évènement page flip est prévu pour ce cas de figure : appliquer un page flip (échange des framebuffers d'écriture et de lecture) avec la synchronisation verticale.
On évite alors d'afficher un framebuffer qui n'aurait été que partiellement écrit en n'effectuant pas le page flip dans ce cas.
Ainsi, on n'observe plus de déchirement d'écran, même en passant notre exécutable par Valgrind.
Bibliographie :
- [1] Wikipedia contributors (2024). Direct Rendering Manager. Wikipedia
- [2] Michel É. (2023). Learn WebGPU for C++. GitHubLearn WebGPU for C++
- [3] Wikipedia contributors (2025b). Linux framebuffer. Wikipedia
- [4] Wikipedia contributors (2024b). IOCTL. Wikipedia
- [5] Ascent GitHub - ascent12/drm_doc : How to write a Linux DRM application.
- [6] Wikipedia contributors (2025c). Mode setting. Wikipedia
- [7] Wikipedia contributors (2025). Framebuffer. Wikipedia
- [8] Multiplane Overlay (MPO) — The Linux Kernel documentation
- [9] Understanding the Linux Graphics Stack training (2025) Bootlin
- [10] drm-kms(7) — libdrm-dev — Debian bookworm — Debian Manpages
- [11] Wikipedia contributors (2025a). Everything is a file. Wikipedia
- [12] drm(7) — libdrm-dev — Debian bookworm — Debian Manpages
- [13] drmModeGetResources(3) — libdrm-dev — Debian testing — Debian Manpages
- [14] drm-memory(7) — libdrm-dev — Debian testing — Debian Manpages
- [15] Wikipedia contributors (2024b). FourCC. Wikipedia
- [16] Wikipedia contributors (2025a). Endianness. Wikipedia
- [17] Wikipedia contributors (2025c). Fixed-point arithmetic. Wikipedia
- [18] Wikipedia contributors (2023). Screen tearing. Wikipedia
- [19] drmHandleEvent(3) — libdrm-dev — Debian unstable — Debian Manpages
- [20] NVIDIA Jetson Linux API Reference : Direct Rendering Manager | NVIDIA Docs
- [21] GitHub Chandra B. Understanding Linux LCD display timings
- [20] NVIDIA Jetson Linux API Reference : Direct Rendering Manager | NVIDIA Docs
- [21] GitHub Chandra B. Understanding Linux LCD display timings