Files
c-programming/book/25-drm-kms.md
T

99 KiB
Raw Blame History

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:

  1. Encontrar un connector conectado.
  2. Elegir un modo (resolución) del connector.
  3. Asociar ese modo a un CRTC compatible.
  4. Crear un dumb buffer del tamaño del modo.
  5. Crear un framebuffer a partir del dumb buffer.
  6. Decirle al CRTC: "mostrá este framebuffer con este modo".
  7. 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 luego ioctl(fd, DRM_IOCTL_..., &args) manualmente. Pero libdrm ya hace todo eso con funciones como drmModeGetResources(), 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):

  1. Verificá qué dispositivos DRM existen: ls -la /dev/dri/.
  2. Instalá las development packages necesarias (libdrm-dev, libgbm-dev, libegl-dev, vulkan-loader-dev).
  3. Compilá un programa mínimo con: gcc prog.c -ldrm y 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 -ldrm sin 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 video o 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:

  1. Abrir /dev/dri/card0 con O_RDWR | O_CLOEXEC.
  2. Si falla, intentar con /dev/dri/card1, etc., hasta encontrar uno disponible.
  3. Consultar y mostrar: universal planes (DRM_CLIENT_CAP_UNIVERSAL_PLANES) y atomic KMS.
  4. 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/cardN correctamente (proba con card0, card1, ...).
  • Muestra las capacidades consultadas con drmGetCap.
  • Activa atomic KMS con drmSetClientCap si 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);

drmModeGetResources devuelve 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 eso drmModeFree(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:

  1. Abrir /dev/dri/card0 y obtener recursos con drmModeGetResources.
  2. Iterar todos los conectores e imprimir su tipo (HDMI, DP, VGA...) y estado (conectado/desconectado).
  3. Para cada connector conectado, mostrar sus encoders asociados y qué CRTCs son compatibles.
  4. 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 drmModeGetResources correctamente (con drmModeFreeResources).
  • 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:

  1. Abrir /dev/dri/card0 y obtener recursos con drmModeGetResources.
  2. Iterar todos los conectores.
  3. Para cada conector, imprimir: ID, tipo (texto legible), subtipo, estado de conexión y dimensiones físicas.
  4. 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:

  1. Abrir /dev/dri/card0 y obtener recursos.
  2. Para cada conector conectado, listar todos sus modos (resoluciones).
  3. Marcar con * el modo preferido (DRM_MODE_TYPE_PREFERRED).
  4. 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:

  1. Pedir al kernel un buffer de tamaño w×h×bpp con DRM_IOCTL_MODE_CREATE_DUMB.
  2. Obtener un handle (uint32_t) que identifica el buffer.
  3. Mapearlo a memoria CPU con DRM_IOCTL_MODE_MAP_DUMB + mmap().
  4. 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);

pitch vs width * 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 calcules y * width * 4 manualmente.

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és destroy. 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:

  1. Abrir /dev/dri/card0 y activar atomic KMS con drmSetClientCap.
  2. Crear un dumb buffer de 640×480 a 32bpp con DRM_IOCTL_MODE_CREATE_DUMB.
  3. Mapearlo con DRM_IOCTL_MODE_MAP_DUMB + mmap().
  4. Imprimir las propiedades del buffer.
  5. 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: pitch está en bytes, no en píxeles. Si usás un array de uint32_t, necesitás dividir pitch por 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 que 2 * err evita 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_fast evita la llamada a función draw_pixel por 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:

  1. Crear un dumb buffer de 800x600 a 32bpp y mapearlo con mmap().
  2. Dibujar una línea de Bresenham de (0, 0) a (799, 599) en blanco.
  3. Dibujar otra línea de (799, 0) a (0, 599) en blanco (cruz).
  4. Dibujar un punto rojo en el centro (400, 300).
  5. Crear framebuffer con drmModeAddFB, activarlo con drmModeSetCrtc y mostrar.
  6. Esperar 5 segundos para ver el resultado.
  7. 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ó");
}

