diff --git a/book/25-drm-kms.md b/book/25-drm-kms.md index 5e8bba2..21217b1 100644 --- a/book/25-drm-kms.md +++ b/book/25-drm-kms.md @@ -39,14 +39,15 @@ ejercicios— para que los uses ya mismo. **No las saltee**: son la mitad del ca | **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** | Dibujar primitivas en C puro | Bresenham, midpoint, lerp — matemática gráfica sin bibliotecas de renderizado. | +| **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 | Contexto gráfico sin windowing system, primitivas con shaders sobre GBM buffers. | -| **13** | Vulkan surfaceless | Swapchain surfaceless, pipeline raster, dibujar sin windowing system. | -| **14** | Ejercicio jefe: pelota rebotando | N monitores, animación con gradiente arco-by-arco, física de rebote, hotplug. | +| **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. @@ -112,10 +113,11 @@ El flujo mínimo es: | Biblioteca | Qué hace | Necesaria para | |---|---|---| -| **libdrm** | Wrappers C de los ioctls de DRM/KMS | Parts 1–9 (todo lo básico) | -| **libgbm** | Generic Buffer Manager de Mesa | Part 12 (OpenGL ES) | -| **EGL** | Ponte entre OpenGL ES y el sistema de ventanas (o la falta de uno) | Part 12 (OpenGL ES) | -| **Vulkan Loader** | Carga los entrypoints de Vulkan en runtime | Part 13 (Vulkan) | +| **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. @@ -126,10 +128,14 @@ El flujo mínimo es: # Para libdrm gcc prog.c -ldrm -o prog -# Para OpenGL ES + EGL sobre DRM +# Para OpenGL ES + EGL sobre DRM (Part 12) gcc prog_egl.c -lGLESv2 -lEGL -ldrm -lgbm -o prog_egl -# Para Vulkan +# 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 ``` @@ -814,14 +820,18 @@ CPU e imprima sus propiedades: handle, pitch, size y dirección de memoria mapea --- -# PARTE 7 — Dibujar primitivas en C puro +# PARTE 7 — Punto y línea en C puro -Aquí empieza la parte divertida: **dibujar formas geométricas directamente en el buffer**. -Sin bibliotecas de gráficos, sin OpenGL. Solo matemáticas y punteros crudos a memoria. +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 -Imagina que el dumb buffer es una cuadrícula infinita de píxeles. Cada píxel tiene 4 bytes (RGBA). +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)`: ```c @@ -845,14 +855,32 @@ 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 Algoritmo de Bresenham: trazado de líneas +## 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: + +```c +// 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()`. ```c // Dibuja una línea de (x0,y0) a (x1,y1) en un dumb buffer 32bpp ARGB -void draw_line(int fd, uint8_t *buf, uint32_t pitch, +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); @@ -880,187 +908,69 @@ void draw_line(int fd, uint8_t *buf, uint32_t pitch, > **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**. +> 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.3 Midpoint circle: círculos perfectos +## 7.4 Dibujar un rectángulo sólido -Para dibujar círculos, usamos el algoritmo midpoint (también llamado "Bresenham para círculos"): +Una vez que tenés puntos y líneas, un rectángulo es trivial: dos bucles anidados que llenan cada fila: ```c -void draw_circle(int fd, uint8_t *buf, uint32_t pitch, - int cx, int cy, int radius, uint32_t color) { - int x = radius; - int y = 0; - int err = 1 - x; // Error inicial - - while (x >= y) { - // Dibujar los 8 píxeles de simetría - draw_pixel_8way(buf, pitch, cx, cy, x, y, color); - - y++; - if (err < 0) { - err += 2 * y + 1; - } else { - x--; - err += 2 * (y - x) + 1; +// 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); } } } -// Dibuja un píxel en las 8 posiciones simétricas del círculo -void draw_pixel_8way(uint8_t *buf, uint32_t pitch, - int cx, int cy, int x, int y, uint32_t color) { - // Los 8 octantes - draw_pixel(buf, pitch, cx + x, cy + y, color); - draw_pixel(buf, pitch, cx - x, cy + y, color); - draw_pixel(buf, pitch, cx + x, cy - y, color); - draw_pixel(buf, pitch, cx - x, cy - y, color); - draw_pixel(buf, pitch, cx + y, cy + x, color); - draw_pixel(buf, pitch, cx - y, cy + x, color); - draw_pixel(buf, pitch, cx + y, cy - x, color); - draw_pixel(buf, pitch, cx - y, cy - x, color); -} - -// Dibuja un solo 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; -} -``` - -> **Simetría de 8 vías:** Un círculo es simétrico en los 8 octantes. Cuando calculamos un punto -> `(x, y)` desde el centro `(cx, cy)`, automáticamente tenemos 7 puntos más: `(-x, y)`, `(x, -y)`, -> `(-x, -y)`, `(y, x)`, `(-y, x)`, `(y, -x)`, `(-y, -x)`. - -## 7.4 Midpoint ellipse: elipses - -Para elipses, el algoritmo midpoint se divide en **dos regiones** porque la pendiente cambia: - -```c -void draw_ellipse(int fd, uint8_t *buf, uint32_t pitch, - int cx, int cy, int rx, int ry, uint32_t color) { - // Región 1: donde |dy/dx| < 1 (más horizontal que vertical) - long long x = rx * rx; - long long y = 0; - long long err1 = x - (long long)rx * ry + (y * y) / 4; - - while (x > (long long)rx * rx * y / (2 * ry)) { // Región 1 - draw_pixel_8way(buf, pitch, cx, cy, - lround(x / rx), lround(y / ry), color); - y++; - err1 += (2 * y - 1) * x; - if (err1 >= 0) { - x--; - err1 -= (long long)ry * ry * 2; - } - } - - // Región 2: donde |dy/dx| >= 1 (más vertical que horizontal) - x = 0; - y = ry * ry; - long long err2 = y - (long long)rx * ry + (x * x) / 4; - - while (y > (long long)ry * ry * x / (2 * rx)) { // Región 2 - draw_pixel_4way(buf, pitch, cx, cy, lround(x / rx), lround(y / ry), color); - x++; - err2 += (2 * x - 1) * y; - if (err2 >= 0) { - y--; - err2 -= (long long)rx * rx * 2; - } - } -} - -void draw_pixel_4way(uint8_t *buf, uint32_t pitch, - int cx, int cy, int x, int y, uint32_t color) { - draw_pixel(buf, pitch, cx + x, cy + y, color); - draw_pixel(buf, pitch, cx - x, cy + y, color); - draw_pixel(buf, pitch, cx + x, cy - y, color); - draw_pixel(buf, pitch, cx - x, cy - y, color); -} -``` - -> **Dos regiones:** La mitad superior delipse tiene pendiente < 1 (cambia más en X que en Y), la -> inferior tiene pendiente >= 1. Por eso el algoritmo se divide: cambia la variable de decisión -> según qué eje avanza más rápido. - -## 7.5 Interpolación lineal (lerp) y gradientes - -Para colorear formas con gradientes, necesitás **interpolación lineal** entre dos colores: - -```c -// Lerp entre dos valores (o componentes de color) -int lerp(int a, int b, float t) { - return a + (int)((b - a) * t); -} - -// Interpolar entre dos colores RGB -uint32_t lerp_color(uint8_t r1, uint8_t g1, uint8_t b1, - uint8_t r2, uint8_t g2, uint8_t b2, float t) { - uint8_t r = lerp(r1, r2, t); - uint8_t g = lerp(g1, g2, t); - uint8_t b = lerp(b1, b2, t); - - // ARGB (little-endian): ABGR en memoria para XRGB8888 - return ((uint32_t)0xFF << 24) | // Alpha = 255 (opaco) - ((uint32_t)b << 16) | // Blue - ((uint32_t)g << 8) | // Green - ((uint32_t)r); // Red -} - -// Gradiente lineal de un color a otro a lo largo del ancho de pantalla -void draw_gradient_h(uint8_t *buf, uint32_t pitch, int width, int height, - uint8_t r1, uint8_t g1, uint8_t b1, - uint8_t r2, uint8_t g2, uint8_t b2) { - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - float t = (float)x / (width - 1); - uint32_t color = lerp_color(r1, g1, b1, r2, g2, b2, t); - - uint32_t *pixel = (uint32_t *)(buf + y * pitch + x * 4); - *pixel = 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; } } } ``` -> **`lerp` es la función más importante de gráficos:** Interpola entre cualquier par de valores. -> Se usa para: colores, coordenadas UV, transparencias, animaciones, texturas... casi todo lo que -> se mueve suavemente en una pantalla pasa por un `lerp`. +> **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 — Lienzo con primitivas geométricas +### Ejercicio 25.6 — Punto y línea en pantalla -**Conceptos** · Bresenham line, midpoint circle/ellipse, lerp color gradient, punteros a buffer +**Conceptos** · Bresenham line, punteros a buffer, protección de boundaries -**Contexto** · Escribí un programa que cree un dumb buffer de 800×600 a 32bpp e dibuje varias -primitivas geométricas en él. El resultado debe verse como una escena compuesta con: -- Un fondo degradado horizontal (rojo → amarillo → verde → azul → morado). -- Una línea diagonal gruesa cruzando el canvas. -- Un círculo centrado de radio 150. -- Dos elipses superpuestas, una horizontal y otra vertical. +**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 800×600 a 32bpp. -2. Dibujar un fondo degradado horizontal usando `lerp_color` (5 colores equidistantes). -3. Trazar una línea de Bresenham de `(50, 50)` a `(750, 550)` en blanco. -4. Dibujar un círculo centrado `(400, 300)` con radio `150` en rojo puro. -5. Dibujar una elipse centrada `(400, 300)` con rx=200, ry=100 en cian. -6. Dibujar otra elipse centrada `(400, 300)` con rx=100, ry=200 en magenta. +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 800×600 a 32bpp correctamente. -- [ ] Fondo degradado horizontal: 5 colores equidistantes usando `lerp`. -- [ ] Línea diagonal dibujada con Bresenham (sin huecos). -- [ ] Círculo centrado con radio 150 usando midpoint. -- [ ] Dos elipses superpuestas (una más ancha, otra más alta) con midpoint. -- [ ] Limpia: munmap → destroy en orden correcto. +- [ ] 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** @@ -1070,7 +980,32 @@ primitivas geométricas en él. El resultado debe verse como una escena compuest --- -# PARTE 8 — Framebuffers +# 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: @@ -1659,11 +1594,12 @@ detecte cambios de estado (conexión/desconexión de monitores, page flips). --- -# PARTE 12 — OpenGL ES + EGL +# 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 usar **OpenGL ES** con **EGL** para renderizar directamente -en un buffer de DRM, sin windowing system. +(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? @@ -1671,7 +1607,7 @@ EGL es la "ponte" entre OpenGL ES y el sistema de composición de display de tu X11, Wayland, Linux Framebuffer, o en nuestro caso... **DRM/KMS con GBM**. Sin windowing system, necesitás: -- **libgbm** para crear buffers queOpenGL ES pueda usar. +- **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. @@ -1727,7 +1663,7 @@ EGLContext egl_ctx = eglCreateContext(egl_dpy, egl_cfg, // Paso 6: Crear surface EGL desde GBM surface EGLSurface egl_surf = eglCreateWindowSurface(egl_dpy, egl_cfg, - (EGLNativeWindowType)gbm_surf, NULL); + (EGLNativeWindowType)gbm_surf, NULL); // Paso 7: Hacer el contexto current eglMakeCurrent(egl_dpy, egl_surf, egl_surf, egl_ctx); @@ -1736,134 +1672,7 @@ 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 Dibujar un triángulo con shaders - -OpenGL ES es **baseless**: necesitás al menos un vertex shader y un fragment shader: - -```c -// Vertex shader simple: pasa coordenadas directas a clip space -const char *vertex_shader = - "attribute vec4 pos;\n" - "void main() {\n" - " gl_Position = pos;\n" - "}\n"; - -// Fragment shader: color sólido (rojo) -const char *fragment_shader = - "precision mediump float;\n" - "void main() {\n" - " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" // Rojo puro - "}\n"; - -// Compilar shaders... -GLuint create_shader(GLenum type, const char *src) { - GLuint shader = glCreateShader(type); - glShaderSource(shader, 1, &src, NULL); - glCompileShader(shader); - - GLint ok; - glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); - if (!ok) { - GLchar log[512]; - glGetShaderInfoLog(shader, 512, NULL, log); - fprintf(stderr, "Shader compile failed: %s\n", log); - } - return shader; -} - -// Crear programa -GLuint vs = create_shader(GL_VERTEX_SHADER, vertex_shader); -GLuint fs = create_shader(GL_FRAGMENT_SHADER, fragment_shader); - -GLuint program = glCreateProgram(); -glAttachShader(program, vs); -glAttachShader(program, fs); -glBindAttribLocation(program, 0, "pos"); // location 0 = "pos" attribute -glLinkProgram(program); - -GLint link_ok; - glGetProgramiv(program, GL_LINK_STATUS, &link_ok); -if (!link_ok) { - GLchar log[512]; - glGetProgramInfoLog(program, 512, NULL, log); - fprintf(stderr, "Program link failed: %s\n", log); -} - -glUseProgram(program); -``` - -## 12.4 Dibujar y presentar - -```c -// Limpiar pantalla con negro -glClearColor(0.0f, 0.0f, 0.0f, 1.0f); -glClear(GL_COLOR_BUFFER_BIT); - -// Dibujar triángulo: vertices en clip space [-1, +1] -GLfloat vertices[] = { - 0.0f, 0.5f, 0.0f, // cima - -0.5f, -0.5f, 0.0f, // izquierda abajo - 0.5f, -0.5f, 0.0f // derecha abajo -}; - -GLuint pos_loc = glGetAttribLocation(program, "pos"); -glEnableVertexAttribArray(pos_loc); -glVertexAttribPointer(pos_loc, 3, GL_FLOAT, GL_FALSE, - 3 * sizeof(float), vertices); - -glDrawArrays(GL_TRIANGLES, 0, 3); - -// Presentar al GBM buffer -eglSwapBuffers(egl_dpy, egl_surf); -``` - -> **`eglSwapBuffers`:** En EGL sobre DRM/GBM, este swap no hace lo que pensás. No cambia buffers -> visuales como en X11. Simplemente sincroniza el rendering de OpenGL con la superficie GBM. -> Para presentar al display, necesitás hacer un page flip manual con el buffer GBM actual. - -### Presentación real a DRM desde EGL - -```c -// Obtener el buffer GBM actual (el que OpenGL escribió) -struct gbm_bo *bo = gbm_surface_lock_front_buffer(gbm_surf); - -// Crear framebuffer a partir del BO -uint32_t width, height; -gbm_bo_get_width(bo, &width); -gbm_bo_get_height(bo, &height); - -uint32_t handles[4] = {0}; -uint32_t strides[4] = {0}; -uint32_t offsets[4] = {0}; - -// Map buffer para obtener handles de DRM -handles[0] = gbm_bo_handle_for_plane(bo, 0).u32; -strides[0] = gbm_bo_get_stride_for_plane(bo, 0); -offsets[0] = gbm_bo_get_offset(bo, 0); - -uint32_t fb_id; -drmModeAddFB2(fd, width, height, GBM_FORMAT_XRGB8888, - handles, strides, offsets, &fb_id, 0); - -// Page flip para mostrar -drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, NULL); - -// Liberar el BO (pero NO el fb — page flip aún lo usa) -gbm_surface_release_buffer(gbm_surf, bo); - -// Cuando llegue el page_flip_handler: -void page_flip_handler(int fd, unsigned int frame, - unsigned int sec, unsigned int usec, void *data) { - // Acá sí podés borrar el fb que fue flippeado - drmModeRmFB(fd, *(uint32_t *)data); -} -``` - -> **Cuidado con la sincronización:** Entre `eglSwapBuffers` y el page flip, hay un race condition -> potencial. OpenGL puede estar aún escribiendo cuando el CRTC empieza a leer. Para una app de -> producción real, necesitás fences (GL fence sync) para garantizar que OpenGL terminó antes del flip. - -## 12.5 Limpieza +## 12.3 Limpieza del contexto EGL ```c eglMakeCurrent(egl_dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); @@ -1879,30 +1688,28 @@ close(fd); ## Práctica de la Parte 12 -### Ejercicio 25.11 — Triángulo colorido con OpenGL ES + EGL +### Ejercicio 25.11 — OpenGL ES sobre DRM sin windowing system -**Conceptos** · EGL initialization, GBM surfaces, shaders GLSL, vertex attributes, eglSwapBuffers +**Conceptos** · EGL initialization, GBM surfaces, `eglMakeCurrent` -**Contexto** · Escribí un programa que cree una ventana sin windowing system: abre la GPU via DRM, -configura EGL, compila un par de shaders GLSL para dibujar un triángulo de color verde sobre -fondo negro. +**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 800×600 a formato XRGB8888. +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. Compilar vertex shader + fragment shader que dibujen un triángulo verde. -6. Dibujar el triángulo con `glDrawArrays(GL_TRIANGLES, ...)`. -7. Presentar con `eglSwapBuffers` + page flip DRM manual. +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. -- [ ] Shaders GLSL compilados y enlazados sin errores. -- [ ] Triángulo verde visible (fondo negro, triángulo verde). +- [ ] Frame sólido rojo visible en pantalla (sin shaders). - [ ] Page flip manual presentado el buffer al display. **Restricciones / pistas** @@ -1911,15 +1718,458 @@ fondo negro. - 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: + +```c +// 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 + +```bash +# 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: + +```c +#include +#define NANOVG_GLES2_IMPLEMENTATION +#include + +// ... 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`: + +```c +// 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 `` de SVG. + +## 13.4 Gradientes con nanoVG + +NanoVG soporta tres tipos de gradientes: lineales, radiales y box (rectángulo difuminado): + +```c +// 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: + +```c +// 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): + +```c +// 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: + +```c +// 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: + +```c +// --- 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: + +```c +// 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: + +```c +// 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 13 — Vulkan surfaceless +# 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. -## 13.1 ¿Qué es "surfaceless" en Vulkan? +## 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. @@ -1936,7 +2186,7 @@ VkSurfaceKHR surface; // "surfaceless" = sin windowing system VkSwapchainKHR swapchain; ``` -## 13.2 Crear la Vulkan Instance +## 14.2 Crear la Vulkan Instance ```c // Habilitar extensiones necesarias para DRM/GBM @@ -1963,7 +2213,7 @@ vkCreateInstance(&create_info, NULL, &instance); > específico como `libvulkan-wayland` o `libvulkan-radeon`. La forma más universal es usar el > **Vulkan Display Protocol** (`VK_DISPLAY_POWER_STATE`). -## 13.3 Crear un swapchain surfaceless (GBM-backed) +## 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: @@ -2011,7 +2261,7 @@ 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). -## 13.4 Dibujar un frame en Vulkan +## 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: @@ -2086,7 +2336,7 @@ vkQueuePresentKHR(graphics_queue, &present); > 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. -## 13.5 Limpieza +## 14.5 Limpieza ```c vkDeviceWaitIdle(device); @@ -2099,9 +2349,9 @@ vkDestroyInstance(instance, NULL); close(fd); ``` -## Práctica de la Parte 13 +## Práctica de la Parte 14 -### Ejercicio 25.12 — Cuadrado rojo con Vulkan surfaceless +### Ejercicio 25.13 — Cuadrado rojo con Vulkan surfaceless **Conceptos** · Vulkan instance, physical device, swapchain GBM-backed, render pass, command buffers @@ -2132,7 +2382,7 @@ Vulkan en modo surfaceless. Sin shaders personalizados — usá el pipeline por --- -# PARTE 14 — Ejercicio jefe: Pelota rebotando en N monitores +# 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. @@ -2142,40 +2392,39 @@ Llegamos al final del capítulo. Este ejercicio combina **todo** lo que aprendis - Hotplug events (Parte 11): detectar cambios de monitores conectados. - OpenGL ES/EGL o Vulkan: renderizar primitivas sobre los buffers DRM. -## 14.1 El desafío completo +## 15.1 El desafío completo -> **Escribí un programa que muestre una pelota blanca rebotando simultáneamente en TODOS los -> monitores conectados a tu GPU**, usando DRM/KMS directo sin windowing system. +> **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. **Animación en tiempo real:** La pelota debe moverse diagonalmente rebotando en los bordes - de CADA monitor (no solo el principal). Si tenés dos monitores lado a lado, la pelota - debe atravesar de uno a otro sin detenerse. +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. **Doble buffering por CRTC:** Cada monitor tiene su propio par front/back buffer. Usá page flip +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. -4. **Gradiente arco-by-arco:** La pelota NO es blanca — está coloreada con un gradiente que varía - según el arco de rebote. Cada vez que la pelota toca un borde, cambia de color siguiendo - el espectro RGB: rojo → naranja → amarillo → verde → cian → azul → violeta → rojo... - -5. **Física de rebote:** La pelota tiene velocidad `(vx, vy)` en píxeles/frame. Rebotá invertiendo - la componente correspondiente cuando choque con un borde del monitor actual. Usá `lerp` para - suavizar la transición de color entre arcos. +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, y matemática en C puro. OpenGL ES / Vulkan son opcionales (podés dibujar las pelotas - usando Bresenham/midpoint sobre dumb buffers como hiciste en la Parte 7). + libgbm, EGL, y nanoVG sobre OpenGL ES. -## 14.2 Estructura de datos sugerida +## 15.2 Estructura de datos sugerida ```c #define MAX_MONITORS 8 @@ -2207,7 +2456,7 @@ typedef struct { } AppContext; ``` -## 14.3 Conversión de hue a RGB (para el gradiente arco-by-arco) +## 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`: @@ -2252,7 +2501,7 @@ uint32_t get_ball_color(float 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). -## 14.4 Lógica de movimiento y colisión +## 15.4 Lógica de movimiento y colisión ```c void update_ball(AppContext *app, Ball *ball, uint64_t dt_ms) { @@ -2313,7 +2562,7 @@ void on_bounce(AppContext *app, Ball *ball) { > La pelota se mueve en este espacio global. Cada frame, la renderizás separadamente en cada > monitor según su offset local. -## 14.5 Renderizado por monitor +## 15.5 Renderizado por monitor ```c void render_frame(AppContext *app) { @@ -2346,7 +2595,7 @@ void render_frame(AppContext *app) { } ``` -## 14.6 Criterios de aceptación finales +## 15.6 Criterios de aceptación finales El programa debe cumplir TODOS estos criterios para considerarse completo: @@ -2384,7 +2633,7 @@ El programa debe cumplir TODOS estos criterios para considerarse completo: - [ ] Manejo correcto de errores (no crash si un ioctl falla). - [ ] Código organizado en funciones pequeñas y reutilizables. -## 14.7 Compilación del ejercicio jefe +## 15.7 Compilación del ejercicio jefe ```bash # Con dumb buffers + Bresenham/midpoint (solo C puro + libdrm) @@ -2407,7 +2656,7 @@ gcc exercise_25_jefe.c -lvulkan -ldrm -lgbm -lm -o exercise_25_vulkan > sudo ./exercise_25_jefe > ``` -## 14.8 Lo que te llevás de este capítulo +## 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): @@ -2417,8 +2666,8 @@ Resumen rápido de lo que aprendiste (y las herramientas que ahora tenés): - **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. -- **Bresenham y midpoint son las bases de la gráficos por computadora.** Sin GPU, sin OpenGL, sin - bibliotecas. Solo sumas, restas y decision tree — eso es una línea en pantalla. +- **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. @@ -2426,14 +2675,19 @@ Resumen rápido de lo que aprendiste (y las herramientas que ahora tenés): - **KMS atómico es el futuro.** Legacy funciona para un monitor, pero multi-monitor real requiere blobs + properties + commit atómico. -- **OpenGL ES y Vulkan pueden correr sobre DRM sin windowing system.** EGL puentea entre GLES y GBM; - Vulkan surfaceless crea un swapchain sobre buffers GBM. Sin X11, sin Wayland — solo tu GPU. +- **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. 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. +> 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. --- diff --git a/dist/25-drm-kms.pdf b/dist/25-drm-kms.pdf index 2d2baad..e8c4623 100644 Binary files a/dist/25-drm-kms.pdf and b/dist/25-drm-kms.pdf differ