wandres.dev
CÓMO SE DIBUJA UN PÍXEL · El pipeline gráfico

Por qué el dibujado está partido en etapas y no en una función

Las dos restricciones del hardware que obligan a que el pipeline gráfico tenga exactamente esta forma, y qué etapa nace de cada una.

⏱ 19 min

El pipeline gráfico se suele presentar como una lista de cajas con flechas, y así memorizado no sirve para nada. Las etapas no son una convención: cada una existe porque hay una restricción física concreta que impide hacer ese trabajo en otro sitio. Esta lección explica el pipeline al revés de lo habitual, empezando por las restricciones, para que la secuencia de etapas salga como consecuencia y no como dogma.

🎯 Al terminar esta lección sabrás
  • Enunciar las dos restricciones del hardware que determinan la forma del pipeline.
  • Explicar por qué el trabajo por vértice y el trabajo por fragmento no pueden fusionarse.
  • Situar cada etapa como respuesta a una restricción concreta.
  • Distinguir etapas programables de etapas de función fija y saber por qué unas lo son y otras no.

Las dos restricciones que lo explican todo

Restricción uno: la aritmética es casi gratis y el acceso a memoria es carísimo. En una GPU moderna, una multiplicación en coma flotante cuesta un ciclo y hay miles de unidades haciéndolo a la vez. Una lectura de memoria externa cuesta cientos de ciclos. La proporción entre ambas cosas lleva décadas empeorando: cada generación añade más potencia aritmética que ancho de banda. De ahí sale la estrategia central del hardware gráfico, que es esconder la latencia con paralelismo: cuando un grupo de ejecución se bloquea esperando una textura, el planificador mete otro grupo en las mismas unidades. Para que eso funcione hacen falta miles de tareas independientes listas a la vez. Todo el pipeline está diseñado para producir esas tareas.

Restricción dos: el número de elementos de trabajo cambia brutalmente a mitad del proceso. Un triángulo tiene tres vértices y puede cubrir un píxel o dos millones. Ese factor de amplificación no se conoce hasta que los vértices están transformados y proyectados. Es decir: hay un punto del proceso en el que un elemento de entrada se convierte en un número imprevisible de elementos de salida.

Esas dos restricciones bastan para deducir la arquitectura. Como la amplificación es imprevisible, no puedes escribir una sola función que vaya del vértice al píxel: tendría que emitir un número variable de resultados, y eso destruye el paralelismo regular que la restricción uno exige. La solución es partir el trabajo justo en el punto de amplificación e insertar ahí una etapa que sepa repartir: el rasterizador. Antes del corte, una tarea por vértice. Después del corte, una tarea por fragmento. Ambas son masivamente paralelas y regulares; el corte es lo que las hace posibles.

flowchart TB
buffers[Buffers de vertices e indices] --> ia[Ensamblado de entrada]
ia --> vs[Vertex shader programable]
vs --> opt[Teselacion y geometria no expuestas en la web]
opt --> pa[Ensamblado de primitivas y recorte]
pa --> div[Division perspectiva y transformacion de viewport]
div --> rast[Rasterizacion funcion fija]
rast --> fs[Fragment shader programable]
fs --> tests[Pruebas por fragmento profundidad y stencil]
tests --> blend[Mezcla y escritura]
blend --> fb[Framebuffer]
style buffers fill:#94e2d5,color:#11111b
style ia fill:#89b4fa,color:#11111b
style vs fill:#fab387,color:#11111b
style opt fill:#f38ba8,color:#11111b
style pa fill:#89b4fa,color:#11111b
style div fill:#89b4fa,color:#11111b
style rast fill:#89b4fa,color:#11111b
style fs fill:#fab387,color:#11111b
style tests fill:#89b4fa,color:#11111b
style blend fill:#f9e2af,color:#11111b
style fb fill:#94e2d5,color:#11111b

En naranja las dos etapas donde se ejecuta código tuyo. En azul las de función fija, implementadas en silicio dedicado. En rojo las etapas opcionales que existen en el hardware de escritorio pero que ninguna API web expone, y que tienen su propia lección.

Qué se paraleliza, qué no, y por qué

La pregunta que ordena todo el diagrama es: ¿puede esta etapa procesar sus elementos sin mirar a los demás?

El vertex shader sí. Cada vértice se transforma sin saber nada de sus vecinos. No puede crear vértices ni destruirlos, no puede leer el resultado de otra invocación, no puede escribir en una posición arbitraria de memoria. Esas prohibiciones son exactamente lo que permite lanzar cien mil invocaciones a la vez sin ninguna sincronización. La API te limita para poder ir rápido, y ese patrón se repite en todo el pipeline.

El fragment shader casi. Cada fragmento se colorea independientemente, con una excepción importante: las derivadas. Para saber cuánto varía una coordenada de textura entre píxeles vecinos —y con ello elegir el nivel de mipmap— el hardware ejecuta los fragmentos en bloques de dos por dos y compara valores entre las cuatro invocaciones del bloque. Eso convierte al fragment shader en casi independiente, con una dependencia local mínima. Tiene consecuencias importantes que se ven en la lección de rasterización.