drmModeSetCrtc es 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:

  1. Abrir /dev/dri/card0, obtener recursos, activar atomic KMS.
  2. Encontrar un conector conectado y su modo preferido.
  3. Crear un dumb buffer del tamaño del modo a 32bpp.
  4. Mapearlo a memoria CPU con mmap().
  5. Crear un framebuffer con drmModeAddFB.
  6. Activarlo en el CRTC compatible con drmModeSetCrtc.
  7. Llenar toda la pantalla con azul (0x0000FF en ARGB).
  8. Esperar 3 segundos para que veas el color.
  9. 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) o usleep).
  • 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 sudo o 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 evento DRM_EVENT_FLIP_COMPLETE cuando 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 como PAGE_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_handler se ejecuta desde dentro de drmHandleEvent(), que a su vez se llama desde tu loop principal (dentro de poll()). No es un thread separado. La ejecución del callback es síncrona respecto al drmHandleEvent().

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:

  1. Configurar el display (dumb buffer → fb → SetCrtc) como en el ejercicio 25.7.
  2. Crear doble buffering: dos framebuffers, dos dumb buffers mapeados.
  3. Dibujar una pelota blanca (círculo con radio ~30px) centrada en la pantalla.
  4. Mover la pelota diagonalmente (~2 píxeles por frame).
  5. Detectar colisiones con los bordes y rebotar (invertir dirección).
  6. Usar page flip con DRM_MODE_PAGE_FLIP_EVENT al final de cada frame.
  7. 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 con drmSetClientCap (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:

  1. Abrir /dev/dri/card0, activar atomic KMS.
  2. Enumerar recursos y encontrar al menos dos conectores conectados.
  3. Para cada conector, elegir un CRTC compatible y un modo preferido.
  4. Crear dumb buffers y framebuffers para cada monitor.
  5. Usar drmModeCreatePropertyBlob para crear state blobs por CRTC.
  6. Agregar todas las properties al request atómico (DRM_IOCTL_MODE_ATOMIC_ALLOC).
  7. Hacer commit atómico con DRM_IOCTL_MODE_ATOMIC_COMMIT.
  8. 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:

  1. Abrir /dev/dri/card0, activar atomic KMS, configurar display básico.
  2. Iniciar un loop con poll() en el fd del DRM.
  3. Registrar handlers para: page_flip, hotplug y vblank (pueden ser stubs).
  4. Al detectar un evento, imprimir qué tipo de evento fue y cuándo.
  5. 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:

  1. Abrir /dev/dri/card0 y crear un dispositivo GBM.
  2. Configurar EGL con el fd del DRM (eglGetDisplay + eglInitialize).
  3. Crear una surface GBM de 800x600 a formato XRGB8888.
  4. Crear contexto OpenGL ES 2.x y hacer current.
  5. Dibujar un frame sólido rojo con glClearColor(1,0,0,1) + glClear(GL_COLOR_BUFFER_BIT).
  6. 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 con nvgBeginPath(), después dibujás segmentos con nvgMoveTo, nvgLineTo, nvgBezierTo, nvgQuadTo, nvgArc. Finalmente llamás a nvgFill() o nvgStroke(). 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,h las dimensiones, r el radio de esquina y f la 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);

nvgCreateFont carga el TTF en GPU: La fuente se compila internamente en atlas de texturas al llamarse nvgBeginFrame. 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á nvgCreateFontMem para 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 en eglSwapBuffers + gbm_surface_lock_front_buffer para 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 que rotate(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 con LIGHTER, 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:

  1. Configurar el pipeline DRM → GBM surface → EGL → nvgCreateGLES2 (como ejercicio 25.9).
  2. Cargar una fuente TTF y configurar estilos.
  3. Dibujar un header con gradiente lineal (800x60 px) y texto "System Monitor".
  4. Dibujar dos tarjetas redondeadas (16px radius) debajo del header.
  5. Dibujar un donut chart (anillo) en el costado derecho: fondo gris + sector coloreado con arc.
  6. Escribir texto de métricas ("CPU: 42%", "RAM: 1.8G / 4.0G") dentro de las tarjetas.
  7. Dibujar un footer con información y fecha en la parte inferior.
  8. Presentar el frame con page flip DRM manual.

Criterios de aceptación

  • Pipeline EGL/GBM configurado correctamente.
  • Header con nvgLinearGradient visible (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_RENDERING sin 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_display o un loader específico como libvulkan-wayland o libvulkan-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_KHR es "tear-through" (sin vblank). Si querés sincronizar con vblank, necesitás VK_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:

  1. Crear Vulkan instance con extensiones para surface display.
  2. Seleccionar physical device y crear logical device con queue gráfica.
  3. Configurar un swapchain GBM-backed (800×600, doble buffering).
  4. Crear pipeline raster básico + render pass (clear color negro → draw rojo).
  5. Dibujar un rectángulo cubriendo toda la pantalla con color rojo sólido.
  6. 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_display no está disponible, usá VK_KHR_surface con 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:

  1. Multi-monitor automático: Detectá todos los conectores DRM_MODE_CONNECTED y configurá cada uno con su modo preferido. Usá KMS atómico para activarlos simultáneamente.

  2. 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.
  3. Animación en tiempo real: Los valores de las métricas deben cambiar cada frame (simulación). Usá nvgText para actualizar los textos y nvgRect/nvgCircle para gráficos animados.

  4. Doble buffering por CRTC: Cada monitor tiene su propio par front/back buffer. Usá page flip con DRM_MODE_PAGE_FLIP_EVENT para sincronizar con vblank.

  5. Hotplug handling: Si un nuevo monitor se conecta o desconecta durante la ejecución, reconfigurá el layout del dashboard automáticamente (sin crash).

  6. 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).
  7. 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 = 0 es 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 drmModeSetCrtc o 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 lerp para 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 drmModeSetCrtc con -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í.