99 KiB
Capítulo 25 — DRM/KMS: Gráficos sin intermediarios
Capítulo avanzado, pero escrito para que se entienda de verdad. Se apoya en file descriptors (
22-file-descriptors), punteros (07), memoria dinámica (09) y structs (08).Este capítulo te lleva del texto plano al video: escribir píxeles directamente en la pantalla, sin X11, sin Wayland, sin SDL, sin Raylib. Solo Linux + tu GPU + syscalls crudas envueltas por libdrm y libgbm.
Cómo leer este capítulo
Imaginate que abrís la terminal y escribís ls. La salida aparece en una ventana, en una fuente,
con colores. Todo eso lo maneja un servidor de ventanas (X11 o Wayland). Pero ese servidor
no es magia: alguien le tiene que decir al kernel "poné esta imagen en la pantalla".
Ese "alguien" es DRM/KMS. Es el subsistema del kernel que controla directamente la GPU y los monitores conectados, sin intermediarios. Cuando apagás la laptop y la pantalla se pone negra, es el kernel quitando el scanout (el barrido de píxeles) porque DRM lo dispuso así.
DRM/KMS es duro. No hay funciones como drawCircle() ni setResolution(). Hay ioctls que
manejá handles, IDs y punteros crudos a buffers de memoria. Pero una vez que entendés el flujo:
Abrís el dispositivo → enumerateás monitores y modos → creás un buffer → lo mapeás a memoria → dibujás píxeles directamente en RAM → le decís al kernel "mostrá este buffer" → repetís.
Ese es el ciclo de vida de toda aplicación gráfica en Linux sin windowing system. Todo lo que verán en este capítulo es variaciones sobre ese tema.
Está dividido en catorce partes. Después de cada concepto hay una o más prácticas —uno o más ejercicios— para que los uses ya mismo. No las saltee: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| 1 | ¿Qué es DRM/KMS? | El kernel controla tu pantalla; DRM es el camino directo para userspace. |
| 2 | Abrir /dev/dri/cardN |
Cada GPU tiene un fd: abrí el primero disponible y listo. |
| 3 | Enumerar recursos | Conectores, CRTC, encoders — todo con drmModeGetResources. |
| 4 | Conectores y estados | Qué monitor está conectado, tipo de puerto, dimensiones físicas. |
| 5 | Modos de pantalla | Resoluciones y refresh rates; elegir uno y aplicarlo a un CRTC. |
| 6 | Dumb buffers | Crear buffer del kernel, mapearlo a memoria CPU con mmap. |
| 7 | Punto y línea en C puro | El dibujo más básico: un píxel y Bresenham. |
| 8 | Framebuffers | Asociar un handle de buffer a un fb_id para que el CRTC lo escanee. |
| 9 | Page flip + vblank | Doble buffering: cambiás el buffer visible al próximo vblank, cero tearing. |
| 10 | KMS atómico | Configurar CRTCs, modos y planes en UNA sola operación; base del multi-monitor. |
| 11 | Eventos DRM y hotplug | poll() + handlers para detectar conexión/desconexión de monitores. |
| 12 | OpenGL ES + EGL sobre DRM | Ponte entre GLES y el hardware sin windowing system. |
| 13 | NanoVG: shapes, gradientes y texto | rects, circles, arcs, bezier paths y TTF sobre GLES + DRM. |
| 14 | Vulkan surfaceless | Swapchain GBM-backed, pipeline raster, dibujar sin windowing system. |
| 15 | Ejercicio jefe: dashboard UI animado | Multi-monitor, nanoVG shapes + fuentes, page flip, hotplug, todo junto. |
Regla de oro del libro: el capítulo explica y muestra cómo funciona cada ioctl/función, pero los ejercicios no traen la solución. El enunciado te da el problema; la respuesta la escribís vos.
PARTE 1 — ¿Qué es DRM/KMS?
DRM = Direct Rendering Manager. KMS = Kernel Mode Setting. Son dos caras de la misma moneda: el subsistema del kernel que gestiona el hardware gráfico.
1.1 El problema: ¿quién controla la pantalla?
En Windows o macOS, el driver gráfico y el windowing system están estrechamente acoplados. En Linux, todo está separado:
- El kernel (DRM) controla el hardware: qué resolución se muestra, qué buffer se escanea, cuándo hacer page flip.
- El windowing system (X11/Wayland) gestiona ventanas, input, composición.
- Las aplicaciones dibujan en buffers que el compositor (o el kernel directamente) presenta al usuario.
Cuando usás X11 o Wayland, ellos hablan con DRM por vos. Pero si los sacás del medio:
Podés hablarle directo al kernel. Abrís
/dev/dri/card0, enviás ioctls, y el kernel configura tu monitor a la resolución que querés, muestra el buffer que le digas, sin intermediarios.
Eso es exactamente lo que hacen los display servers (X11/Wayland). Solo que acá lo hacés vos.
1.2 Los bloques fundamentales de DRM/KMS
Todo en DRM/KMS se reduce a unos pocos conceptos del kernel:
| Concepto | Qué es | Analogía |
|---|---|---|
| Connector | El puerto físico (HDMI, DP, VGA) | La toma de corriente |
| Encoder | Convierte señal digital → señal del conector | El adaptador de corriente |
| CRTC | Controlador: qué fb y qué modo se escanean | El enchufe en la pared |
| Mode | Resolución + refresh rate (1920×1080@60) | 110V vs 220V |
| Framebuffer | Buffer de píxeles que el CRTC escanea | La pantalla misma |
| Dumb buffer | Buffer mapeable por userspace | Los pixels en RAM del CPU |
El grafo de relación es:
Connector A ──→ Encoder X ──→ [CRTC 0, CRTC 2]
Connector B ──→ Encoder Y ──→ [CRTC 1]
Connector C ──→ Encoder Z ──→ [CRTC 0, CRTC 1, CRTC 2]
El flujo mínimo es:
- Encontrar un connector conectado.
- Elegir un modo (resolución) del connector.
- Asociar ese modo a un CRTC compatible.
- Crear un dumb buffer del tamaño del modo.
- Crear un framebuffer a partir del dumb buffer.
- Decirle al CRTC: "mostrá este framebuffer con este modo".
- Dibujar píxeles en el buffer mapeado → page flip → repetir.
1.3 Las bibliotecas que vamos a usar
Nada de X11, nada de Wayland, nada de SDL. Solo estas:
| Biblioteca | Qué hace | Necesaria para |
|---|---|---|
| libdrm | Wrappers C de los ioctls de DRM/KMS | Parts 1–11 (todo lo básico) |
| libgbm | Generic Buffer Manager de Mesa | Parts 12–14 (OpenGL ES, NanoVG, Vulkan) |
| EGL | Ponte entre OpenGL ES y el sistema de ventanas (o la falta de uno) | Parts 12–13 (OpenGL ES + NanoVG) |
| nanoVG | API 2D vectorial sobre OpenGL ES | Part 13 (shapes, gradients, texto TTF) |
| Vulkan Loader | Carga los entrypoints de Vulkan en runtime | Part 14 (Vulkan surfaceless) |
libdrm vs ioctls crudos: Podrías hacer
open("/dev/dri/card0")y luegoioctl(fd, DRM_IOCTL_..., &args)manualmente. Pero libdrm ya hace todo eso con funciones comodrmModeGetResources(),drmModeSetCrtc(), etc. Más legible, menos boilerplate, mismo resultado.
Flags de compilación
# Para libdrm
gcc prog.c -ldrm -o prog
# Para OpenGL ES + EGL sobre DRM (Part 12)
gcc prog_egl.c -lGLESv2 -lEGL -ldrm -lgbm -o prog_egl
# Con NanoVG (Part 13) — necesitás nanovg.h, nanovg.c y nanovg_gl.h
# Cloná https://github.com/memononen/nanovg y copiá nanovg.h, src/nanovg.c, src/nanovg_gl.h
gcc prog_nvg.c nanovg.c src/nanovg_gl.c -lGLESv2 -lEGL -ldrm -lgbm -lm -o prog_nvg
# Para Vulkan (Part 14)
gcc prog_vulkan.c -lvulkan -ldrm -lgbm -o prog_vulkan
Requisito de sistema: Este capítulo solo se puede desarrollar y ejecutar en Linux con un driver DRM (Intel, AMD, Nouveau). En Windows o macOS no existe
/dev/dri/cardN. Si estás en WSL2, la GPU no es accesible directamente desde el subsistema Linux. Necesitás una máquina real, VM con GPU passthrough, o un sistema remoto con KMS habilitado.
Práctica de la Parte 1
Ejercicio 25.0 — Compilación del proyecto DRM
Conceptos · flags de compilación para libdrm (-ldrm), path de dispositivos /dev/dri/
Contexto · Antes de escribir código, necesitás que tu entorno tenga las bibliotecas necesarias y sepas dónde está el dispositivo DRM de tu GPU.
Consigna · En tu sistema de desarrollo (Linux con Mesa/intel/nouveau/amdgpu):
- Verificá qué dispositivos DRM existen:
ls -la /dev/dri/. - Instalá las development packages necesarias (
libdrm-dev,libgbm-dev,libegl-dev,vulkan-loader-dev). - Compilá un programa mínimo con:
gcc prog.c -ldrmy verificá que no da error de link.
Criterios de aceptación
- Existe al menos un dispositivo
/dev/dri/card*. - Las development packages están instalados (headers de libdrm).
- Compila con
gcc prog.c -ldrmsin errores ni warnings.
PARTE 2 — Abrir /dev/dri/cardN
El primer paso es abrir el dispositivo DRM de la GPU que querés usar. Es un file descriptor normal, como abrir cualquier archivo:
#include <fcntl.h>
#include <unistd.h>
#include <drm.h>
int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("No se pudo abrir /dev/dri/card0");
return 1;
}
// Ahora fd es tu conexión con la GPU. Todo ioctl va por acá.
2.1 ¿Por qué /dev/dri/card0?
Linux expone cada tarjeta gráfica como un dispositivo de character en /dev/dri/:
| Dispositivo | Qué es | Usar para |
|---|---|---|
/dev/dri/card0 |
Primera GPU del sistema | Rendering + display (KMS) |
/dev/dri/card1 |
Segunda GPU (si hay) | Lo mismo |
/dev/dri/renderD128 |
Node de rendering (no tiene KMS) | Solo Vulkan/OpenGL, no configurar pantalla |
Para KMS necesitás el cardN (master node), que requiere privilegios para modeset. En la práctica:
- Si estás como root o con
CAP_SYS_ADMIN, abrís directamente/dev/dri/card0. - Si tenés permisos de grupo
videoo si el display server ya te dio acceso, también funciona. - Para rendering sin KMS (solo OpenGL/Vulkan), usá
renderD128.
2.2 Verificar capacidades con DRM_GET_CAP
No todos los drivers soportan todo. Podés consultar características del driver con drmGetCap:
#include <drm.h>
// ¿Soporta render node?
uint64_t has_renderer = 0;
drmGetCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, &has_renderer);
printf("Universal planes: %s\n", has_renderer ? "sí" : "no");
// ¿Soporte atómico?
drmGetCap(fd, DRM_CLIENT_CAP_ATOMIC, &has_renderer);
printf("Atomic KMS: %s\n", has_renderer ? "sí" : "no");
2.3 Activar el modo atómico
Antes de usar la API atómica (Parte 10), hay que decírselo al kernel con drmSetClientCap():
// Activar modo atómico — hacerlo UNA vez después de abrir el fd
uint64_t atomic = 1;
int ret = drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, atomic);
if (ret) {
fprintf(stderr, "Atomic no soportado: %s\n", strerror(-ret));
}
DRM_CLIENT_CAP_ATOMIC: Es una llamada única por fd. Si la hacés ANTES de hacer modeset, el kernel prepara los structs atómicos para todos los ioctls subsecuentes. No tiene efecto si la llamás después de haber hecho un modeset legacy.
Práctica de la Parte 2
Ejercicio 25.1 — Conectar con la GPU
Conceptos · open() en /dev/dri/card0, O_RDWR, verificación de error, drmGetCap
Contexto · Escribí un programa que se conecte a tu GPU y consulte sus capacidades: si soporta universal planes, si soporta modo atómico, y cuántos buffers puede tener el scheduler.
Consigna · El programa debe:
- Abrir
/dev/dri/card0conO_RDWR | O_CLOEXEC. - Si falla, intentar con
/dev/dri/card1, etc., hasta encontrar uno disponible. - Consultar y mostrar: universal planes (
DRM_CLIENT_CAP_UNIVERSAL_PLANES) y atomic KMS. - Activar atomic KMS con
drmSetClientCap()si aún no está activado.
Entrada · Ninguna (lee los dispositivos disponibles automáticamente).
Salida · Alguna salida similar a:
GPU card0 opened successfully
Universal planes: sí
Atomic KMS: activado
Identificadores sugeridos gpuFd, cardIndex, hasUniversalPlanes, hasAtomic
Criterios de aceptación
- Abre
/dev/dri/cardNcorrectamente (proba con card0, card1, ...). - Muestra las capacidades consultadas con
drmGetCap. - Activa atomic KMS con
drmSetClientCapsi no está activado. - Maneja el error de apertura (imprime
perror). - Cierra el fd con
close(fd)al terminar.
PARTE 3 — Enumerar recursos del sistema
Una vez que tenés el fd, necesitás saber qué hay conectado: cuántos monitores, cuántas resoluciones, cuántos controladores de pantalla (CRTC). Todo eso se obtiene con una llamada:
3.1 drmModeGetResources — El inventario completo
#include <drm_mode.h>
drmModeRes *resources = drmModeGetResources(fd);
if (!resources) {
perror("drmModeGetResources falló");
return 1;
}
// resources->countConnectors → cuántos conectores hay
// resources->connector_id_ptr → array de IDs de conector (o NULL)
// resources->countCrtcs → cuántos CRTC hay
// resources->crtc_id_ptr → array de IDs de CRTC (o NULL)
// resources->countEncoders → cuántos encoders hay
// resources->encoder_id_ptr → array de IDs de encoder (or NULL)
printf("Connectors: %d\n", resources->countConnectors);
printf("CRTCs: %d\n", resources->countCrtcs);
printf("Encoders: %d\n", resources->countEncoders);
// Liberar cuando terminás
drmModeFreeResources(resources);
drmModeGetResourcesdevuelve un bloque de memoria con arrays internos. No son punteros sueltos: el kernel llena una estructura y todos los sub-arrays (connectors, crtcs, encoders) apuntan a memoria dentro del mismo bloque. Por esodrmModeFree(resources)los libera TODOS juntos.
3.2 El grafo de conexión: connector → encoder → crtc
No todos los conectores se pueden conectar a todos los CRTCs. Cada conector tiene un encoder asociado, y cada encoder puede conectarse a un subconjunto de CRTCs. La relación es:
Connector A → Encoder X → [CRTC 0, CRTC 2]
Connector B → Encoder Y → [CRTC 1]
Connector C → Encoder Z → [CRTC 0, CRTC 1, CRTC 2]
Esto significa: no podés arbitrariamente asignar cualquier monitor a cualquier CRTC. Necesitás respetar las compatibilidades del hardware. Podés consultar qué CRTCs puede usar un encoder así:
// Obtener info de un encoder específico
drmModeEncoder *enc = drmModeGetEncoder(fd, connector->encoder_id);
printf("CRTC posible: %x\n", enc->possible_crtcs); // bitmask
// Verificar si el CRTC N es compatible:
if (enc->possible_crtcs & (1 << crtcIndex)) {
printf("CRTC %d es compatible\n", crtcIndex);
}
drmModeFreeEncoder(enc);
3.3 Mapear conectores a CRTCs compatibles
Para cada connector conectado, necesitás encontrar un CRTC libre y compatible:
// Para cada conector...
for (int i = 0; i < resources->countConnectors; i++) {
uint32_t conn_id = resources->connector_id_ptr[i];
// Obtener info completa del connector (incluye sus encoders)
drmModeConnector *conn = drmModeGetConnector(fd, conn_id);
if (conn->connection == DRM_MODE_CONNECTED) {
printf("Conector %d conectado\n", conn_id);
// Para cada encoder asociado...
for (int e = 0; e < conn->count_encoders; e++) {
drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoders[e]);
// Para cada CRTC posible en este encoder...
for (int c = 0; c < resources->countCrtcs; c++) {
if (enc->possible_crtcs & (1 << c)) {
printf(" → CRTC %d compatible\n", resources->crtc_id_ptr[c]);
}
}
drmModeFreeEncoder(enc);
}
}
drmModeFreeConnector(conn);
}
DRM_MODE_CONNECTED: Es el estado del connector. Puede ser:
DRM_MODE_CONNECTED— hay algo enchufado.DRM_MODE_DISCONNECTED— nada conectado.DRM_MODE_UNKNOWNCONNECTION— el kernel no puede determinar.
Práctica de la Parte 3
Ejercicio 25.2 — Inventario del sistema gráfico
Conceptos · drmModeGetResources, arrays de IDs, iteración sobre conectores/CRTC/encoders
Contexto · Escribí un programa que haga un inventario completo del subsistema DRM/KMS: qué monitores están conectados, qué CRTCs hay disponibles y qué compatibilidades existen entre ellos.
Consigna · El programa debe:
- Abrir
/dev/dri/card0y obtener recursos condrmModeGetResources. - Iterar todos los conectores e imprimir su tipo (HDMI, DP, VGA...) y estado (conectado/desconectado).
- Para cada connector conectado, mostrar sus encoders asociados y qué CRTCs son compatibles.
- Imprimir un resumen como:
HDMI-A-1 → conectado → Encoder 12 → CRTC [0, 2] DP-1 → desconectado
Entrada · Ninguna (detecta automáticamente).
Salida · Listado de conectores, estados y compatibilidades de CRTC.
Identificadores sugeridos resources, connectors, crtcIds, encoderIds, compatibleCrtcs
Criterios de aceptación
- Usa
drmModeGetResourcescorrectamente (condrmModeFreeResources). - Itera todos los conectores e imprime tipo + estado.
- Para cada conector conectado, muestra encoders y CRTCs compatibles.
- Limpia toda la memoria liberada con
drmModeFreeConnector/drmModeFreeEncoder.
PARTE 4 — Conectores y estados
Cada connector representa un puerto físico en tu GPU: HDMI, DisplayPort, VGA, eDP (laptop). DRM/KMS te da información detallada sobre cada uno.
4.1 Consultar un connector
drmModeConnector *conn = drmModeGetConnector(fd, connector_id);
if (!conn) {
perror("drmModeGetConnector falló");
return 1;
}
printf("Tipo: %d\n", conn->connector_type);
printf("Subtipo: %d\n", conn->connector_type_id);
printf("Estado: %d\n", conn->connection); // DRM_MODE_CONNECTED o DRMMODE_DISCONNECTED
printf(" mmWidth: %d\n", conn->mm_width); // ancho en mm (para DPI)
printf(" mmHeight: %d\n", conn->mm_height); // alto en mm
// Tipo de conector (DRM_MODE_CONNECTOR_*):
switch (conn->connector_type) {
case DRM_MODE_CONNECTOR_VGA: printf("VGA\n"); break;
case DRM_MODE_CONNECTOR_DVII: printf("DVI-I\n"); break;
case DRM_MODE_CONNECTOR_DVID: printf("DVI-D\n"); break;
case DRM_MODE_CONNECTOR_DisplayPort: printf("DisplayPort\n"); break;
case DRM_MODE_CONNECTOR_HDMIA: printf("HDMI-A\n"); break;
case DRM_MODE_CONNECTOR_eDP: printf("eDP (laptop)\n"); break;
case DRM_MODE_CONNECTOR_LVDS: printf("LVDS (laptop LCD)\n"); break;
// ... más tipos
}
drmModeFreeConnector(conn);
4.2 Tipos de conector más comunes
| Constante | Valor | Descripción |
|---|---|---|
DRM_MODE_CONNECTOR_Unknown |
0 | No se pudo identificar |
DRM_MODE_CONNECTOR_VGA |
1 | VGA analógico |
DRM_MODE_CONNECTOR_DVID |
3 | DVI-D digital |
DRM_MODE_CONNECTOR_HDMIA |
11 | HDMI tipo A (estándar) |
DRM_MODE_CONNECTOR_HDMIB |
12 | HDMI tipo B (ancha) |
DRM_MODE_CONNECTOR_DisplayPort |
10 | DisplayPort |
DRM_MODE_CONNECTOR_eDP |
14 | Embedded DP (pantalla de laptop) |
4.3 El estado de conexión
El campo connection puede tener estos valores:
| Estado | Significado |
|---|---|
DRM_MODE_CONNECTED |
Hay algo enchufado y activo |
DRM_MODE_DISCONNECTED |
Nada enchufado |
DRM_MODE_UNKNOWNCONNECTION |
El kernel no sabe (hardware defectuoso o driver limitado) |
Hotplug: Cuando conectás o desconectás un monitor, el kernel envía un evento. Tu programa debe estar escuchando eventos de DRM (
poll()/epoll()en el fd) para reaccionar a cambios en la configuración de monitores. Lo veremos más adelante (Parte 11).
Práctica de la Parte 4
Ejercicio 25.3 — Detector de monitores conectados
Conceptos · drmModeGetConnector, tipos de conector, estado de conexión
Contexto · Escribí un programa que detecte qué monitores están físicamente conectados a tu GPU y muestre información detallada de cada uno: tipo de puerto, resolución preferida (si la hay), dimensiones en milímetros.
Consigna · El programa debe:
- Abrir
/dev/dri/card0y obtener recursos condrmModeGetResources. - Iterar todos los conectores.
- Para cada conector, imprimir: ID, tipo (texto legible), subtipo, estado de conexión y dimensiones físicas.
- Solo mostrar conectores con estado
DRM_MODE_CONNECTED.
Entrada · Ninguna.
Salida · Ejemplo:
[Conector] ID=32 Tipo=HDMI-A Subtipo=0 Estado=conectado 598x336 mm
Identificadores sugeridos resources, connId, connectorType, connectedMonitors
Criterios de aceptación
- Detecta automáticamente qué conectores están conectados.
- Muestra tipo legible (HDMI-A, DisplayPort, etc.).
- Incluye dimensiones físicas en mm del conector.
- Solo imprime conectores
DRM_MODE_CONNECTED. - Limpia memoria correctamente (
drmModeFreeConnector,drmModeFreeResources).
PARTE 5 — Modos de pantalla
Cada conector conectado tiene una lista de modos disponibles: combinaciones de resolución, refresh rate y sincronización. Por ejemplo: 1920×1080@60Hz, 1280×720@60Hz, etc.
5.1 Los modos de un connector
Cuando consultás un connector con drmModeGetConnector, el kernel devuelve todos los modos
que ese monitor soporta (incluidos los EDID):
drmModeConnector *conn = drmModeGetConnector(fd, connector_id);
if (conn->connection != DRM_MODE_CONNECTED) {
// Nada enchufado — no hay modo válido que aplicar.
goto cleanup;
}
printf("Modos disponibles (%d):\n", conn->count_modes);
for (int i = 0; i < conn->count_modes; i++) {
drmModeModeInfo *mode = &conn->modes[i];
printf(" [%s] %dx%d @ %.2f Hz ",
mode->type & DRM_MODE_TYPE_PREFERRED ? "*" : " ",
mode->hdisplay, mode->vdisplay,
mode->clock / 1000.0);
// Flags de sincronización
if (mode->flags & DRM_MODE_FLAG_PHSYNC) printf("+H ");
if (mode->flags & DRM_MODE_FLAG_NHSYNC) printf("-H ");
if (mode->flags & DRM_MODE_FLAG_PVSYNC) printf("+V ");
if (mode->flags & DRM_MODE_FLAG_NVSYNC) printf("-V ");
printf("\n");
}
// El modo preferido es el que el monitor recomienda:
drmModeModeInfo *preferred = NULL;
for (int i = 0; i < conn->count_modes; i++) {
if (conn->modes[i].type & DRM_MODE_TYPE_PREFERRED) {
preferred = &conn->modes[i];
break;
}
}
if (preferred) {
printf("Modo preferido: %s (%dx%d)\n",
preferred->name, preferred->hdisplay, preferred->vdisplay);
}
drmModeFreeConnector(conn);
DRM_MODE_TYPE_PREFERRED: Es el modo que el monitor reporta como óptimo (vía EDID). Es la resolución nativa del panel. Siempre preferí este a menos que haya una razón específica para elegir otro.
5.2 El campo hdisplay × vdisplay
Cada modo tiene estas dimensiones:
| Campo | Descripción | Ejemplo (1920×1080@60) |
|---|---|---|
hdisplay |
Píxeles horizontales visibles | 1920 |
vdisplay |
Píxeles verticales visibles | 1080 |
hsync_start |
Inicio de sync horizontal | 2008 |
hsync_end |
Fin de sync horizontal | 2052 |
htotal |
Total horizontal (incluyendo blanking) | 2200 |
clock |
Pixel clock en kHz | 148500 (= 148.5 MHz) |
name |
Nombre descriptivo (ej: "1920x1080") | "1920x1080" |
5.3 Aplicar un modo a un CRTC (modo legacy)
Una vez que elegiste un conector conectado y un modo, necesitás aplicarlo. El modo legacy es la forma más simple:
// Paso 1: Crear un framebuffer temporal (necesario incluso para inicializar el display).
drmModeFB fb;
if (drmModeAddFB(fd, mode->hdisplay, mode->vdisplay, 24, 32,
mode->hdisplay * 4, handle, &fb.id)) {
perror("drmModeAddFB falló");
}
// Paso 2: Establecer el modo en un CRTC compatible.
uint32_t crtc_id = <elegir_crtc_compatible(conn, resources)>;
if (drmModeSetCrtc(fd, crtc_id, fb.id, 0, 0,
&connector_id, 1, mode)) {
perror("drmModeSetCrtc falló");
}
// Paso 3: Liberar el framebuffer temporal.
drmModeRmFB(fd, fb.id);
drmModeSetCrtc: Es la función "haz todo" del modo legacy:
- Asigna un CRTC a un connector.
- Establece resolución (modo).
- Establece el framebuffer visible.
- Posiciona el fb en el CRTC (x=0, y=0 para pantalla completa).
Elegir CRTC compatible
Para elegir el CRTC correcto, usamos la técnica de la Parte 3:
uint32_t find_compatible_crtc(int fd, uint32_t connector_id,
drmModeRes *resources) {
drmModeConnector *conn = drmModeGetConnector(fd, connector_id);
if (!conn || conn->encoder_id == 0) {
goto err;
}
drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoder_id);
if (!enc) {
goto err;
}
// Buscar el primer CRTC compatible.
for (int i = 0; i < resources->countCrtcs; i++) {
uint32_t crtc_id = resources->crtc_id_ptr[i];
// ¿Este CRTC es compatible con el encoder?
if (!(enc->possible_crtcs & (1 << i))) continue;
// Está disponible — lo usamos.
return crtc_id;
}
err:
if (conn) drmModeFreeConnector(conn);
if (enc) drmModeFreeEncoder(enc);
return resources->crtc_id_ptr[0]; // fallback
}
Práctica de la Parte 5
Ejercicio 25.4 — Listar resoluciones por monitor
Conceptos · drmModeGetConnector, modos (drmModeModeInfo), DRM_MODE_TYPE_PREFERRED
Contexto · Escribí un programa que liste todas las resoluciones disponibles para cada monitor conectado, marcando cuál es la resolución preferida (nativa) del display.
Consigna · El programa debe:
- Abrir
/dev/dri/card0y obtener recursos. - Para cada conector conectado, listar todos sus modos (resoluciones).
- Marcar con
*el modo preferido (DRM_MODE_TYPE_PREFERRED). - Mostrar para cada modo: nombre, resolución, refresh rate estimado, flags de sync.
Entrada · Ninguna.
Salida · Ejemplo:
=== Conector HDMI-A-1 (ID=32) — conectado ===
* 1920x1080 @ 60.0 Hz +H +V
1280x720 @ 60.0 Hz +H +V
1024x768 @ 60.0 Hz +H +V
800x600 @ 60.3 Hz +H +V
Identificadores sugeridos resources, connId, modeCount, preferredMode
Criterios de aceptación
- Solo procesa conectores
DRM_MODE_CONNECTED. - Lista todos los modos con nombre, resolución y refresh rate.
- Marca el modo preferido con
*. - Muestra flags de sincronización (+H/+V o -H/-V).
PARTE 6 — Dumb buffers
Una vez que tenés un monitor configurado (modo aplicado a un CRTC), necesitás mostrar algo en él. El primer paso: pedirle al kernel un buffer de video. En DRM/KMS, esto se llama dumb buffer.
6.1 ¿Qué es un dumb buffer?
Un dumb buffer es un bloque de memoria del kernel que representa píxeles en VRAM (o gart). Se llama "dumb" (tonto) porque el kernel lo administra todo: asignación, deallocation, y mapeo. Vos solo Lees/Escribes los bytes cuando está mapeado.
El flujo es:
- Pedir al kernel un buffer de tamaño w×h×bpp con
DRM_IOCTL_MODE_CREATE_DUMB. - Obtener un handle (uint32_t) que identifica el buffer.
- Mapearlo a memoria CPU con
DRM_IOCTL_MODE_MAP_DUMB+mmap(). - Escribir píxeles directamente en el puntero resultante.
#include <drm.h>
#include <drm_mode.h>
#include <sys/mman.h>
// Estructura para crear un dumb buffer
struct dumb_buffer {
uint32_t handle;
uint32_t pitch; // bytes por fila (puede ser > width*bytes_per_pixel)
uint32_t size; // tamaño total en bytes
void *addr; // puntero mapeado a memoria CPU
};
// Paso 1: Crear el dumb buffer
struct drm_mode_create_dumb create = {0};
create.width = mode->hdisplay;
create.height = mode->vdisplay;
create.bpp = 32; // 32 bits por píxel (RGBA)
int ret = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);
if (ret < 0) {
perror("DRM_IOCTL_MODE_CREATE_DUMB falló");
}
struct dumb_buffer dbuf;
dbuf.handle = create.handle;
dbuf.pitch = create.pitch;
dbuf.size = create.size;
// Paso 2: Mapear el buffer a memoria CPU
struct drm_mode_map_dumb map = {0};
map.handle = dbuf.handle;
ret = ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);
if (ret < 0) {
perror("DRM_IOCTL_MODE_MAP_DUMB falló");
}
// Paso 3: mmap() — esto es un offset físico del driver, NO un offset de archivo
dbuf.addr = mmap(NULL, dbuf.size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, map.offset);
if (dbuf.addr == MAP_FAILED) {
perror("mmap falló");
}
// ¡Ahora podés escribir píxeles!
printf("Dumb buffer: handle=%u pitch=%u size=%u\n",
dbuf.handle, dbuf.pitch, dbuf.size);
pitchvswidth * bytes_per_pixel: El pitch es la cantidad de bytes por fila del buffer. Puede ser mayor que el ancho real por alineación (por ejemplo, cada fila debe empezar en un boundary de 64 bytes). Siempre usápitch, nunca calculesy * width * 4manualmente.
6.2 Escribir píxeles RGB888 en el buffer
Para escribir un pixel en la posición (x, y) con profundidad 32bpp (RGBA):
// Puntero al primer byte del píxel en (x, y)
uint32_t *row = (uint32_t *)((char *)dbuf.addr + y * dbuf.pitch);
uint32_t *pixel = &row[x];
// Escribir color ARGB (little-endian: ABGR en memoria)
*pixel = 0xFF0000FF; // Rojo puro (en formato que tu driver espere)
// Para 24bpp (RGB, sin alpha):
uint8_t *byte_row = (uint8_t *)dbuf.addr + y * dbuf.pitch;
uint8_t *pixel_byte = &byte_row[x * 3];
pixel_byte[0] = 0xFF; // R
pixel_byte[1] = 0x00; // G
pixel_byte[2] = 0x00; // B
Formato de píxel: El formato exacto depende del driver. Los más comunes son:
- XRGB8888 (32bpp, alpha ignorado) — el más universal en Linux con Mesa.
- XBGR8888 (32bpp, canal B y R invertidos).
- RGB565 (16bpp, 2 bytes por píxel). Para saber cuál soporta tu driver, consultá
drmModeGetFB2()o usá GBM buffers (Parte 12).
6.3 Limpieza
Al terminar de usar el dumb buffer, necesitás liberarlo en orden:
// 1. Desmapear la memoria
munmap(dbuf.addr, dbuf.size);
// 2. Destruir el buffer del kernel
struct drm_mode_destroy_dumb destroy = {0};
destroy.handle = dbuf.handle;
ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
Orden importa: primero
munmap(), despuésdestroy. Si destruyés antes de desmapear, el kernel libera la VRAM y cualquier acceso futuro es un use-after-free.
Práctica de la Parte 6
Ejercicio 25.5 — Crear y mapear un dumb buffer
Conceptos · DRM_IOCTL_MODE_CREATE_DUMB, DRM_IOCTL_MODE_MAP_DUMB, mmap(), munmap()
Contexto · Escribí un programa que cree un dumb buffer de 640×480 a 32bpp, lo mapee a memoria CPU e imprima sus propiedades: handle, pitch, size y dirección de memoria mapeada.
Consigna · El programa debe:
- Abrir
/dev/dri/card0y activar atomic KMS condrmSetClientCap. - Crear un dumb buffer de 640×480 a 32bpp con
DRM_IOCTL_MODE_CREATE_DUMB. - Mapearlo con
DRM_IOCTL_MODE_MAP_DUMB+mmap(). - Imprimir las propiedades del buffer.
- Liberar en orden:
munmap()→DRM_IOCTL_MODE_DESTROY_DUMB.
Criterios de aceptación
- Crea un dumb buffer de 640×480 a 32bpp sin errores.
- Mapea el buffer con mmap() correctamente (no devuelve MAP_FAILED).
- Imprime handle, pitch, size y dirección base del mapeo.
- Limpia en orden: munmap → destroy.
- Maneja errores en cada ioctl (imprime
perror).
PARTE 7 — Punto y línea en C puro
Aquí empieza la parte divertida: dibujar directamente en el buffer, sin bibliotecas de gráficos, sin OpenGL. Solo punteros crudos a memoria y matemática entera.
Pero no vamos a complicarnos con círculos ni elipses todavía. Empezamos con lo más simple que se puede dibujar: un punto y una línea recta. Si podés hacer eso, ya sabés el 80% de cómo funciona el dibujo por software.
7.1 El canvas: un array de bytes
Imaginate que el dumb buffer es una cuadrícula infinita de píxeles. Cada píxel tiene 4 bytes (RGBA).
Para escribir en la posición (x, y):
// Puntero base al buffer mapeado
uint8_t *buf = (uint8_t *)dbuf.addr;
// Calcular offset del píxel en (x, y):
int offset = y * dbuf.pitch + x * 4;
// Escribir un color ARGB (little-endian)
buf[offset + 0] = 255; // A (alpha, 255 = opaco)
buf[offset + 1] = 0; // R (rojo)
buf[offset + 2] = 0; // G (verde)
buf[offset + 3] = 0; // B (azul)
// En little-endian, el uint32 que escribís es ABGR en memoria:
uint32_t *pixels = (uint32_t *)dbuf.addr;
pixels[y * (dbuf.pitch / 4) + x] = 0xFF0000FF; // ARGB: rojo en XRGB8888
IMPORTANTE:
pitchestá en bytes, no en píxeles. Si usás un array deuint32_t, necesitás dividirpitchpor 4 para obtener el stride en píxeles.
7.2 Dibujar un solo punto (píxel)
La función más simple que vas a escribir. Recibe la posición y el color, escribe 4 bytes:
// Dibuja un píxel con protección de boundaries
void draw_pixel(uint8_t *buf, uint32_t pitch, int x, int y, uint32_t color) {
// Protección simple: no dibujar fuera del buffer
if (x < 0 || x >= 640 || y < 0 || y >= 480) return;
uint32_t *pixel = (uint32_t *)(buf + y * pitch + x * 4);
*pixel = color;
}
Eso es. Una función de cinco líneas que hace todo lo necesario: protege los límites, calcula el offset y escribe. Nada más.
7.3 Algoritmo de Bresenham: trazado de líneas
Bresenham es el algoritmo clásico para trazar una línea recta entre dos puntos usando solo
sumas, restas y multiplicaciones enteras. Sin float, sin divisiones, sin sqrt().
// Dibuja una línea de (x0,y0) a (x1,y1) en un dumb buffer 32bpp ARGB
void draw_line(uint8_t *buf, uint32_t pitch,
int x0, int y0, int x1, int y1, uint32_t color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
// Dirección en cada eje (+1 o -1)
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy; // Error acumulado
while (1) {
// Escribir pixel en (x0, y0)
uint32_t *pixel = (uint32_t *)(buf + y0 * pitch + x0 * 4);
*pixel = color;
// ¿Llegamos al final?
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
}
Cómo funciona Bresenham: El algoritmo mantiene un "error acumulado" (
err). En cada paso, decide si avanza en X, en Y, o en ambos según si el error supera cierto umbral. El resultado es una línea con píxeles lo más cerca posible de la recta ideal — sin punto flotante. La magia está en que2 * errevita toda división, manteniendo todo en enteros.
7.4 Dibujar un rectángulo sólido
Una vez que tenés puntos y líneas, un rectángulo es trivial: dos bucles anidados que llenan cada fila:
// Dibuja un rectángulo relleno de color sólido
void draw_rect(uint8_t *buf, uint32_t pitch, int x0, int y0, int x1, int y1, uint32_t color) {
for (int y = y0; y <= y1; y++) {
for (int x = x0; x <= x1; x++) {
draw_pixel(buf, pitch, x, y, color);
}
}
}
// Versión optimizada: escribe bytes directo por fila (más rápido)
void draw_rect_fast(uint8_t *buf, uint32_t pitch, int x0, int y0, int x1, int y1, uint32_t color) {
for (int y = y0; y <= y1; y++) {
uint32_t *row = (uint32_t *)(buf + y * pitch + x0 * 4);
for (int x = x0; x <= x1; x++) {
*row++ = color;
}
}
}
Versión optimizada:
draw_rect_fastevita la llamada a funcióndraw_pixelpor cada píxel, escribiendo directamente en el buffer. En un canvas de 1920x1080, esto puede ser 5-10x más rápido.
Práctica de la Parte 7
Ejercicio 25.6 — Punto y línea en pantalla
Conceptos · Bresenham line, punteros a buffer, protección de boundaries
Contexto · Escribí un programa que cree un dumb buffer de 800x600 a 32bpp, dibuje una cruz blanca (dos líneas diagonales) sobre un fondo negro.
Consigna · El programa debe:
- Crear un dumb buffer de 800x600 a 32bpp y mapearlo con
mmap(). - Dibujar una línea de Bresenham de
(0, 0)a(799, 599)en blanco. - Dibujar otra línea de
(799, 0)a(0, 599)en blanco (cruz). - Dibujar un punto rojo en el centro
(400, 300). - Crear framebuffer con
drmModeAddFB, activarlo condrmModeSetCrtcy mostrar. - Esperar 5 segundos para ver el resultado.
- Limpiar: RmFB -> munmap -> destroy dumb buffer.
Entrada · Ninguna (todo automático).
Salida · Pantalla negra con una cruz blanca y un punto rojo en el centro durante 5 segundos.
Identificadores sugeridos resources, dbuf, mode, fbId, crtcId, connId
Criterios de aceptación
- Crea dumb buffer de 800x600 a 32bpp correctamente.
- Dos líneas diagonales se cruzan en el centro sin huecos (Bresenham).
- Punto rojo visible exactamente en el centro del canvas.
- Framebuffer creado y activado (la pantalla muestra la cruz).
- Limpia correctamente: RmFB -> munmap -> destroy.
Restricciones / pistas
- NO usar bibliotecas de gráficos (no SDL, no OpenGL, no Cairo).
- Usar solo
ioctl,mmap, y matemática entera. - Protección de boundaries: verificar que
(x, y)estén dentro del buffer antes de escribir.
PARTE 7.5 — ¿Por qué punteros crudos si la GPU puede hacerlo?
Dibujaste un punto y una línea. Es impresionante — pero también limitado. Con Bresenham podés hacer líneas, rectángulos y cualquier forma que construyas píxel a píxel. Pero hay problemas:
- No tenés curvas suaves: dibujar un círculo con Bresenham ya es más complejo, y una curva Bézier sería una pesadilla de cuentas enteras.
- No tenés gradientes: cada píxel lleva un color fijo. Un degradado radial requiere calcular distancia para cada píxel del canvas — lento desde la CPU.
- No tenés texto: dibujar letras con punteros crudos significa cargar los bits de cada glifo y escribirlos pixel por pixel. Funciona, pero es tedioso.
- La GPU está esperando: dedicaste todas esas líneas a dibujar rectas mientras el shader unit duerme en la GPU, listo para hacer todo eso en paralelo en nanosegundos.
La solución es sencilla: usar OpenGL ES para delegar el dibujo a la GPU. Pero escribir los shaders de rasterización desde cero para cada forma? No. Hay una biblioteca que lo hace por vos: nanoVG.
nanoVG es una API 2D vectorial sobre OpenGL ES (y OpenGL). Te dá funciones como
nvgRect(),nvgCircle(),nvgStrokeColor(),nvgText()— y se encarga de convertir cada llamada en triángulos que la GPU rasteriza con antialiasing, gradientes y transformaciones.
En la Part 12 configuramos OpenGL ES sobre DRM/GBM. En la Part 13 vas a usar nanoVG sobre ese mismo pipeline para dibujar shapes vectoriales, gradientes y texto TTF con muy poco código.
Hasta ahora dibujaste píxeles en un dumb buffer... pero el CRTC no sabe que ese buffer existe. Necesitás crear un framebuffer asociado al handle del dumb buffer, para que el kernel sepa: "este buffer de VRAM es lo que hay que escanear".
8.1 drmModeAddFB — Asociar buffer a framebuffer
Un framebuffer (fb) en DRM es una estructura del kernel que dice "este buffer es una pantalla": tiene un handle, un width, height, pitch y un format (XRGB8888, ARGB8888, etc.).
#include <drm_mode.h>
uint32_t fb_id;
// Crear framebuffer a partir del dumb buffer
int ret = drmModeAddFB(fd, mode->hdisplay, mode->vdisplay, 24, 32,
dbuf.pitch, dbuf.handle, &fb_id);
if (!ret) {
printf("Framebuffer creado: fb_id=%u\n", fb_id);
} else {
perror("drmModeAddFB falló");
}
// Liberar cuando ya no lo necesitás
drmModeRmFB(fd, fb_id);
Parámetros de
drmModeAddFB:
width,height— dimensiones del framebuffer.depth— bits por píxel (24 para 32bpp con padding).bpp— bytes totales por píxel (32 para XRGB8888).pitch— bytes por fila (NO width * bytesPerPixel, sino el pitch real del dumb buffer).handle— handle del dumb buffer.&fb_id— salida: ID único del framebuffer.
8.2 Activar el framebuffer con drmModeSetCrtc
Una vez que tenés el fb_id y un CRTC disponible, le decís al kernel "mostrá este framebuffer":
// Mostrar este framebuffer en el CRTC seleccionado
uint32_t connector_ids[16]; // máximo 16 conectores
int n_connectors = 1;
connector_ids[0] = <tu_connector_id>;
ret = drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0,
connector_ids, n_connectors, mode);
if (ret) {
perror("drmModeSetCrtc falló");
}
drmModeSetCrtces el comando "haz todo" del modo legacy:
- Asigna el framebuffer al CRTC.
- Posiciona el fb en el CRTC (x=0, y=0).
- Aplica el modo de resolución.
- Activa los conectores especificados.
8.3 El flujo completo: dumb buffer → fb → show → draw → flip
El ciclo mínimo es:
// 1. Crear dumb buffer
create_dumb_buffer(fd, width, height, 32, &dbuf);
// 2. Crear framebuffer a partir del handle
drmModeAddFB(fd, width, height, 24, 32, dbuf.pitch, dbuf.handle, &fb_id);
// 3. Activar el framebuffer en el CRTC (esto "enciende" la pantalla)
drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &connector_id, 1, mode);
// 4. Dibujar algo en el buffer mapeado
uint8_t *buf = (uint8_t *)dbuf.addr;
draw_rect(buf, pitch, width, height, color);
// 5. Limpiar y salir
drmModeRmFB(fd, fb_id);
munmap(dbuf.addr, dbuf.size);
destroy_dumb_buffer(fd, &dbuf);
¡Importante! En este punto, la pantalla se "enciende" después del
SetCrtc. Hasta ahí, todo lo que dibujaste en el buffer era invisible porque el CRTC no lo estaba escaneando. Ahora sí aparece. Pero si queres animar, necesitás page flip (Parte 9).
Práctica de la Parte 8
Ejercicio 25.7 — Encender una pantalla con color sólido
Conceptos · drmModeAddFB, drmModeSetCrtc, dumb buffer, framebuffer
Contexto · Escribí un programa que encienda tu monitor principal con un color sólido (azul). Esto demuestra el flujo mínimo: dumb buffer → framebuffer → SetCrtc → llenar pantalla.
Consigna · El programa debe:
- Abrir
/dev/dri/card0, obtener recursos, activar atomic KMS. - Encontrar un conector conectado y su modo preferido.
- Crear un dumb buffer del tamaño del modo a 32bpp.
- Mapearlo a memoria CPU con
mmap(). - Crear un framebuffer con
drmModeAddFB. - Activarlo en el CRTC compatible con
drmModeSetCrtc. - Llenar toda la pantalla con azul (0x0000FF en ARGB).
- Esperar 3 segundos para que veas el color.
- Limpiar: RmFB → munmap → destroy dumb buffer.
Entrada · Ninguna (detecta automáticamente el primer monitor conectado).
Salida · Pantalla azul durante ~3 segundos, luego vuelve a la normalidad.
Identificadores sugeridos resources, connId, crtcId, mode, dbuf, fbId
Criterios de aceptación
- Detecta el primer conector conectado y su modo preferido.
- Crea dumb buffer del tamaño del modo.
- Crea framebuffer con drmModeAddFB sin errores.
- Activa pantalla con drmModeSetCrtc (la pantalla cambia a azul).
- Espera 3 segundos (usando
sleep(3)ousleep). - Limpia correctamente en orden: RmFB → munmap → destroy.
Restricciones / pistas
- NO usar bibliotecas de ventanas (ni X11, ni Wayland, ni SDL).
- Si tu entorno usa un display manager que toma el control del DRM, podría bloquear SetCrtc.
Ejecutá con
sudoo en una TTY sin servidor gráfico (Ctrl+Alt+F3).
PARTE 9 — Page flip + vblank
En la Parte 8 viste cómo encender la pantalla con un color sólido. Pero si dibujás directamente en el buffer visible, vas a ver tearing (la mitad superior de una imagen y la mitad inferior de otra al mismo tiempo). Para evitarlo, necesitás page flip sincronizado con vblank.
9.1 ¿Qué es tearing?
Cuando el CRTC escanea píxeles del buffer y vos lo modificás al mismo tiempo:
Escaneo (CRTC) → ██████████░░░░░░░░ ← leyendo mitad superior
Dibujo → ████████ ← escribiendo mitad inferior
El resultado en pantalla es una imagen partida con dos fases distintas. Eso es tearing.
9.2 Double buffering: front buffer + back buffer
La solución estándar es doble buffering: tenés DOS buffers, uno visible (front) y otro donde dibujás (back):
Fase 1: Dibujar en back buffer
Front (visible): [████████] ← CRTC lo escanea
Back (escondido):[░░░░░░░░] ← vos dibujás aquí
Fase 2: Page flip al próximo vblank
Front (visible): [░░░░░░░░] ← se volcó el back
Back (escondido):[████████] ← listo para siguiente frame
El page flip es un ioctl que le dice al kernel "al próximo vblank, mostrá el otro buffer". Vblank = vertical blanking = el momento en que el CRTC termina de escanear una frame completa y está reiniciando desde arriba. Es el ÚNICO momento seguro para cambiar qué buffer se muestra.
9.3 Page flip con drmModePageFlip
#include <drm.h>
#include <drm_mode.h>
// Parámetros opcionales de page flip (pueden ser NULL)
struct drm_mode_crtc_page_flip flip = {0};
flip.crtc_id = crtc_id;
flip.fb_id = new_fb_id; // el buffer a mostrar al próximo vblank
flip.flags = DRM_MODE_PAGE_FLIP_EVENT; // quiere notificación cuando termine
flip.user_data = your_callback_ptr; // datos pasados al handler
// Enviar page flip (no bloquea)
int ret = ioctl(fd, DRM_IOCTL_MODE_PAGE_FLIP, &flip);
if (ret < 0) {
perror("DRM_IOCTL_MODE_PAGE_FLIP falló");
}
DRM_MODE_PAGE_FLIP_EVENT: Si usás esta flag, el kernel envía un eventoDRM_EVENT_FLIP_COMPLETEcuando el page flip se completa. Así sabés cuándo podés empezar a dibujar en el back buffer sin riesgo de que el CRTC lo esté escaneando.
9.4 Escuchar eventos con poll() + handler
El fd del DRM es un file descriptor normal. Podés hacer poll() o epoll() sobre él:
// Preparar poll para esperar eventos del DRM
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN | POLLOUT; // queremos leer eventos
while (1) {
int ret = poll(&pfd, 1, 16); // timeout: 16ms (~60 fps max)
if (ret < 0) {
perror("poll falló");
continue;
}
if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) {
fprintf(stderr, "Error en fd del DRM\n");
break;
}
// Procesar eventos DRM
drmHandleEvent evctx = {0};
evctx.version = DRM_EVENT_CONTEXT_VERSION;
evctx.page_flip_handler = page_flip_handler; // tu callback
evctx.sequence_handler = vblank_handler; // opcional
drmHandleEvent(&evctx);
}
// Callback de page flip: se llama cuando el flip terminó
void page_flip_handler(int fd, unsigned int frame,
unsigned int sec, unsigned int usec, void *data) {
printf("Page flip completado en frame %u\n", frame);
// ¡Acá podés empezar a dibujar en el back buffer!
uint8_t *buf = (uint8_t *)flip_data->back_buffer_addr;
draw_next_frame(buf, flip_data);
}
drmHandleEvent: Es la función que interpreta los eventos crudos del fd DRM. Traduce events comoPAGE_FLIP,VBLANK,SEQ_UPDATE, etc. en llamadas a tus callbacks.
9.5 El ciclo de animación completo
El loop principal para una aplicación gráfica con KMS se ve así:
// Setup inicial (Parte 8): dumb buffer → fb → SetCrtc
setup_display(fd, resources);
// Inicializar double buffering
uint32_t fbs[2] = {fb0_id, fb1_id};
void *bufs[2] = {addr0, addr1}; // ambos mapeados con mmap
int front = 0; // cuál buffer es el visible ahora
while (running) {
// 1. Esperar poll() → page_flip_handler
// (se ejecuta en el handler, no acá)
// 2. Dibujar en el back buffer (el que NO está en front)
int back = 1 - front;
draw_frame(bufs[back], pitch, width, height);
// 3. Enviar page flip al próximo vblank
struct drm_mode_crtc_page_flip flip = {0};
flip.crtc_id = crtc_id;
flip.fb_id = fbs[back];
flip.flags = DRM_MODE_PAGE_FLIP_EVENT;
flip.user_data = &front;
ioctl(fd, DRM_IOCTL_MODE_PAGE_FLIP, &flip);
// 4. front se vuelve el back para la próxima iteración
front = back;
}
Importante: el
page_flip_handlerse ejecuta desde dentro dedrmHandleEvent(), que a su vez se llama desde tu loop principal (dentro depoll()). No es un thread separado. La ejecución del callback es síncrona respecto aldrmHandleEvent().
Práctica de la Parte 9
Ejercicio 25.8 — Animación de rebote con page flip
Conceptos · Page flip, vblank events, poll(), double buffering
Contexto Escribí un programa que muestre una pelota blanca rebotando sobre un fondo azul. La pelota se mueve diagonalmente y rebotá en los bordes de la pantalla. Usá page flip sincronizado con vblank para evitar tearing.
Consigna · El programa debe:
- Configurar el display (dumb buffer → fb → SetCrtc) como en el ejercicio 25.7.
- Crear doble buffering: dos framebuffers, dos dumb buffers mapeados.
- Dibujar una pelota blanca (círculo con radio ~30px) centrada en la pantalla.
- Mover la pelota diagonalmente (~2 píxeles por frame).
- Detectar colisiones con los bordes y rebotar (invertir dirección).
- Usar page flip con
DRM_MODE_PAGE_FLIP_EVENTal final de cada frame. - Ejecutar el loop hasta que se presione Ctrl+C.
Entrada · Ctrl+C para salir.
Salida · Pelota blanca rebotando suavemente sobre fondo azul, sin tearing.
Identificadores sugeridos fbs, buffers, frontBuf, backBuf, ballX, ballY, velX, velY
Criterios de aceptación
- Doble buffering con 2 dumb buffers + 2 framebuffers.
- Page flip al vblank (no hay tearing visible).
- Pelota se mueve diagonalmente y rebota en los 4 bordes.
- Usa
poll()+drmHandleEvent()para esperar page flip completion. - Limpia todos los resources al salir.
PARTE 10 — KMS atómico
El modo legacy (drmModeSetCrtc) funciona, pero tiene limitaciones: no podés configurar
múltiples CRTCs simultáneamente, ni hacer changes atómicos (todos o nada). La API atómica
resuelve eso.
10.1 ¿Por qué KMS atómico?
Con el modo legacy, cada llamada a SetCrtc reemplaza la configuración anterior. Si intentás
configurar dos monitores con llamadas separadas, uno puede "robarte" el CRTC del otro.
La API atómica te permite configurar todos los recursos en una sola operación atómica:
o todo cambia, o nada cambia. Es como un commit de base de datos para tu configuración gráfica.
10.2 Estructura básica de la API atómica
#include <drm_fourcc.h>
#include <libdrm/drm_mode_atomic.h>
// 1. Crear un request atómico
uint32_t req_id;
int ret = drmIoctl(fd, DRM_IOCTL_MODE_ATOMIC_ALLOC, &req_id);
if (ret) { perror("Atomic alloc failed"); }
// 2. Obtener o crear un "state object" para cada recurso
drmModePropertyBlobProps *blob_props;
uint32_t modeset_blob_id;
drmModeGetPropertyBlob(fd, existing_blob_id, &blob_props, &modeset_blob_id);
// Para crear un blob nuevo (ej: configuración de CRTC):
struct {
uint32_t crtc_id;
uint32_t fb_id;
uint32_t mode_blob_id;
uint32_t connectors_ptr; // pointer a array de connector IDs
uint32_t count_connectors;
} modeset_state = {0};
// ... llenar modeset_state ...
uint32_t blob_len = sizeof(modeset_state);
void *blob_data = &modeset_state;
uint32_t new_blob_id;
drmModeCreatePropertyBlob(fd, blob_data, blob_len, &new_blob_id);
// 3. Agregar properties al request atómico
drmModeAtomicAddReqProp(req_id, crtc_id, PROP_MODE_ID, mode_blob_id);
drmModeAtomicAddReqProp(req_id, crtc_id, PROP_FB_ID, fb_id);
drmModeAtomicAddReqProp(req_id, connector_id, PROP_CRTC_ID, crtc_id);
// 4. Enviar el commit atómico
struct drm_mode_atomic atomic = {0};
atomic.req_ptr = (uint64_t)req_id;
atomic.flags = 0; // DRM_MODE_ATOMIC_ALLOW_USERIO
atomic.reserved = 0; // padding obligatorio
ret = ioctl(fd, DRM_IOCTL_MODE_ATOMIC_COMMIT, &atomic);
if (ret) { perror("Atomic commit failed"); }
// 5. Liberar request
drmModeAtomicFree(req_id);
DRM_CLIENT_CAP_ATOMIC: Recordá que necesitás activar el modo atómico condrmSetClientCap(Parte 2.3) ANTES de usar cualquiera de estos ioctls. Si no lo activás, los ioctls atómicos fallarán con-EINVAL.
10.3 Usando libdrm's atomic helpers (recomendado)
Hacer todo esto a mano es verboso. libdrm provee helpers que simplifican el proceso:
#include <drm.h>
#include <libdrm/drm_mode_atomic.h>
// Crear contexto atómico
struct kms_device {
int fd;
struct kms_connector *connectors;
struct kms_crtc *crtcs;
};
struct kms_state {
struct drm_mode_atomic_req *req;
// ... estado de cada CRTC, connector, fb ...
};
// Función para preparar un commit atómico:
int kms_commit(int fd, struct kms_state *state) {
struct drm_mode_atomic atomic = {0};
atomic.req_ptr = (uint64_t)state->req;
atomic.flags = DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_ATOMIC_ALLOW_USERIO;
int ret = ioctl(fd, DRM_IOCTL_MODE_ATOMIC_COMMIT, &atomic);
return ret;
}
NOTA: Las funciones
kms_*anteriores son pseudocódigo de ejemplo. La API real de libdrm para atómico es verbosa pero directa: creás blobs, agregás props al request, hacés commit. No hay una librería "KMS helpers" madura como SDL — esa es la gracia.
10.4 Multi-monitor con KMS atómico
La gran ventaja del atómico es configurar múltiples CRTCs simultáneamente:
// Crear blobs para cada CRTC
struct {
uint32_t crtc_id;
uint32_t fb_id;
uint32_t mode_blob_id;
} state0 = {.crtc_id = crtc_ids[0], .fb_id = fb_ids[0]};
struct {
uint32_t crtc_id;
uint32_t fb_id;
uint32_t mode_blob_id;
} state1 = {.crtc_id = crtc_ids[1], .fb_id = fb_ids[1]};
uint32_t blob0, blob1;
drmModeCreatePropertyBlob(fd, &state0, sizeof(state0), &blob0);
drmModeCreatePropertyBlob(fd, &state1, sizeof(state1), &blob1);
// Agregar propiedades al request atómico
drmModeAtomicAddReqProp(req_id, crtc_ids[0], PROP_MODE_ID, mode_blob_ids[0]);
drmModeAtomicAddReqProp(req_id, crtc_ids[0], PROP_FB_ID, fb_ids[0]);
drmModeAtomicAddReqProp(req_id, connector_ids[0], PROP_CRTC_ID, crtc_ids[0]);
drmModeAtomicAddReqProp(req_id, crtc_ids[1], PROP_MODE_ID, mode_blob_ids[1]);
drmModeAtomicAddReqProp(req_id, crtc_ids[1], PROP_FB_ID, fb_ids[1]);
drmModeAtomicAddReqProp(req_id, connector_ids[1], PROP_CRTC_ID, crtc_ids[1]);
// Commit atómico: ambos CRTCs cambian simultáneamente
atomic.req_ptr = (uint64_t)req_id;
ret = ioctl(fd, DRM_IOCTL_MODE_ATOMIC_COMMIT, &atomic);
Simultaneidad real: Con el modo legacy, configurar un segundo monitor podría "robarle" el CRTC al primero. Con atómico, ambos CRTCs se configuran en el mismo kernel call. Si alguno falla, el commit entero es revertido.
Práctica de la Parte 10
Ejercicio 25.9 — Dual monitor con KMS atómico
Conceptos · API atómica (DRM_IOCTL_MODE_ATOMIC_COMMIT), blobs, properties, multi-CRTC
Contexto · Escribí un programa que configure DOS monitores conectados simultáneamente usando la API atómica de KMS. Cada monitor debe tener su propia resolución y framebuffer.
Consigna · El programa debe:
- Abrir
/dev/dri/card0, activar atomic KMS. - Enumerar recursos y encontrar al menos dos conectores conectados.
- Para cada conector, elegir un CRTC compatible y un modo preferido.
- Crear dumb buffers y framebuffers para cada monitor.
- Usar
drmModeCreatePropertyBlobpara crear state blobs por CRTC. - Agregar todas las properties al request atómico (
DRM_IOCTL_MODE_ATOMIC_ALLOC). - Hacer commit atómico con
DRM_IOCTL_MODE_ATOMIC_COMMIT. - Dibujar un color sólido diferente en cada pantalla (azul + rojo).
Criterios de aceptación
- Detecta y configura DOS monitores conectados simultáneamente.
- Usa blobs de propiedades para cada CRTC.
- Agrega todas las properties al mismo request atómico.
- Commit atómico exitoso (ambas pantallas encendidas).
- Cada monitor muestra su color asignado (azul + rojo).
Restricciones / pistas
- Necesitás dos monitores físicamente conectados y un GPU con suficientes CRTCs.
- Si solo tenés un monitor, podés simular el segundo con un "fake connector" (pero no es ideal).
- En VM sin GPU passthrough, este ejercicio no va a funcionar.
PARTE 11 — Eventos DRM y hotplug
Hasta ahora configuraste pantallas estáticas. Pero en la vida real, los usuarios conectan y desconectan monitores todo el tiempo. DRM/KMS te avisa mediante eventos del kernel.
11.1 El evento de hotplug
Cuando conectás un cable HDMI o DisplayPort, el driver envía un evento DRM_EVENT_HOTPLUG al
fd del DRM. Podés detectarlo con poll():
void hotplug_handler(int fd, unsigned int frame,
unsigned int sec, unsigned int usec, void *data) {
printf("Hotplug event: conectar/desconectar detectado\n");
// Necesitás re-enumerar recursos porque algo cambió
drmModeRes *resources = drmModeGetResources(fd);
if (!resources) return;
// Recorrer conectores y actualizar estados
for (int i = 0; i < resources->countConnectors; i++) {
uint32_t conn_id = resources->connector_id_ptr[i];
drmModeConnector *conn = drmModeGetConnector(fd, conn_id);
if (conn->connection == DRM_MODE_CONNECTED) {
printf("Conector %u conectado\n", conn_id);
// Configurar este connector...
} else {
printf("Conector %u desconectado\n", conn_id);
// Desactivar CRTC de este connector...
}
drmModeFreeConnector(conn);
}
drmModeFreeResources(resources);
}
11.2 Registrar el handler de hotplug
En el loop principal, asociás tu callback al contexto de eventos:
drmHandleEvent evctx = {0};
evctx.version = DRM_EVENT_CONTEXT_VERSION;
evctx.page_flip_handler = page_flip_handler;
evctx.hotplug_handler = hotplug_handler; // ← handler de hotplug
evctx.vblank_handler = vblank_handler; // opcional
while (running) {
struct pollfd pfd = {.fd = fd, .events = POLLIN};
poll(&pfd, 1, 100); // timeout: 100ms
if (pfd.revents & POLLIN) {
drmHandleEvent(&evctx);
}
}
11.3 Tipos de eventos DRM
| Evento | Cuándo se dispara | Handler |
|---|---|---|
DRM_EVENT_VBLANK |
Completó un vblank | vblank_handler |
DRM_EVENT_FLIP_COMPLETE |
Page flip terminado | page_flip_handler |
DRM_EVENT_CRTC_SEQUENCE |
Secuencia de CRTC completada | crtc_sequence_handler |
DRM_EVENT_HOTPLUG |
Conector conectado/desconectado | hotplug_handler |
DRM_EVENT_PAGE_FLIP_ERROR |
Page flip falló | page_flip_handler (con error) |
Práctica de la Parte 11
Ejercicio 25.10 — Monitor de eventos DRM
Conceptos · poll(), drmHandleEvent, hotplug_handler, eventos DRM
Contexto · Escribí un programa que escuche eventos del fd DRM e imprima en consola cuando detecte cambios de estado (conexión/desconexión de monitores, page flips).
Consigna · El programa debe:
- Abrir
/dev/dri/card0, activar atomic KMS, configurar display básico. - Iniciar un loop con
poll()en el fd del DRM. - Registrar handlers para: page_flip, hotplug y vblank (pueden ser stubs).
- Al detectar un evento, imprimir qué tipo de evento fue y cuándo.
- Mantenerse corriendo hasta Ctrl+C.
Criterios de aceptación
- Usa poll() en el fd del DRM con timeout razonable (~100ms).
- Registra hotplug_handler en drmHandleEvent.
- Imprime eventos detectados con timestamp.
- Limpia resources al salir (Ctrl+C).
PARTE 12 — OpenGL ES + EGL sobre DRM
Llegamos al punto donde la GPU hace el trabajo pesado. Hasta ahora dibujaste píxeles desde la CPU (matemática pura en C). Ahora vamos a configurar OpenGL ES con EGL para renderizar directamente en un buffer de DRM, sin windowing system. Esto es el cimiento sobre el que nanoVG va a construir shapes vectoriales en la Part 13.
12.1 ¿Qué es EGL?
EGL es la "ponte" entre OpenGL ES y el sistema de composición de display de tu plataforma: X11, Wayland, Linux Framebuffer, o en nuestro caso... DRM/KMS con GBM.
Sin windowing system, necesitás:
- libgbm para crear buffers que OpenGL ES pueda usar.
- EGL para crear un contexto de OpenGL ES.
- El driver Mesa (o similar) para hacer el bridge entre EGL y DRM.
12.2 Inicialización EGL sobre GBM + DRM
#include <GLES2/gl2.h>
#include <EGL/egl.h>
#include <gbm.h>
#include <drm_fourcc.h>
// Paso 1: Abrir el dispositivo DRM
int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
// Paso 2: Crear un GBM surface (buffer para OpenGL)
struct gbm_device *gbm = gbm_create_device(fd);
if (!gbm) {
perror("gbm_create_device falló");
}
// Paso 3: Crear un buffer GBM que EGL pueda usar
uint32_t width = 1920, height = 1080;
struct gbm_surface *gbm_surf = gbm_surface_create(
gbm,
width, height,
GBM_FORMAT_XRGB8888, // formato de píxel
GBM_BO_USE_RENDERING | GBM_BO_USE_SCANOUT // OpenGL + scanout
);
// Paso 4: Configurar EGL
EGLint attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 1,
EGL_GREEN_SIZE, 1,
EGL_RED_SIZE, 1,
EGL_NONE
};
EGLDisplay egl_dpy = eglGetDisplay((EGLNativeDisplayId)fd);
eglInitialize(egl_dpy, NULL, NULL);
EGLConfig egl_cfg;
EGLint num_configs;
eglChooseConfig(egl_dpy, attribs, &egl_cfg, 1, &num_configs);
// Paso 5: Crear contexto OpenGL ES
EGLint ctx_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE
};
EGLContext egl_ctx = eglCreateContext(egl_dpy, egl_cfg,
EGL_NO_CONTEXT, ctx_attribs);
// Paso 6: Crear surface EGL desde GBM surface
EGLSurface egl_surf = eglCreateWindowSurface(egl_dpy, egl_cfg,
(EGLNativeWindowType)gbm_surf, NULL);
// Paso 7: Hacer el contexto current
eglMakeCurrent(egl_dpy, egl_surf, egl_surf, egl_ctx);
GBM_FORMAT_XRGB8888: Es el formato de píxel estándar para OpenGL ES sobre DRM. Asegura que los shaders reciban los colores en el orden correcto (no invertido como a veces pasa con X11).
12.3 Limpieza del contexto EGL
eglMakeCurrent(egl_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroySurface(egl_dpy, egl_surf);
eglDestroyContext(egl_dpy, egl_ctx);
eglTerminate(egl_dpy);
gbm_surface_destroy(gbm_surf);
gbm_device_destroy(gbm);
close(fd);
Práctica de la Parte 12
Ejercicio 25.11 — OpenGL ES sobre DRM sin windowing system
Conceptos · EGL initialization, GBM surfaces, eglMakeCurrent
Contexto · Escribí un programa que configure OpenGL ES sobre DRM/GBM y presente un frame sólido (color rojo) usando page flip manual. No necesitás shaders — simplemente demostrá que el pipeline GLES → GBM → DRM está funcionando.
Consigna · El programa debe:
- Abrir
/dev/dri/card0y crear un dispositivo GBM. - Configurar EGL con el fd del DRM (
eglGetDisplay+eglInitialize). - Crear una surface GBM de 800x600 a formato XRGB8888.
- Crear contexto OpenGL ES 2.x y hacer current.
- Dibujar un frame sólido rojo con
glClearColor(1,0,0,1)+glClear(GL_COLOR_BUFFER_BIT). - Presentar el buffer al display con
eglSwapBuffers+ page flip DRM manual (lock front BO -> AddFB2 -> PageFlip).
Criterios de aceptación
- EGL initialized correctamente desde fd DRM.
- GBM surface creada y vinculada a EGL.
- Frame sólido rojo visible en pantalla (sin shaders).
- Page flip manual presentado el buffer al display.
Restricciones / pistas
- Compilá con:
gcc prog.c -lGLESv2 -lEGL -ldrm -lgbm. - Si falla eglGetDisplay, intentá con
eglGetDisplay(EGL_DEFAULT_DISPLAY). - Verificá que Mesa está instalada (
mesa-utils,libgl1-mesa-dri).
PARTE 13 — NanoVG: shapes, gradientes y texto
Llegó el momento de lo bueno. Con el pipeline EGL/GBM configurado (Part 12), ahora podés usar nanoVG para dibujar shapes vectoriales, gradientes suaves y texto TTF sin escribir un solo shader. NanoVG se encarga de toda la rasterización por vos.
13.1 ¿Qué es nanoVG?
nanoVG es una biblioteca 2D vectorial escrita en C, creada por Mikko Mononen. Su objetivo es simple: darle a los desarrolladores una API tipo Canvas de HTML5 pero para OpenGL ES y OpenGL.
nvgRect(0, 0, 100, 50)dibuja un rectángulo relleno con el color de fill actual.nvgCircle(50, 50, 30)dibuja un círculo.nvgLinearGradient(...)crea un degradado lineal que podés usar como fill o stroke.nvgText(ctx, x, y, "Hola mundo", NULL)renderiza texto con una fuente TTF cargada.
Por debajo, nanoVG convierte cada llamada en triángulos que la GPU rasteriza con antialiasing, gradientes interpolados y transformaciones de coordenadas. Vos solo llamás a la API y nanoVG hace todo el trabajo pesado.
Estructura de archivos
Para usar nanoVG necesitás estos archivos del repositorio oficial (https://github.com/memononen/nanovg):
nanovg.h ← interfaz principal (incluir)
src/nanovg.c ← implementación central (compilar)
src/nanovg_gl.h ← backend OpenGL/ES (incluir + compilar con flag)
src/nanovg_gl.c ← implementación del backend GL (compilar)
La compilación depende de qué versión de OpenGL querés usar. Para GLES2 sobre DRM:
// nanovg_gl.c necesita esta definición ANTES de incluir nanovg_gl.h
#define NANOVG_GLES2_IMPLEMENTATION
#include "src/nanovg_gl.h"
Flags de compilación
# nanoVG + OpenGL ES 2 sobre DRM/GBM
gcc main.c nanovg.c src/nanovg_gl.c \
-lGLESv2 -lEGL -ldrm -lgbm -lm \
-o nvg_demo
13.2 Crear un contexto nanoVG
El primer paso es crear un contexto GL y luego uno nanoVG:
#include <nanovg.h>
#define NANOVG_GLES2_IMPLEMENTATION
#include <nanovg_gl.h>
// ... después de configurar EGL + GLES (Parte 12) ...
// Crear contexto nanoVG con antialiasing habilitado
NVGcontext *nvg = nvgCreateGLES2(NVG_ANTIALIAS | NVG_STENCIL_STROKES);
if (!nvg) {
fprintf(stderr, "No se pudo crear contexto nanoVG\n");
}
// ¡Listo! Ahora podés dibujar.
NVG_STENCIL_STROKES: Esta flag hace que los contornos se dibujen usando el buffer de stencil, lo que evita artefactos cuando las líneas se superponen o hacen giros muy pronunciados. Vale la pena activarlo si vas a dibujar muchas formas con stroke.
13.3 Dibujar shapes básicos
Cada frame, nanoVG sigue este patrón: nvgBeginFrame -> dibujá cosas -> nvgEndFrame:
// Configurar tamaño de viewport (debe coincidir con el GBM surface)
float w = 800.0f;
float h = 600.0f;
nvgBeginFrame(nvg, w, h, 1.0f); // devicePixelRatio = 1 para pantallas no-retina
// Fondo negro
nvgFillColor(nvg, nvgRGB(20, 20, 30));
nvgRect(nvg, 0, 0, w, h);
nvgFill(nvg);
// Rectángulo redondeado blanco con stroke azul
nvgStrokeColor(nvg, nvgRGB(50, 120, 255));
nvgStrokeWidth(nvg, 2.0f);
nvgFillColor(nvg, nvgRGB(240, 240, 245));
nvgRoundedRect(nvg, 50, 50, 300, 200, 10);
nvgFill(nvg);
nvgStroke(nvg);
// Círculo rojo centrado en (500, 300), radio 80
nvgFillColor(nvg, nvgRGBA(255, 60, 60, 200));
nvgCircle(nvg, 500, 300, 80);
nvgFill(nvg);
// Arco (sector circular) — desde PI/4 hasta 3*PI/4, radio 100
nvgFillColor(nvg, nvgRGB(255, 200, 0));
nvgBeginPath(nvg);
nvgMoveTo(nvg, 200, 200);
nvgArc(nvg, 200, 200, 100, NVG_PI * 0.25f, NVG_PI * 0.75f, NVG_CCW);
nvgFill(nvg);
// Elipse centrada en (600, 400), rx=80, ry=50
nvgFillColor(nvg, nvgRGB(100, 255, 100));
nvgEllipse(nvg, 600, 400, 80, 50);
nvgFill(nvg);
// Terminar el frame — envía todo a la GPU
nvgEndFrame(nvg);
nvgBeginPath,nvgMoveTo,nvgArc: Para formas complejas, empezás connvgBeginPath(), después dibujás segmentos connvgMoveTo,nvgLineTo,nvgBezierTo,nvgQuadTo,nvgArc. Finalmente llamás anvgFill()onvgStroke(). Esto equivale al<path>de SVG.
13.4 Gradientes con nanoVG
NanoVG soporta tres tipos de gradientes: lineales, radiales y box (rectángulo difuminado):
// Gradiente lineal: de (10,10) a (200,100), rojo -> azul
NVGpaint grad = nvgLinearGradient(nvg, 10, 10, 200, 100,
nvgRGB(255, 0, 0),
nvgRGB(0, 0, 255));
nvgFillPaint(nvg, grad);
nvgRect(nvg, 10, 10, 200, 100);
nvgFill(nvg);
// Gradiente radial: centro (400,300), radio interno 20, externo 80, blanco -> transparente
NVGpaint rad = nvgRadialGradient(nvg, 400, 300, 20, 80,
nvgRGBA(255, 255, 255, 255),
nvgRGBA(255, 255, 255, 0));
nvgFillPaint(nvg, rad);
nvgRect(nvg, 300, 200, 200, 200);
nvgFill(nvg);
// Box gradient (drop shadow / highlight): rectángulo difuminado de 100x60 con radio de esquina 8
NVGpaint box = nvgBoxGradient(nvg, 350, 150, 100, 60, 8, 15,
nvgRGBA(0, 0, 0, 80),
nvgRGBA(0, 0, 0, 0));
nvgFillPaint(nvg, box);
nvgRect(nvg, 350, 150, 100, 60);
nvgFill(nvg);
Box gradient: Es un rectángulo con relleno del color interior que se desvanece suavemente hacia el color exterior. Ideal para sombras de UI (drop shadow), bordes luminosos y efectos de elevación.
(x,y)es la esquina superior-left,w,hlas dimensiones,rel radio de esquina yfla distancia del difuminado.
13.5 Texto TTF con nanoVG
La magia de nanoVG: podés cargar fuentes TrueType y renderizar texto con una llamada:
// Cargar fuente desde un archivo .ttf (una sola vez, al inicio)
int fontRegular = nvgCreateFont(nvg, "roboto", "/path/to/Roboto-Regular.ttf");
if (fontRegular == -1) {
fprintf(stderr, "No se pudo cargar la fuente\n");
}
// ... dentro del loop de dibujo:
// Configurar estilo de texto y dibujar
nvgFontSize(nvg, 28.0f);
nvgFontFace(nvg, "roboto");
nvgFillColor(nvg, nvgRGBA(255, 255, 255, 255));
nvgText(nvg, 60, 100, "Hola Mundo", NULL);
// Texto multilínea con word-wrap (rompe líneas a los 300px de ancho)
nvgFontSize(nvg, 16.0f);
nvgFillColor(nvg, nvgRGB(200, 200, 200));
nvgTextBox(nvg, 60, 140, 300, "Este es un párrafo de texto que se ajusta automáticamente a lo ancho del contenedor. nanoVG hace word-wrap en los espacios.", NULL);
// Medir texto antes de dibujar (útil para centrado)
float bounds[4]; // [xmin, ymin, xmax, ymax]
nvgTextBounds(nvg, 0, 0, "Medida del texto", NULL, bounds);
float textW = bounds[2] - bounds[0];
// Centrar horizontalmente en el canvas (800px ancho)
float cx = (800.0f - textW) / 2.0f;
nvgText(nvg, cx, 300, "Centrado", NULL);
nvgCreateFontcarga el TTF en GPU: La fuente se compila internamente en atlas de texturas al llamarsenvgBeginFrame. Por eso necesitás tener acceso al archivo .ttf mientras nanoVG está activo. Para evitar dependencias de ruta, copiá el TTF junto al ejecutable o usánvgCreateFontMempara cargar desde un blob en memoria.
Cargar fuente desde memoria (sin archivo)
Si embebés la fuente en el binario (útil para distribuir sin dependencias externas):
// font_data[] y font_size son un array de bytes del .ttf compilado en el binary
int fontMem = nvgCreateFontMem(nvg, "memfont", font_data, font_size, 1);
if (fontMem != -1) {
printf("Fuente cargada desde memoria: %d\n", fontMem);
}
13.6 Transformaciones y clipping
nanoVG soporta transformaciones de coordenadas y scissoring para recortar regiones:
// Guardar estado actual
nvgSave(nvg);
// Rotar 45 grados alrededor del centro (200, 200)
nvgTranslate(nvg, 200, 200);
nvgRotate(nvg, NVG_PI / 4.0f);
nvgScale(nvg, 1.5f, 1.0f);
// Dibujar rect transformada
nvgFillColor(nvg, nvgRGB(255, 150, 50));
nvgRect(nvg, -40, -30, 80, 60);
nvgFill(nvg);
// Restaurar estado original (deshace translate+rotate+scale)
nvgRestore(nvg);
// Scissoring: recortar dibujado a una región rectangular
nvgScissor(nvg, 100, 100, 200, 150);
nvgFillColor(nvg, nvgRGB(0, 200, 100));
nvgRect(nvg, 0, 0, 800, 600);
nvgFill(nvg);
// Desactivar scissoring
nvgResetScissor(nvg);
nvgSave()/nvgRestore(): Guardan y restauran el estado completo (transformaciones, colores, fuentes, scissor). Es como una pila de snapshots. Útil para dibujar widgets que necesitan su propio sistema de coordenadas sin afectar al resto del canvas.
13.7 Integración completa: pipeline DRM + EGL + nanoVG
Este es el ciclo completo de un frame con nanoVG sobre DRM/GBM/EGL:
// --- Setup (una sola vez, antes del loop) ---
int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
struct gbm_device *gbm = gbm_create_device(fd);
// ... crear GBM surface, EGL display/context/surface, hacer eglMakeCurrent ...
NVGcontext *nvg = nvgCreateGLES2(NVG_ANTIALIAS | NVG_STENCIL_STROKES);
int fontId = nvgCreateFont(nvg, "roboto", "Roboto-Regular.ttf");
eglSwapInterval(egl_dpy, 1); // vsync
// --- Loop principal ---
while (running) {
// 1. Dibujar frame con nanoVG
float w = 800.0f, h = 600.0f;
nvgBeginFrame(nvg, w, h, 1.0f);
// Fondo
nvgFillColor(nvg, nvgRGB(25, 25, 35));
nvgRect(nvg, 0, 0, w, h);
nvgFill(nvg);
// Shape decorativo con gradiente
NVGpaint bg = nvgLinearGradient(nvg, 50, 50, 750, 200,
nvgRGB(60, 120, 255),
nvgRGB(180, 40, 200));
nvgFillPaint(nvg, bg);
nvgRoundedRect(nvg, 50, 50, 700, 160, 16);
nvgFill(nvg);
// Texto sobre el gradiente
nvgFontSize(nvg, 32.0f);
nvgFontFace(nvg, "roboto");
nvgFillColor(nvg, nvgRGBA(255, 255, 255, 255));
nvgText(nvg, 80, 140, "nanoVG on DRM/KMS", NULL);
// Circulo con drop shadow
nvgFontSize(nvg, 18.0f);
nvgFillColor(nvg, nvgRGB(180, 180, 200));
nvgTextBox(nvg, 80, 230, 640, "OpenGL ES 2.0 + GBM surfaceless", NULL);
nvgEndFrame(nvg);
// 2. Presentar al display (page flip manual desde GBM)
struct gbm_bo *bo = gbm_surface_lock_front_buffer(gbm_surf);
uint32_t bo_width, bo_height;
gbm_bo_get_width(bo, &bo_width);
gbm_bo_get_height(bo, &bo_height);
uint32_t handles[4] = {0};
uint32_t strides[4] = {0};
uint32_t offsets[4] = {0};
handles[0] = gbm_bo_handle_for_plane(bo, 0).u32;
strides[0] = gbm_bo_get_stride_for_plane(bo, 0);
uint32_t fb_id;
drmModeAddFB2(fd, bo_width, bo_height, GBM_FORMAT_XRGB8888,
handles, strides, offsets, &fb_id, 0);
drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL);
gbm_surface_release_buffer(gbm_surf, bo);
// 3. Esperar evento (page flip completion)
struct pollfd pfd = {.fd = fd, .events = POLLIN};
poll(&pfd, 1, 16); // ~60fps max
if (pfd.revents & POLLIN) {
drmHandleEvent(evctx); // page_flip_handler limpia el fb_id flippeado
}
// Cleanup del fb anterior en el handler:
// void page_flip_handler(int fd, unsigned int frame, ...) {
// drmModeRmFB(fd, *fb_id_ptr);
// }
}
// --- Cleanup (al salir) ---
nvgDeleteGLES2(nvg);
eglTerminate(egl_dpy);
gbm_surface_destroy(gbm_surf);
gbm_device_destroy(gbm);
close(fd);
Sincronización con fences: Entre
nvgEndFrame()y el page flip, OpenGL puede estar aún renderizando. Para una app de producción real, necesitás GL fence sync para garantizar que la GPU terminó antes del flip. En este ejemplo simplificado confiamos eneglSwapBuffers+gbm_surface_lock_front_bufferpara hacer la sincronización implícita.
13.8 Transformaciones: matrices y puntos
nanoVG usa una matriz de transformación 2x3 que podés manipular directamente. Las funciones
como nvgTranslate, nvgRotate, nvgScale pre-multiplícan la matriz actual:
// Obtener la matriz actual (6 floats: a, b, c, d, e, f)
float tx[6];
nvgCurrentTransform(nvg, tx);
// Calcular transform inversa (util para hit-testing: screen -> local coords)
float inv[6];
nvgTransformInverse(inv, tx);
// Transformar un punto de espacio local a espacio de pantalla
float sx, sy;
nvgTransformPoint(&sx, &sy, tx, 100.0f, 50.0f);
// Matrices pre-armadas (llenan dst directamente)
float identity[6];
nvgTransformIdentity(identity);
float translated[6];
nvgTransformTranslate(translated, 100.0f, 200.0f);
float scaled[6];
nvgTransformScale(scaled, 2.0f, 2.0f);
// Multiplicar dos matrices: dst = dst * src
float combined[6];
nvgTransformMultiply(combined, translated); // starts as translate
nvgTransformMultiply(combined, scaled); // becomes translate * scale
Pre-multiplicación: Cada transformación se multiplica a la izquierda de la matriz actual. Esto significa que
translate(100,0); rotate(90)no es lo mismo querotate(90); translate(100,0). El orden importa — pensá en la secuencia inversa: el punto se rota primero y luego se traslada.
13.9 Combinación por composición (blend modes)
nanoVG soporta los mismos blend modes que HTML Canvas:
// Superposición normal (default): SOURCE OVER
nvgGlobalCompositeOperation(nvg, NVG_SOURCE_OVER);
// Solo dibujar donde ya hay algo del canvas (SOURCE IN)
nvgGlobalCompositeOperation(nvg, NVG_SOURCE_IN);
// Solo la parte que NO se superpone (SOURCE OUT)
nvgGlobalCompositeOperation(nvg, NVG_SOURCE_OUT);
// Lo que se solapa (ATOP) — útil para recortes con forma
nvgGlobalCompositeOperation(nvg, NVG_ATOP);
// Lo que hay después de dibujar (DESTINATION_OVER) — coloca el nuevo shape DETRAS
nvgGlobalCompositeOperation(nvg, NVG_DESTINATION_OVER);
// Suma de colores (LIGHTER) — para efectos de brillo y glow
nvgGlobalCompositeOperation(nvg, NVG_LIGHTER);
// Combinación personalizada por canal RGB y alpha separados
nvgGlobalCompositeBlendFunc(nvg, NVG_SRC_ALPHA, NVG_ONE_MINUS_SRC_ALPHA,
NVG_ONE, NVG_ONE_MINUS_SRC_ALPHA);
NVG_LIGHTER: Es el blend mode más útil para efectos de partículas, brillos y glow. Cuando dos formas se superponen conLIGHTER, sus valores de canal se suman. Si ambos son rojos puros (255+255), el resultado es un rojo brillante que "satura" en 255 — perfecto para fuego, neón y destellos.
Práctica de la Parte 13
Ejercicio 25.12 — Dashboard UI con nanoVG
Conceptos · nvgBeginFrame, nvgRect, nvgRoundedRect, nvgCircle, nvgArc,
nvgLinearGradient, nvgRadialGradient, nvgTextBox, nvgText, nvgCreateFont,
nvgSave/nvgRestore, scissor
Contexto · Escribí un programa que muestre un mini dashboard en pantalla usando nanoVG sobre DRM/GBM/EGL. El dashboard debe contener:
- Un header con degradado horizontal (azul → morado) y texto de título centrado.
- Dos tarjetas con fondo blanco redondeado y texto descriptivo.
- Un gráfico circular tipo "donut" hecho con arc + composite operation.
- Un footer con información de sistema en texto pequeño.
Consigna · El programa debe:
- Configurar el pipeline DRM → GBM surface → EGL →
nvgCreateGLES2(como ejercicio 25.9). - Cargar una fuente TTF y configurar estilos.
- Dibujar un header con gradiente lineal (800x60 px) y texto "System Monitor".
- Dibujar dos tarjetas redondeadas (16px radius) debajo del header.
- Dibujar un donut chart (anillo) en el costado derecho: fondo gris + sector coloreado con arc.
- Escribir texto de métricas ("CPU: 42%", "RAM: 1.8G / 4.0G") dentro de las tarjetas.
- Dibujar un footer con información y fecha en la parte inferior.
- Presentar el frame con page flip DRM manual.
Criterios de aceptación
- Pipeline EGL/GBM configurado correctamente.
- Header con
nvgLinearGradientvisible (azul → morado). - Dos tarjetas con
nvgRoundedRect, stroke y fill diferenciados. - Donut chart usando
nvgArc+ al menos dos colores diferentes. - Texto legible con
nvgText/nvgTextBox. - Frame presentado exitosamente al display.
Restricciones / pistas
- Compilá con:
gcc prog.c nanovg.c src/nanovg_gl.c -lGLESv2 -lEGL -ldrm -lgbm -lm. - Usá una fuente libre como Roboto (https://fonts.google.com/specimen/Roboto).
- Si no tenés monitor conectado, podés probar con
GBM_BO_USE_RENDERINGsin scanout.
PARTE 14 — Vulkan surfaceless
Vulkan es la sucesora moderna de OpenGL ES: más explícita, más controlada, pero también más verbosa. Con surfaceless (sin surface de windowing system), renderizás directamente a buffers GBM.
14.1 ¿Qué es "surfaceless" en Vulkan?
Normalmente Vulkan necesita un swapchain vinculado a una ventana (X11, Wayland, Win32). En modo surfaceless, el swapchain se crea sobre GBM buffers — no hay ventanas involucradas.
#include <vulkan/vulkan.h>
#include <gbm.h>
VkInstance instance;
VkPhysicalDevice phys_dev;
VkDevice device;
VkQueue graphics_queue;
VkSurfaceKHR surface; // "surfaceless" = sin windowing system
VkSwapchainKHR swapchain;
14.2 Crear la Vulkan Instance
// Habilitar extensiones necesarias para DRM/GBM
const char *instance_exts[] = {
VK_KHR_surface_EXTENSION_NAME,
VK_KHR_display_EXTENSION_NAME, // o VK_KHR_wayland_surface / VK_KHR_xlib_surface
};
VkApplicationInfo app = {0};
app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
app.pApplicationName = "drm-kms-vulkan";
app.apiVersion = VK_MAKE_VERSION(1, 1, 0);
VkInstanceCreateInfo create_info = {0};
create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
create_info.pApplicationInfo = &app;
create_info.enabledExtensionNamecount = 2;
create_info.ppEnabledExtensionNames = instance_exts;
vkCreateInstance(&create_info, NULL, &instance);
Extensiones: Para DRM/KMS puro (sin X11/Wayland), necesitás
VK_KHR_displayo un loader específico comolibvulkan-waylandolibvulkan-radeon. La forma más universal es usar el Vulkan Display Protocol (VK_DISPLAY_POWER_STATE).
14.3 Crear un swapchain surfaceless (GBM-backed)
Este es el paso más complejo. Necesitás crear un swapchain cuyos buffers vengan de GBM:
// Paso A: Crear VkSurfaceKHR desde GBM
// (Usamos la extensión VK_GOI_mesa_platform o similar para "surfaceless")
VkWin32SurfaceCreateInfoKHR win32 = {0}; // dummy, no usamos Win32
win32.hwnd = NULL;
win32.hinstance = NULL;
vkCreateWin32SurfaceKHR(instance, &win32, NULL, &surface);
// Paso B: Configurar swapchain con GBM buffers
uint32_t width = 1920;
uint32_t height = 1080;
uint32_t image_count = 2; // doble buffering
VkSurfaceCapabilitiesKHR caps;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys_dev, surface, &caps);
// Validar que el surface soporte al menos 2 imágenes
if (caps.minImageCount < 2) {
fprintf(stderr, "Surface no soporta doble buffering\n");
}
image_count = caps.minImageCount; // usar el mínimo soportado
VkSwapchainCreateInfoKHR sc_create = {0};
sc_create.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
sc_create.surface = surface;
sc_create.minImageCount = image_count;
sc_create.imageFormat = VK_FORMAT_B8G8R8A8_SRGB; // o A8B8G8R8
sc_create.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR;
sc_create.imageExtent = (VkExtent2D){ width, height };
sc_create.imageArrayLayers = 1;
sc_create.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
sc_create.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
sc_create.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
sc_create.imageSharingMode = SHARING_MODE_EXCLUSIVE;
sc_create.presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; // vsync o no?
sc_create.clipped = VK_TRUE;
VkResult ret = vkCreateSwapchainKHR(device, &sc_create, NULL, &swapchain);
Present mode:
VK_PRESENT_MODE_IMMEDIATE_KHRes "tear-through" (sin vblank). Si querés sincronizar con vblank, necesitásVK_PRESENT_MODE_FIFO_KHR(el modo seguro con page flip).
14.4 Dibujar un frame en Vulkan
Vulkan es muy explícito: necesitás crear una command buffer, registrar comandos de draw, y enviarla a la queue gráfica:
// Paso 1: Obtener imagen del swapchain
uint32_t image_index;
vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
image_available_semaphores[image_index],
NULL, &image_index);
// Paso 2: Esperar signal (semáforo de que la imagen está disponible)
VkWaitForFencesInfo wait_info = {0};
wait_info.sType = VK_STRUCTURE_TYPE_WAIT_FOR_FENCES_INFO;
wait_info.fenceCount = 1;
wait_info.pFences = &render_fences[frame_number];
wait_info.timeout = UINT64_MAX;
vkWaitForFences(device, 1, wait_info.pFences, VK_TRUE, UINT64_MAX);
// Paso 3: Resetear la command buffer
vkResetCommandBuffers(cmd_buffers[image_index], 0, &cmd_buf);
VkCommandBufferBeginInfo begin = {0};
begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd_buf, &begin);
// Paso 4: Render pass — inicio de la zona de draw
VkRenderPassBeginInfo rp_begin = {0};
rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rp_begin.renderPass = render_pass;
rp_begin.framebuffer = framebuffers[image_index];
rp_begin.renderArea = (VkRect2D){ {0, 0}, {width, height} };
rp_begin.clearValueCount = 1;
rp_begin.pClearValues = &clear_value;
vkCmdBeginRenderPass(cmd_buf, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
// Paso 5: Bind pipeline + draw
vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_GRAPHICS, graphics_pipeline);
vkCmdDraw(cmd_buf, 3, 1, 0, 0); // 3 vertices, 1 instancia
// Paso 6: Finalizar render pass y command buffer
vkCmdEndRenderPass(cmd_buf);
vkEndCommandBuffer(cmd_buf);
// Paso 7: Submit a la graphics queue
VkSubmitInfo submit = {0};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.waitSemaphoreCount = 1;
submit.pWaitSemaphores = &image_available_semaphores[image_index];
submit.pWaitDstStageMask = &pipeline_stage;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd_buf;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &render_complete_semaphores[image_index];
vkQueueSubmit(graphics_queue, 1, &submit, render_fences[frame_number]);
// Paso 8: Present
VkPresentInfoKHR present = {0};
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.waitSemaphoreCount = 1;
present.pWaitSemaphores = &render_complete_semaphores[image_index];
present.swapchainCount = 1;
present.pSwapchains = &swapchain;
present.pImageIndices = &image_index;
vkQueuePresentKHR(graphics_queue, &present);
Vulkan es verboso: Notá que hay ~30 llamadas de Vulkan por frame. Cada una con structs específicos, semáforos, fences, pipelines... Es el precio del rendimiento: control total sobre la GPU, pero boilerplate masivo. OpenGL ES te daba 10 líneas para lo mismo.
14.5 Limpieza
vkDeviceWaitIdle(device);
vkDestroySwapchainKHR(device, swapchain, NULL);
vkDestroySurfaceKHR(instance, surface, NULL);
vkDestroyDevice(device, NULL);
vkDestroyInstance(instance, NULL);
close(fd);
Práctica de la Parte 14
Ejercicio 25.13 — Cuadrado rojo con Vulkan surfaceless
Conceptos · Vulkan instance, physical device, swapchain GBM-backed, render pass, command buffers
Contexto Escribí un programa minimalista que dibuje un cuadrado rojo sobre fondo negro usando Vulkan en modo surfaceless. Sin shaders personalizados — usá el pipeline por defecto de rasterización.
Consigna · El programa debe:
- Crear Vulkan instance con extensiones para surface display.
- Seleccionar physical device y crear logical device con queue gráfica.
- Configurar un swapchain GBM-backed (800×600, doble buffering).
- Crear pipeline raster básico + render pass (clear color negro → draw rojo).
- Dibujar un rectángulo cubriendo toda la pantalla con color rojo sólido.
- Submit command buffer → present frame.
Criterios de aceptación
- Vulkan instance creada sin errores.
- Swapchain configurado sobre GBM buffers (sin windowing system).
- Render pass limpia a negro y dibuja un rectángulo rojo.
- Frame presentado exitosamente al display.
Restricciones / pistas
- Compilá con:
gcc prog_vulkan.c -lvulkan -ldrm -lgbm. - Si
VK_KHR_displayno está disponible, usáVK_KHR_surfacecon un surface dummy GBM. - Podés usar glslangValidator (shader compiler de SPIR-V) o construir el pipeline sin shaders.
PARTE 15 — Ejercicio jefe: Dashboard UI animado con nanoVG
Llegamos al final del capítulo. Este ejercicio combina todo lo que aprendiste:
- DRM/KMS básico (Parte 2–5): abrir GPU, enumerar recursos, configurar modos.
- Dumb buffers + framebuffers (Parte 6–8): crear buffers, mapearlos, asociar a CRTCs.
- Page flip + vblank (Parte 9): animación fluida sin tearing.
- KMS atómico (Parte 10): multi-monitor con configuración simultánea.
- Hotplug events (Parte 11): detectar cambios de monitores conectados.
- OpenGL ES/EGL o Vulkan: renderizar primitivas sobre los buffers DRM.
15.1 El desafío completo
Escribí un programa que muestre un dashboard UI animado en TODOS los monitores conectados a tu GPU, usando nanoVG sobre DRM/KMS directo sin windowing system.
Requisitos obligatorios:
-
Multi-monitor automático: Detectá todos los conectores
DRM_MODE_CONNECTEDy configurá cada uno con su modo preferido. Usá KMS atómico para activarlos simultáneamente. -
Dashboard por monitor: Cada monitor debe mostrar un mini dashboard con:
- Header con degradado (usando
nvgLinearGradient) y título centrado. - Tarjetas redondeadas (
nvgRoundedRect) con métricas simuladas ("CPU: 42%", etc.). - Gráfico circular tipo "donut" o "barras" usando shapes de nanoVG.
- Footer con información del sistema en texto pequeño.
- Header con degradado (usando
-
Animación en tiempo real: Los valores de las métricas deben cambiar cada frame (simulación). Usá
nvgTextpara actualizar los textos ynvgRect/nvgCirclepara gráficos animados. -
Doble buffering por CRTC: Cada monitor tiene su propio par front/back buffer. Usá page flip con
DRM_MODE_PAGE_FLIP_EVENTpara sincronizar con vblank. -
Hotplug handling: Si un nuevo monitor se conecta o desconecta durante la ejecución, reconfigurá el layout del dashboard automáticamente (sin crash).
-
Manejo de hotplug: Si un nuevo monitor se conecta o desconecta durante la ejecución:
- Reconectar → configurar automáticamente con su modo preferido.
- Desconectar → quitar de la configuración (sin crash).
-
Sin bibliotecas de windowing: NADA de X11, Wayland, SDL, GLFW, Raylib... Solo libdrm, libgbm, EGL, y nanoVG sobre OpenGL ES.
15.2 Estructura de datos sugerida
#define MAX_MONITORS 8
typedef struct {
uint32_t conn_id;
uint32_t crtc_id;
uint32_t fb_id;
uint32_t fb_handle; // handle del dumb buffer
void *buf_addr; // puntero mmap'ed
uint32_t pitch; // bytes por fila
uint32_t width, height;
drmModeModeInfo mode;
} MonitorConfig;
typedef struct {
int x, y; // posición en píxeles (coordenada global)
int vx, vy; // velocidad en píxeles/frame
float color_hue; // 0.0–1.0 para ciclo de arco-by-arco
int bounce_count; // cuántos bordes tocó
} Ball;
typedef struct {
int fd; // fd del dispositivo DRM
MonitorConfig monitors[MAX_MONITORS];
int num_monitors;
Ball balls[4]; // hasta 4 pelotas simultáneas
int running; // flag para el loop principal
} AppContext;
15.3 Conversión de hue a RGB (para el gradiente arco-by-arco)
Necesitás convertir un valor de "tonalidad" (hue, 0–1) a color RGB usando lerp:
// Convierte hue (0-1) a RGB usando lerp — ciclo completo del espectro
void hsv_to_rgb(float hue, uint8_t *r, uint8_t *g, uint8_t *b) {
int h = (int)(hue * 6.0f);
float f = hue * 6.0f - h;
// 6 sectores del espectro: rojo → amarillo → verde → cian → azul → magenta → rojo
uint8_t sectors[6][3] = {
{255, 255, 0}, // 0: amarillo (rojo + verde)
{ 0, 255, 0}, // 1: verde
{ 0, 255, 255}, // 2: cian
{ 0, 0, 255}, // 3: azul
{255, 0, 255}, // 4: magenta
{255, 255, 0}, // 5: vuelve a amarillo → rojo
};
uint8_t *c1 = sectors[h % 6];
uint8_t *c2 = sectors[(h + 1) % 6];
*r = lerp(c1[0], c2[0], f);
*g = lerp(c1[1], c2[1], f);
*b = lerp(c1[2], c2[2], f);
}
// Convierte hue a color ARGB 32bpp (XRGB8888)
uint32_t get_ball_color(float hue) {
uint8_t r, g, b;
hsv_to_rgb(hue, &r, &g, &b);
// XRGB8888 en little-endian: 0xFFRRGGBB
return ((uint32_t)0xFF << 24) |
((uint32_t)r << 16) |
((uint32_t)g << 8) |
((uint32_t)b);
}
Ciclo de arco: Cada vez que la pelota toca un borde (
bounce_count++), avanzás el hue:ball.color_hue = fmodf(ball.bounce_count / 7.0f, 1.0f). Así el color completa un ciclo después de 7 rebotes (un arco completo del espectro).
15.4 Lógica de movimiento y colisión
void update_ball(AppContext *app, Ball *ball, uint64_t dt_ms) {
// Determinar en qué monitor está la pelota
int monitor_idx = find_monitor_at_pos(app, ball->x, ball->y);
if (monitor_idx < 0) return; // pelota no asignada a ningún monitor
MonitorConfig *mon = &app->monitors[monitor_idx];
// Actualizar posición (velocidad en píxeles/frame)
int steps_per_frame = 2; // velocidad base
ball->x += ball->vx * steps_per_frame;
ball->y += ball->vy * steps_per_frame;
// Colisión con bordes horizontales del monitor actual
if (ball->x < mon->width / BALL_RADIUS) {
ball->x = mon->width / BALL_RADIUS;
ball->vx = abs(ball->vx); // rebotar: invertir dirección horizontal
on_bounce(app, ball); // actualizar color del arco
} else if (ball->x > mon->width - mon->width / BALL_RADIUS) {
ball->x = mon->width - mon->width / BALL_RADIUS;
ball->vx = -abs(ball->vx);
on_bounce(app, ball);
}
// Colisión con bordes verticales del monitor actual
if (ball->y < BALL_RADIUS) {
ball->y = BALL_RADIUS;
ball->vy = abs(ball->vy);
on_bounce(app, ball);
} else if (ball->y > mon->height - BALL_RADIUS) {
ball->y = mon->height - BALL_RADIUS;
ball->vy = -abs(ball->vy);
on_bounce(app, ball);
}
// ¿La pelota cruzó al monitor vecino?
int next_mon = check_cross_monitor(app, ball->x, ball->y);
if (next_mon >= 0 && next_mon != monitor_idx) {
// Transferir la pelota al nuevo monitor
printf("Pelota cruzó a monitor %d\n", next_mon);
}
}
void on_bounce(AppContext *app, Ball *ball) {
ball->bounce_count++;
// Avanzar el ciclo de color: cada 7 rebotes = ciclo completo
ball->color_hue = fmodf((float)(ball->bounce_count % 64) / 64.0f, 1.0f);
}
Coordenadas globales: Usá un sistema de coordenadas global donde
x = 0, y = 0es la esquina superior izquierda del monitor más a la izquierda. Si tenés dos monitores de 1920×1080 lado a lado:
- Monitor 0: x=0–1919, y=0–1079
- Monitor 1: x=1920–3839, y=0–1079 La pelota se mueve en este espacio global. Cada frame, la renderizás separadamente en cada monitor según su offset local.
15.5 Renderizado por monitor
void render_frame(AppContext *app) {
for (int i = 0; i < app->num_monitors; i++) {
MonitorConfig *mon = &app->monitors[i];
// Fondo: color sólido por monitor (azul para el primero, rojo para el segundo...)
fill_rect(mon->buf_addr, mon->pitch, mon->width, mon->height,
get_monitor_bg_color(i));
// Dibujar pelotas visibles en este monitor
for (int b = 0; b < 4; b++) {
Ball *ball = &app->balls[b];
if (is_ball_visible_in_monitor(ball, i)) {
// Calcular posición relativa a este monitor
int rel_x = ball->x - get_monitor_offset_x(app, i);
int rel_y = ball->y;
uint32_t color = get_ball_color(ball->color_hue);
// Dibujar círculo con midpoint
draw_circle_anti_alias(mon->buf_addr, mon->pitch,
rel_x, rel_y, BALL_RADIUS, color);
}
}
// Page flip si hay un framebuffer nuevo listo
// (ver Parte 9 para detalles de page flip)
}
}
15.6 Criterios de aceptación finales
El programa debe cumplir TODOS estos criterios para considerarse completo:
Funcionalidad básica (obligatorio)
- Abre
/dev/dri/card0, enumera recursos, detecta al menos un conector conectado. - Configura cada monitor conectado con su modo preferido (KMS atómico).
- Crea dumb buffers y framebuffers por cada monitor.
- Enciende TODOS los monitores simultáneamente con
drmModeSetCrtco atomic commit.
Animación
- Pelota blanca (o de color) visible en pantalla, moviéndose diagonalmente.
- Rebota suavemente en los bordes de cada monitor individual.
- Si la pelota cruza entre monitores, aparece en el siguiente sin saltos.
- Doble buffering con page flip + vblank (sin tearing).
Gradiente arco-by-arco
- La pelota cambia de color cada vez que toca un borde.
- El ciclo de colores sigue el espectro: rojo → naranja → amarillo → verde → cian → azul → violeta.
- Usa
lerppara interpolación suave entre colores (sin saltos bruscos). - Al completar 7–64 rebotes, el color vuelve a rojo (ciclo completo).
Multi-monitor
- Funciona con 1 monitor (degradado simple) y con 2+ monitores.
- Si un monitor se desconecta, la pelota que estaba en él desaparece sin crash.
- Si un monitor nuevo se conecta, aparece la pelota en el nuevo espacio disponible.
Código limpio
- Estructura de datos clara (
MonitorConfig,Ball,AppContext). - Sin fugas de memoria: todos los buffers y framebuffers liberados al salir.
- Manejo correcto de errores (no crash si un ioctl falla).
- Código organizado en funciones pequeñas y reutilizables.
15.7 Compilación del ejercicio jefe
# Con dumb buffers + Bresenham/midpoint (solo C puro + libdrm)
gcc exercise_25_jefe.c -ldrm -o exercise_25_jefe
# Con OpenGL ES/EGL (si preferís shaders en vez de punteros crudos)
gcc exercise_25_jefe.c -lGLESv2 -lEGL -ldrm -lgbm -lm -o exercise_25_egl
# Con Vulkan surfaceless (para los valientes)
gcc exercise_25_jefe.c -lvulkan -ldrm -lgbm -lm -o exercise_25_vulkan
Ejecución: Corré el programa desde una TTY (Ctrl+Alt+F3), sin servidor gráfico activo. Si X11/Wayland están corriendo, van a "robar" el control de DRM y tu programa va a fallar en
drmModeSetCrtccon-EBUSY.sudo systemctl stop gdm # o sddm, lightdm, etc. Ctrl+Alt+F3 # ir a TTY limpia sudo ./exercise_25_jefe
15.8 Lo que te llevás de este capítulo
Resumen rápido de lo que aprendiste (y las herramientas que ahora tenés):
-
DRM/KMS es el camino directo al hardware gráfico. Abrís
/dev/dri/cardN, hablás con el kernel, y controlás directamente qué resolución se muestra y qué buffer se escanea. Sin intermediarios. -
El flujo mínimo es siempre el mismo: abrir fd → enumerar recursos → configurar CRTC + connector → crear dumb buffer + framebuffer → dibujar → page flip. Todo lo demás es variación de esto.
-
Un punto y una línea son todo lo que necesitás empezar. Bresenham para rectas, protección de boundaries para no escribir fuera del buffer — eso es el 80% del dibujo por software.
-
Page flip + vblank = cero tearing. Cambiás el buffer visible solo cuando el CRTC terminó de escanear la frame anterior. Es la base de toda animación fluida en Linux.
-
KMS atómico es el futuro. Legacy funciona para un monitor, pero multi-monitor real requiere blobs + properties + commit atómico.
-
nanoVG te salva de dibujar píxeles a mano. Con
nvgRect(),nvgCircle(),nvgText()y gradientes, la GPU rasteriza todo con antialiasing. Vos llamás a la API y nanoVG hace los triángulos por vos. -
OpenGL ES sobre DRM sin windowing system es el puente entre tus shapes vectoriales y la pantalla. EGL + GBM = gráficos nativos en Linux.
En el siguiente capítulo, veremos... bueno, depende de qué quieras aprender después. Pero por ahora:
Felicidades. Acabás de escribir gráficos nativos en Linux sin ninguna biblioteca de windowing system — con nanoVG para shapes vectoriales, texto TTF y gradientes. Eso significa que entendés más del hardware gráfico que el 99% de los desarrolladores de videojuegos y apps — porque la mayoría NI SIQUIERA SABE que DRM existe.
¿Qué sigue?
Este capítulo cierra el recorrido por gráficos en Linux sin intermediarios. Los próximos capítulos podrían explorar:
- Archivos de configuración udev: Cómo hacer hotplug automático sin reiniciar el sistema.
- DRM/KMS con V4L2: Captura de video directamente desde la GPU.
- GPU computing con OpenCL: Usar DRM para compartir buffers entre render y compute.
- Wayland compositor mínimo: Ahora que conocés DRM, escribí tu propio compositor en ~300 líneas.
¡Tu elección! El capítulo sobre DRM/KMS está completo — las ideas quedan a partir de ahí.