El rasterizador no. Su trabajo es determinar qué píxeles cubre un triángulo, y ese trabajo tiene una estructura espacial que no se reparte bien en un modelo de una tarea por elemento. Además necesita coherencia: los fragmentos vecinos deben acabar en la misma unidad para aprovechar cachés y para formar los bloques de dos por dos. Es un algoritmo idéntico siempre, ejecutado billones de veces, con una estructura de datos muy específica. Ese es el perfil exacto de lo que conviene poner en silicio dedicado en lugar de en un shader.

La mezcla tampoco, y por un motivo distinto. Dos triángulos que caen en el mismo píxel tienen que combinarse en el orden en que los enviaste, porque la mezcla no es conmutativa. Eso significa que el final del pipeline tiene un punto de serialización obligatorio por píxel. La GPU ejecuta miles de fragmentos en desorden, pero el resultado tiene que ser como si se hubieran procesado en orden de envío. Mantener esa ilusión es trabajo de unidades dedicadas, y es la razón profunda de que la transparencia sea cara.

El pipeline no está partido por etapas conceptuales, está partido por dónde cambia el número de tareas

La lectura habitual —vértices, luego píxeles, porque primero va la forma y luego el color— es una racionalización a posteriori. El corte real está donde la cardinalidad se rompe. Antes del rasterizador, el número de elementos es conocido, fijo y proporcional a lo que enviaste. Después, es imprevisible y depende de la proyección: el mismo triángulo puede generar cuatro fragmentos o un millón según dónde esté la cámara. Un sistema paralelo no puede planificar trabajo cuya cantidad no conoce, así que hace falta una etapa que convierta lo imprevisible en lotes regulares, y esa etapa tiene que estar en hardware porque se ejecuta una vez por píxel candidato. Esta forma de verlo tiene un rendimiento práctico inmediato: te dice que el coste de una escena no es un número, son dos números independientes. Un coste proporcional a los vértices y otro proporcional a los fragmentos cubiertos, con un cambio de régimen en medio. Casi todo el diagnóstico de rendimiento en 3D consiste en averiguar cuál de los dos domina, y la prueba definitiva es tan burda como reducir la ventana a una cuarta parte: si el fotograma mejora mucho, el cuello está después del rasterizador; si no cambia, está antes.

El ciclo de vida de un triángulo

Con las restricciones sobre la mesa, la secuencia completa se lee sola.

Ensamblado de entrada. El hardware lee los búferes de vértices y, si existe, el búfer de índices, y compone para cada vértice el conjunto de atributos que el shader espera. Existe porque los datos están en memoria en un formato compacto que hay que descodificar.

Vertex shader. Una invocación por vértice. Su única salida obligatoria es una posición en espacio de recorte. Existe porque las transformaciones son distintas en cada aplicación y no se pueden fijar en silicio.

Ensamblado de primitivas y recorte. Los vértices se agrupan de tres en tres para formar triángulos y se recortan contra el volumen de visión. El recorte existe por una razón puramente aritmética: la proyección en perspectiva divide por una coordenada que puede ser cero o negativa detrás de la cámara, y dividir ahí produce basura. Hay que cortar antes de dividir.

División perspectiva y viewport. Se divide por esa coordenada y se pasa a un cubo normalizado; después se escalan las coordenadas al tamaño real en píxeles.

Rasterización. Se determina qué muestras del framebuffer caen dentro del triángulo y se generan fragmentos con los atributos interpolados.

Fragment shader. Una invocación por fragmento. Calcula el color. Es donde vive casi todo el coste de una escena moderna.

Pruebas por fragmento. Profundidad, stencil, tijera. Deciden si el fragmento sobrevive.

Mezcla y escritura. El color superviviente se combina con lo que ya había y se escribe.

Lo que este modelo predice antes de medir

Un modelo mental sirve si genera predicciones. Este genera al menos cuatro, y todas se comprueban.

Predice que subdividir una malla es barato hasta que deja de serlo. Duplicar los triángulos duplica el trabajo por vértice, pero no cambia el número de píxeles cubiertos. Si tu escena está limitada por fragmentos, una malla con el doble de detalle puede costar prácticamente lo mismo.

Predice que el tamaño de la ventana afecta a una mitad del coste y no a la otra. Es la prueba de diagnóstico que acabas de leer.

Predice que un shader de fragmento complejo cuesta mucho más que un shader de vértice complejo, porque se ejecuta órdenes de magnitud más veces. Mover un cálculo del fragment shader al vertex shader, cuando el resultado se puede interpolar sin error visible, es la optimización más rentable que existe en gráficos.

Predice que dibujar de delante a atrás ahorra trabajo en objetos opacos, porque los fragmentos tapados pueden descartarse antes de ejecutar el shader. Three.js lo hace por defecto y por eso no lo notas, pero explica por qué el orden de dibujo importa incluso cuando el resultado visual es idéntico.

Las cuatro predicciones se refinan en las lecciones siguientes, empezando por qué es exactamente un vértice, que resulta no ser lo que casi todo el mundo cree.