Cómo construí un backend de GPU para Emacs

Hace unos meses me obsesioné con una pregunta tonta: ¿por qué mi Emacs, con un portátil que tiene una GPU perfectamente capaz, dibuja todo el texto a base de CPU? Y eso me llevó a otras: ¿Por qué no puedo reproducir vídeo dentro de un buffer? ¿Por qué no puedo tener efectos de cursor animados? ¿Por qué no puedo hacer cross-fade entre buffers? Necesitaba saciar mi curiosidad, así que me puse a investigar.

Me puse a leer el código, con ayuda de una IA como compañero. Descubrí que cada glifo, cada subrayado, cada scroll, es recalculado y repintado por el procesador. El motor de redisplay de Emacs (xdisp.c) nació en una época en la que no había otra opción, y está afinado al milímetro para eso. Y nadie había conseguido meter una GPU por debajo sin reescribir medio Emacs... hasta hace poco.

Así que decidí intentarlo. Lo que empezó como un experimento de fin de semana terminó siendo un backend de display completo para macOS con Metal, un segundo backend para GNU/Linux con OpenGL, reproductor de vídeo dentro del buffer, efectos de cursor por shader, y un debate de más de cien mensajes en la lista de correo de los desarrolladores de Emacs que tocó desde el rendimiento de cairo hasta la libertad del software y la ética de la inteligencia artificial.

Este artículo existe porque me apetece contarlo y puede ser interesante para futuras implementaciones. Al final dejo las lecciones que me llevo y una conclusión que no es la que esperaba al empezar.

Una nota de honestidad por delante: este proyecto lo construí con la ayuda de un LLM como copiloto, de principio a fin. Lo digo aquí igual que lo dije en público cuando me preguntaron. Volveré sobre ello, porque resultó ser el giro de guion más importante de todo el camino.

Fase 1: la decisión de arquitectura

El primer impulso de cualquiera sería abrir el código de macOS, el backend Cocoa (nsterm.m), y empezar a sustituir llamadas de CoreGraphics por llamadas a Metal. Es lo más directo. Y es justo lo que decidí no hacer.

El problema de ese enfoque es que te ata a una plataforma. Si escribo "Emacs con Metal", tengo un Emacs para Mac y nada más. Había que escribir una abstracción del backend de display que me permitiera tener un driver por plataforma. Así que dibujé en un Post-it una arquitectura de tres capas:

flowchart TD
    X["Motor de redisplay
(xdisp.c, intacto)"]:::core --> P["src/gfxterm.c
Política de dibujo neutral (C puro)"]:::policy P --> D["src/gfxdrv.h
Interfaz del driver (~25 operaciones)"]:::iface D --> M["src/mtlterm.m (macOS)
Driver Metal"]:::mtl D --> G["src/glterm.c (GNU/Linux, X11)
Driver OpenGL ES / EGL"]:::gl classDef core fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff classDef policy fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff classDef iface fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff classDef mtl fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff classDef gl fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff

La idea es que toda la lógica de dibujo (cómo se compone una cadena de glifos, dónde va el subrayado ondulado, cómo se recorta una imagen a la ventana, cómo se hace el scroll) viva en un archivo de C puro, sin una sola línea específica de plataforma. Y que cada plataforma solo tenga que implementar un contrato pequeño: unas 25 operaciones primitivas del estilo "sube esta textura", "dibuja este quad", "presenta el frame". Ese contrato es gfxdrv.h. El primer driver sería Metal, en mtlterm.m.

La regla de oro, la que me autoimpuse y no rompí ni una vez: xdisp.c no se toca. El motor de redisplay calcula las matrices de glifos exactamente igual que siempre; yo solo me engancho en la interfaz de dibujo que ya existe. Si el experimento salía mal, Emacs seguía siendo Emacs.

Visto en retrospectiva, ¡esta fue la mejor decisión del proyecto!

Fase 2: el backend de Metal y la tiranía del píxel

Con la arquitectura clara, me lancé a por Metal. El plan técnico era el de cualquier renderizador de texto moderno:

  1. Rasterizar cada glifo una sola vez, vía CoreText, a una textura en escala de grises (un atlas de glifos en formato R8).
  2. Dibujar el texto como quads texturizados que muestrean ese atlas.
  3. Subir las imágenes (PNG, JPEG, SVG, GIF) como texturas.
  4. Componer el frame entero en la GPU, en una textura persistente, y presentarlo.

Sobre el papel, dos tardes. En la práctica, semanas. El motivo tiene nombre: paridad de píxel.

Mi criterio de éxito no era "que se vea bien". Era que el resultado fuese idéntico, píxel a píxel, al backend Cocoa original. Mismo binario, con la GPU activada y desactivada, y el diff entre las dos capturas tenía que ser prácticamente cero. Monté un harness que arrancaba el mismo Emacs por duplicado, cargaba un escenario idéntico, capturaba la pantalla en ambos y comparaba con Python y PIL. El listón quedó en torno al 0,055% de píxeles distintos en el baseline, y todo lo que se desviaba de ahí era un bug que cazar.

Ese harness fue implacable y sacó a la luz una colección de detalles que debí mirar con lupa:

  • El peso de la tinta. CoreText y mi shader aplicaban el antialiasing distinto.
  • Los colores del relieve (los bordes 3D de los botones y la mode-line) no salían.
  • Había un off-by-one en la posición vertical de los glifos.

No debemos ignorar que la forma de dibujar es completamente distinta, tanto en enfoque como en arquitectura. Ello provoca que los bugs sean sutiles y difíciles de detectar.

Fase 3: el cursor que se congelaba

De todos los bugs, el que más me enseñó fue el del cursor.

Quería efectos de cursor animados: un anillo que se expande al saltar, una estela tipo cometa, ese tipo de azúcar visual que la GPU hace casi gratis. Los implementé como una capa de compositor por encima del frame, sin tocar el contenido del buffer debajo. Funcionaban perfectamente... mientras tecleaba. En cuanto dejaba de tocar el teclado, la animación se congelaba a medias. El culpable era el mecanismo de sincronización de Apple, CADisplayLink: se muere en reposo, el event loop de Emacs no lo alimenta cuando no hay entrada del usuario. Mientras tecleaba, los eventos de teclado bombeaban el run loop y todo iba fino; en cuanto paraba, no había quien moviera el reloj.

La solución fue dejar de depender del sistema y mover todo lo continuo desde un timer de Lisp. Cursor, cross-fade entre buffers y vídeo, todo avanza desde un único "pump" en Emacs Lisp que late de forma periódica y le dice al driver "avanza todo lo que tengas y presenta como mucho una vez". Más adelante unifiqué los tres timers en uno solo con auto-pacing (60 Hz cuando hay un fundido, 30 Hz el resto, y se apaga solo cuando no hay nada que animar).

Cuando esto quedó resuelto, macOS estaba completo. Texto, decoraciones, imágenes, GIF animado, line numbers, fringes con bitmaps personalizados, mode-line, header-line, tab-bar, Retina/HiDPI a 2x, los cuatro tipos de cursor, splits, text-scale dinámico. Todo pixel-perfect contra Cocoa.

Ahora tocaba añadir las cosas que solo la GPU puede hacer:

  • Vídeo dentro del buffer
  • Efectos de cursor por shader
  • Cross-fade al cambiar de buffer.

Como experimento llegué a montar un pequeño frontend de YouTube dentro de Emacs: buscaba el vídeo y lo reproducía directamente en un buffer, con la GPU componiendo los fotogramas sobre el texto. Una tontería divertida que solo es posible cuando el frame lo pinta la tarjeta gráfica.

Y el cross-fade al cambiar de buffer, un fundido suave que en la GPU es una simple pasada de shader más:

Fue relativamente sencillo, ya que el motor de redisplay no sabe ni le importa lo que haga por encima; son solo operaciones de composición en la GPU.

Fase 4: empaquetar es la mitad del trabajo

Tener el binario funcionando en mi máquina y tener algo que otra persona pueda instalar son dos planetas distintos. Esta fase no tiene glamour pero me comió días enteros.

La firma y la notarización de Apple fueron su propio laberinto. Y cuando añadí native-comp (compilación nativa AOT), aparecieron ~1564 archivos .eln que también son código Mach-O y también hay que firmar uno por uno, con timestamp seguro, para que la notarización los acepte.

Publiqué la primera release firmada y notarizada, un cask de Homebrew, y empecé a usarlo a diario yo y algún compañero. Funcionaba. Estaba contento. Pensé que la parte difícil había quedado atrás.

Entonces decidí enseñárselo a la lista de correo de Emacs.

Fase 5: emacs-devel, o cómo aprender humildad en un hilo de correo

El 8 de junio de 2026 mandé un [RFC PATCH] a emacs-devel con el asunto "GPU display backend with a neutral driver layer (Metal on macOS)". Lo planteé con cuidado: no vendía "Emacs con Metal", vendía la abstracción. Una capa de dibujo neutral más un driver fino por plataforma detrás de un vtable pequeño, con Metal como primer driver, xdisp.c intacto, paridad verificada con harness automático, y copyright assignment de la FSF ya en regla.

Mi primer error fue enviar el parche completo, y no un RFC con la idea, el diseño y una demo mínima. La respuesta llegó rápido:

Sean Whitton escribió:

"People don't normally post such large patches at once without first discussing the design issues with people on the list. Given this, I just have to ask, this isn't LLM-generated, is it?"

Respondí con honestidad:

"100% created with LLM.

I understand that this is a rather large addition, and if it's rejected, I won't be offended. My intention was to share it because it's fully developed [...]; I'm using it daily without any problems (along with other colleagues)."

La respuesta de Whitton fue cortés y definitiva:

"I'm afraid there is a policy conflict. The GNU project does not accept any LLM-generated contributions at present. Thank you for your interest in Emacs, anyway."

Y ahí, en términos de "¿esto se mergea?", el proyecto murió en menos de un día. El proyecto GNU no acepta, a día de hoy, contribuciones generadas con LLM. Punto. No hay debate técnico que valga cuando choca con una política tajante.

Lo que no me esperaba es que, lejos de cerrarse el hilo, se abriera en tres direcciones a la vez.

El giro hacia "objeto de estudio"

Dmitry Gutov marcó el tono de lo que vendría:

"We cannot accept this as code contribution, but if you are already using it locally, it might be useful as a study subject. It might be more useful to test with a Linux port, though."

Es decir: como código no entra, pero como referencia o como experimento puede valer. Y ahí estaba, además, la semilla de lo que haría después.

El debate de la libertad

Y entonces entró Richard Stallman, que bifurcó el asunto del hilo a "GPU-specific code with no GPU-specific features?" y lo elevó a una cuestión moral:

"In general the GPU is a disaster for software freedom: it turns your computer into a prison."

Más adelante, insistiendo:

"They don't put physical chains on the user, but they do put digital chains on the user's computing. GPUs are a substantial part of what we are fighting to free people from."

No todo el mundo lo compró. Arsen Arsenović respondió con la objeción técnica más afilada del hilo:

"This is a bizarre comparison based on frivolous word association. GPU programming APIs such as Vulkan or OpenGL [...] can be implemented fully in software, and indeed are implemented using fully free software in Mesa, so there's no downside to using them from this perspective."

Y Madhu aportó el dato incómodo que desmonta media discusión:

"If you are using X11 on a modern (say post 2021) intel machine on linux, all your 2d graphics probably goes through the GPU backend, X11 windows are just textures."

Tenían razón en algo que yo podía demostrar: mi driver de OpenGL corre también sobre el rasterizador software de Mesa (llvmpipe). De hecho, la suite de paridad se ejecuta headless sobre él. O sea, el código no requiere firmware no libre de GPU para ejercitarse. Lo dije en el hilo, aunque a esas alturas el debate ya tenía vida propia.

La duda técnica de fondo

La crítica más sustanciosa, y la que más me hizo pensar, no fue ni la política ni la ideológica. Vino de Eli Zaretskii, uno de los mantenedores históricos:

"I'm not really surprised that using a GPU in the display backend yields performance gains that are not really spectacular with reasonable sizes of the frame: the design of the current display engine is optimized towards CPU-driven redisplay, so taking a better advantage of GPUs will probably need a more thorough redesign, not just a separate backend."

Y Gerd Möllmann, el mantenedor del redisplay, lo remató con elegante indiferencia:

"It looks to me as if this adds GPU support without adding GPU-only features or changing the architecture of redisplay [...]. Can have performance benefits, maybe, don't know, but it's outside of my field of interest."

Tenían parte de razón. El motor está pensado para repintar rectángulos sucios pequeños en la CPU, y eso lo hace cairo extraordinariamente bien. Meter una GPU debajo sin rediseñar el motor tiene un techo. Pero también tenían una parte que solo se ve con un segundo backend y con números. Y yo aún no tenía ni una cosa ni la otra.

La comunidad, sin saberlo, me había escrito la hoja de ruta de las semanas siguientes: hacer un segundo backend (para validar la abstracción) y traer números honestos (para zanjar la duda de Eli).

Fase 6: el driver de OpenGL, o cómo cobrar la apuesta de la arquitectura

Si la gran promesa de mi diseño era "la capa neutral se reutiliza tal cual, solo cambias el driver", la única forma de demostrarlo era escribir un segundo driver desde cero y ver cuánto código compartido sobrevivía sin tocarse.

Elegí GNU/Linux sobre OpenGL ES 3 con EGL, en X11. El equivalente multiplataforma del driver Metal: rasterizo los glifos con FreeType a un atlas en la GPU, renderizo a un FBO y presento haciendo un blit a la superficie de la ventana con eglSwapBuffers. La política de dibujo, gfxterm.c, la reutilicé entera. Y funcionó: el segundo backend salió pixel-perfect contra el Emacs estándar de GTK/cairo, en la misma batería de pruebas exhaustiva que macOS, corriendo tanto sobre un servidor X real con GPU como headless bajo Xvfb para el harness.

Ese fue el momento en que la arquitectura dejó de ser una promesa y pasó a ser un hecho. Escribir el driver entero, con todas sus particularidades de EGL y FreeType, me llevó muchísimo menos que el primero, porque toda la lógica difícil ya estaba escrita y probada en la capa neutral.

Pero Linux trajo su propio infierno, con los bugs más difíciles de todo el proyecto. El peor: al cambiar de buffer, durante un parpadeo (un solo vblank), se colaba el dashboard de arranque a medio pintar, imágenes de otro contenido fantasma. Tardé días en descubrir que la raíz no era mi código de GPU, sino el back buffer de la extensión de doble buffer de X11 (XDBE), que Emacs pintaba al arrancar y mi backend nunca volvía a tocar.

Sin embargo, después de un tiempo de trabajo y depuración, el driver de OpenGL quedó estable y funcional. No perfecto, pero sí lo suficiente para poder ejecutar el harness de rendimiento y comparar con cairo.

Fase 7: optimizar y traer números honestos

Los resultados, en un portátil con GPU integrada AMD Radeon (Renoir), frame de 1616x912, buffer de 8000 líneas con font-lock:

Carga Estándar (X/cairo) GPU (OpenGL) Ratio
Scroll de línea 530 fps 487 fps 0.92x
Scroll de página 297 fps 296 fps 1.00x
Repintado completo 247 fps 294 fps 1.19x
Tecleo 1857 fps 1311 fps 0.71x
Scroll de imágenes 1359 fps 1239 fps 0.91x

En un frame de tamaño portátil, tecleo y scroll de línea siguen siendo más lentos que cairo, que es buenísimo recortando rectángulos pequeños. Mi suelo es un swap de buffer EGL por redisplay; el suyo, un rectángulo de daño minúsculo sin swapchain. En términos absolutos, todo está muy por encima de lo perceptible (el peor caso, tecleo, son ~0,8 ms por pulsación), así que es una cuestión de throughput, no de fluidez.

En conclusión: un backend de GPU no le gana a un rasterizador de CPU maduro en texto estático.

Pero las mismas cargas a 4K (3760x2210) le dan la vuelta a la tortilla:

Carga cairo (CPU) GPU Mejora
Scroll de línea 117 fps 240 fps 2.05x
Scroll de página 102 fps 124 fps 1.22x
Repintado completo 66 fps 121 fps 1.84x
Tecleo 238 fps 1766 fps 7.4x
Scroll de imágenes 115 fps 1328 fps 11.5x

El coste de cairo crece linealmente con el número de píxeles; el de la GPU apenas se mueve. El scroll de imágenes es el caso extremo: cairo re-blitea la imagen desde memoria de CPU en cada frame, la GPU re-compone una textura ya cacheada. Ahí, más las funciones que solo existen en la GPU (vídeo, cross-fades, efectos de cursor), está el valor real. El throughput de texto en una pantalla pequeña, no.

Conclusión

Seamos sinceros, para texto cotidiano en una pantalla normal, no hay razón para cambiar; el backend de CPU es igual de rápido o más. La GPU se gana su sitio en movimiento, efectos y vídeo a muchos píxeles, cosas que la CPU hace más caras o no hace.

El backend nunca se mergeará en Emacs, y está bien. Choca con la política de no aceptar contribuciones generadas con LLM, y esa es una decisión legítima del proyecto que respeto. Lo mantengo como un fork propio, detrás de --with-gpu, opt-in y desactivable con una variable de entorno, y lo uso a diario.

¿Mereció la pena, entonces? Para mí, sin ninguna duda. Salí con un backend de display completo en dos plataformas, con vídeo dentro del buffer y efectos que el Emacs estándar no puede dibujar, con una arquitectura que demostró sostenerse en dos implementaciones, y con un montón de cicatrices técnicas que valen su peso en oro. Pero sobre todo salí con algo que no buscaba: una conversación pública, dura y honesta, con gente que lleva décadas manteniendo este editor, sobre rendimiento, sobre libertad del software y sobre el papel de la IA en el código que escribimos. Esa conversación, citada arriba con sus palabras exactas, vale más que cualquier merge.

Ahora estoy centrando mis esfuerzos en el backend de OpenGL para GNU/Linux, que aún hay mucho que pulir y optimizar. Si estáis interesados en probarlo, en el respositorio encontraréis un .deb con los binarios de prueba.

Mi consejo final, si te planteas algo parecido: persigue el experimento que te obsesiona aunque sepas que igual no acaba donde imaginas. Yo empecé queriendo que mi GPU dibujara texto y terminé aprendiendo de arquitectura, de rasterización, de empaquetado, de la cultura de un proyecto de 40 años y de mí mismo. El destino resultó ser irrelevante. El camino, no.

El código está en github.com/tanrax/emacs-gpu, y el hilo completo de emacs-devel se puede leer en el archivo de junio de 2026.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Will you buy me a coffee?

Comments

Jesús Gómez

Que historia tan inspiradora. Muchas gracias por compartirla.
1 answers

Andros Fenollosa

Muchas gracias a ti por el comentario 🙂

Written by Andros Fenollosa

June 23, 2026

15 min of reading

You may also like

Visitors in real time

You are alone: 🐱