Saltar al contenido principal
Graphics

Pipeline de Renderizado GPU: Blend Modes, Compositing Porter-Duff y Rendering Tile-Based

11 min lectura
LD
Lucio Durán
Engineering Manager & AI Solutions Architect
También disponible en: English, Italiano

El Pipeline de Renderizado del Navegador Después del Layout

La mayoría de los diagramas del pipeline de renderizado del browser muestran algo como: DOM → Style → Layout → Paint → Composite. No está mal, pero es engañoso porque sugiere un flujo lineal limpio. En realidad, las últimas dos etapas son donde vive toda la complejidad, y interactúan de formas no obvias.

Lo que realmente pasa después del layout:

1. Generación de paint records (hilo principal)
 - Recorrer el layout tree en orden de stacking
 - Generar una display list de operaciones de pintado (dibujar rect, texto, etc.)
 - Agrupar operaciones en paint layers según disparadores de compositing

2. Rasterización (hilo compositor + workers de raster)
 - Dividir cada paint layer en tiles (típicamente 256x256 px)
 - Programar rasterización de tiles en hilos worker o GPU
 - Skia convierte ops de pintura a comandos GPU (o rendering por software en CPU)

3. Compositing (hilo compositor → proceso GPU)
 - Organizar texturas de tiles rasterizados según el layer tree
 - Aplicar transforms, clip, opacity, filters, blend modes
 - Enviar draw calls a la GPU para output final al framebuffer

4. Display (GPU → controlador de pantalla)
 - Swap de buffers o present
 - Sincronización VSync

La insight crítica: rasterización y compositing son operaciones GPU diferentes. Rasterización convierte contenido vectorial (paths, texto, cajas) en texturas de píxeles. Compositing combina esas texturas en la imagen final. Pueden ocurrir en hilos diferentes, colas GPU diferentes, y hasta a frame rates diferentes.

Blend Modes: La Matemática Detrás de la Magia

Los blend modes se definen en la especificación W3C de Compositing and Blending, que a su vez se basa en el álgebra de compositing de Porter-Duff de 1984. Cada operación de blend tiene dos componentes:

  1. Función de blending B(Cb, Cs): Cómo se combinan los colores source y destination
  2. Operador de compositing: Cómo interactúan los canales alpha de source y destination
// Fórmula general de compositing:
// Co = αs × Fa × Cs + αb × Fb × Cb
//
// Donde Fa y Fb dependen del operador Porter-Duff:
// source-over: Fa = 1, Fb = 1 - αs
// source-in: Fa = αb, Fb = 0
// source-out: Fa = 1 - αb, Fb = 0
// etc.

// Para blending con compositing source-over:
// Co = αs × (1 - αb) × Cs + αs × αb × B(Cb, Cs) + (1 - αs) × αb × Cb

// Funciones comunes de blend mode:
// Multiply: B(Cb, Cs) = Cb × Cs
// Screen: B(Cb, Cs) = Cb + Cs - Cb × Cs
// Overlay: B(Cb, Cs) = HardLight(Cs, Cb) // sí, está invertido
// Soft-Light: B(Cb, Cs) = función piecewise compleja...

A nivel de GPU, los blend modes "simples" que mapean a la unidad de blend de función fija son esencialmente gratis — el hardware los hace sin costo extra durante la rasterización. El source-over estándar (compositing normal con alpha) es una sola configuración de estado de blending:

// Equivalente en OpenGL del compositing source-over
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
 GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Pero aquí es donde se pone caro. Blend modes avanzados como multiply, screen, overlay, color-dodge, etc., no se pueden expresar como ecuaciones de blend de función fija en GPUs viejas. La unidad de blend del hardware solo soporta un set limitado de operaciones (add, subtract, min, max) con factores configurables de source/destination.

Para manejar blend modes avanzados, la GPU (o el browser) tiene tres opciones:

Opción 1: KHR_blend_equation_advanced (extensión GPU)
 - Soportada en la mayoría de GPUs modernas (Vulkan, GL 4.x, Metal)
 - Agrega blend modes por hardware para multiply, screen, overlay, etc.
 - Requiere barreras de blending coherente entre draw calls
 - Rápido, pero el overhead de barreras puede acumularse

Opción 2: Truco de dual-source blending (modos limitados)
 - Usar fragment shader para producir dos colores
 - Configurar la unidad de blend para combinarlos
 - Solo funciona para modos expresables con dos términos lineales

Opción 3: Readback del framebuffer en shader
 - Leer el color actual del framebuffer en el fragment shader
 - Calcular el resultado del blend manualmente
 - Escribir el color blended
 - LENTO: rompe el modelo de pipeline paralelo de la GPU

¿Esa animación SVG que mencioné? Estaba cayendo en la opción 3. El mix-blend-mode: multiply en un elemento de 1920x1080 hacía que el fragment shader leyera el framebuffer en cada píxel, serializando lo que debería haber sido una operación paralela. Mover el blend mode a un elemento más chico, pre-compuesto, redujo el costo un 98%.

Tile-Based Rendering: Por Qué la GPU de Tu Celular Es Rara

Las GPUs de escritorio (NVIDIA, AMD) usan una arquitectura de Immediate Mode Renderer (IMR): cada triángulo se procesa completamente a través del pipeline y sus fragmentos se escriben directamente al framebuffer en memoria off-chip.

Las GPUs móviles (Qualcomm Adreno, ARM Mali, Apple GPU) usan Tile-Based Deferred Rendering (TBDR): el viewport se divide en tiles chiquitos (típicamente 16x16 o 32x32 píxeles), toda la geometría se clasifica en tiles, y cada tile se renderiza enteramente en memoria on-chip rápida antes de escribirse una vez.

IMR (Desktop):
 Para cada triángulo:
 Rasterizar → shading de fragmentos → escribir al framebuffer (off-chip)
 Ancho de banda: ALTO (lectura/escritura constante del framebuffer)
 Paralelismo: ALTO (triángulos son independientes)

TBDR (Mobile):
 Pase 1 - Binning:
 Para cada triángulo:
 Determinar qué tiles toca → agregar al bin del tile
 Pase 2 - Rendering:
 Para cada tile:
 Cargar tile en memoria on-chip
 Para cada triángulo en el bin de este tile:
 Rasterizar → shading de fragmentos → escribir a memoria del tile (on-chip)
 Escribir tile terminado al framebuffer (off-chip)
 Ancho de banda: BAJO (una escritura por tile, no por fragmento)
 Latencia: MÁS ALTA (pase extra de binning)

Esto importa para el renderizado del browser porque los blend modes y la transparencia se comportan diferente en GPUs tile-based. Como todos los fragmentos de un tile se procesan juntos, la GPU puede resolver blending en memoria on-chip sin readbacks costose es del framebuffer. Por eso mix-blend-mode puede ser más rápido en mobile que en desktop en algunos case es — contraintuitivo pero cierto.

Sin embargo, el tile-based rendering odia una cosa: overdraw con shaders complejos. Cada fragmento en un tile se shadea, aunque eventualmente esté ocluido por un fragmento posterior. El Forward Pixel Kill de Mali y el Hidden Surface Removal de Apple intentan mitigar esto, pero stacks de capas complejas con blending derrotan estas optimizaciones porque los píxeles ocluidos realmente contribuyen al color final.

Skia: El Motor de Renderizado 2D del Navegador

Chrome, Android, Flutter, y Firefox (parcialmente) todos usan Skia para renderizado 2D. El backend GPU de Skia (Ganesh, que está siendo reemplazado por Graphite) traduce comandos de dibujo de alto nivel a llamadas de API GPU.

Acá va una vista simplificada de cómo Skia procesa un draw con blend mode:

// Skia interno: procesando un draw con blend mode
void GrOpsRenderPass::draw(const GrProgramInfo& programInfo,
 const GrMesh& mesh) {
 // 1. Chequear si el blend mode es "simple" (puede usar blend de HW)
 if (programInfo.pipeline().getXferProcessor().hasHWBlendEquation()) {
 // Configurar estado de blend de función fija
 this->setHWBlendState(programInfo.pipeline());
 this->issueDrawCall(mesh);
 }
 // 2. Chequear KHR_blend_equation_advanced
 else if (fGpu->caps()->advancedBlendEquationSupport()) {
 this->setAdvancedBlendEquation(xferMode);
 glBlendBarrierKHR(); // Barrera de coherencia requerida
 this->issueDrawCall(mesh);
 }
 // 3. Fallback: leer dst en shader
 else {
 // Bindear framebuffer actual como input de textura al shader
 this->bindDstTexture(programInfo);
 // Fragment shader lee dst, calcula blend, escribe resultado
 this->issueDrawCall(mesh);
 }
}

El backend Graphite de Skia (el sucesor de Ganesh) toma un approach fundamentalmente diferente: construye un render graph del frame completo antes de enviar cualquier trabajo a la GPU. Esto le permite:

  • Batchear draw calls más agresivamente
  • Eliminar cambios redundantes de render target
  • Programar trabajo GPU más eficientemente en APIs modernas (Vulkan, Metal, Dawn)

Benchmarkeé Graphite vs Ganesh en un workload de compositing complejo (50 capas superpuestas con varios blend modes, 1080p):

Ganesh (backend GL): 8,2 ms/frame (~122 fps)
Graphite (Vulkan): 3,1 ms/frame (~322 fps)
Graphite (Metal M2): 2,4 ms/frame (~416 fps)

El approach de render graph es particularmente efectivo para blend modes porque puede batchear todos los elementos que usan el mismo blend mode en un solo render pass, reduciendo cambios de estado.

Vello: El Futuro Son Compute Shaders

Vello (antes piet-gpu) toma un approach radical: hacer todo en compute shaders. Sin pipeline de rasterización, sin blending de función fija. Todo el pipeline de renderizado 2D — aplanamiento de paths, expansión de strokes, tiling, rasterización fina, compositing — corre como una serie de dispatches de compute shader.

// Etapas del pipeline de Vello (simplificado):
// 1. Encoding de paths: curvas Bézier → segmentos de línea (flatten)
// 2. Binning: asignar segmentos de path a tiles
// 3. Rasterización gruesa: construir listas de comandos por tile
// 4. Rasterización fina: ejecutar comandos por tile, output de píxeles

// El kernel de rasterización fina maneja blending inline:
// (pseudocódigo WGSL)
@compute @workgroup_size(256, 1, 1)
fn fine(@builtin(global_invocation_id) gid: vec3<u32>) {
 let tile_xy = gid.xy;
 var rgba = vec4<f32>(0.0); // acumulador local del tile

 for (cmd in tile_commands[tile_xy]) {
 switch cmd.tag {
 CMD_FILL: {
 let src = compute_coverage(cmd);
 rgba = blend(src, rgba, cmd.blend_mode);
 }
 CMD_STROKE: { /* similar */ }
 CMD_IMAGE: { /* texture sample + blend */ }
 }
 }

 textureStore(output, tile_xy, rgba);
}

La ventaja: como Vello controla todo el pipeline, los blend modes son simplemente code paths diferentes en el shader de rasterización fina. No hay "fast path" vs "slow path" — todos los blend modes tienen las mismas características de performance. Y como todo corre en compute, no hay overhead por cambio de estado al switchear blend modes entre draw calls.

Mis benchmarks comparando Vello con Skia/Ganesh en contenido SVG pesado en paths (10.000 paths con varios blend modes):

Skia Ganesh (GL): 12,4 ms
Skia Graphite (Vk): 5,8 ms
Vello (Vk compute): 2,1 ms

Vello todavía es experimental y no maneja todos los edge cases (rendering de texto, stacks complejos de clip), pero para contenido pesado en paths y blending ya es significativamente más rápido.

Profiling GPU Práctico para Contenido Web

Cuando algo renderiza lento, este es mi workflow de profiling:

Paso 1: Inspección de capas

// En la Consola de Chrome DevTools:
// Forzar bordes de capas
document.querySelectorAll('*').forEach(el => {
 const style = getComputedStyle(el);
 if (style.willChange !== 'auto' ||
 style.transform !== 'none' ||
 style.opacity !== '1' ||
 style.mixBlendMode !== 'normal' ||
 style.filter !== 'none') {
 console.log(el.tagName, el.className, {
 willChange: style.willChange,
 transform: style.transform,
 opacity: style.opacity,
 mixBlendMode: style.mixBlendMode,
 filter: style.filter
 });
 }
});

Paso 2: Captura de trace GPU

# Tracing GPU de Chrome
google-chrome --enable-gpu-benchmarking \
 --enable-tracing=gpu,viz,cc \
 --trace-startup-file=gpu_trace.json \
 "https://tu-pagina.com"

# Analizar con chrome://tracing o Perfetto
# Buscar:
# - Duraciones de "RasterTask" > 4ms (tiles lentos)
# - "DrawQuad" con blend_mode != NORMAL
# - "GLRenderer::DrawContentQuad" como indicadores de readback

Paso 3: Estimación de ancho de banda del framebuffer

// Estimar costo de compositing basado en tamaños de capas y blend modes
function estimateCompositingCost(layers) {
 let totalPixels = 0;
 let readbackPixels = 0;

 layers.forEach(layer => {
 const pixels = layer.width * layer.height;
 totalPixels += pixels;

 // Blend modes no separables pueden disparar readback
 const expensiveBlends = [
 'hue', 'saturation', 'color', 'luminosity'
 ];
 if (expensiveBlends.includes(layer.blendMode)) {
 readbackPixels += pixels;
 }
 });

 // A 4 bytes/pixel, 60fps:
 const bandwidthGB = (totalPixels * 4 * 60) / 1e9;
 const readbackGB = (readbackPixels * 4 * 2 * 60) / 1e9; // 2x por read+write
 return { bandwidthGB, readbackGB };
}

La Trampa del Compositing

Esto es lo que me mordió con esa animación SVG original, y lo que ahora veo en todos lados: los desarrolladores aplican blend modes sin darse cuenta del costo downstream en la GPU. Un mix-blend-mode: multiply en un elemento grande no solo cambia cómo se combinan los colores — puede forzar la creación de un render target intermedio (buffer offscreen), un readback del framebuffer por píxel, y la imposibilidad de mergear esa capa con capas adyacentes.

La regla que uso ahora:

  1. Blend modes separables (multiply, screen, darken, lighten): Generalmente acelerados por hardware. Están bien en elementos de tamaño razonable.
  2. Blend modes no separables (hue, saturation, color, luminosity): Frecuentemente disparan fallback a software o paths de shader costosos. Evitar en áreas grandes.
  3. Cualquier blend mode en un elemento grande: Fuerza una asignación de textura intermedia proporcional al área en píxeles del elemento. Preferir aplicar blend modes a elementos más chicos, pre-compuestos.
  4. Blend modes anidados: Cada nivel de anidamiento puede crear una textura intermedia adicional. El costo de memoria se multiplica.

El pipeline de renderizado del browser es una de las piezas de software de gráficos en tiempo real más sofisticadas corriendo en hardware de consumo. Entenderlo a nivel de GPU me hizo un desarrollador web fundamentalmente mejor — no porque esté haciendo programación de GPU, sino porque ahora puedo predecir qué va a ser rápido y qué va a ser dolorosamente lento. Y la predicción supera al profiling cuando se está tomando decisiones de diseño.

gpurenderizadoblend-modescompositingskiavellotile-based-renderingbrowserpipeline-gráfico

Herramientas mencionadas en este artículo

CloudflareProbá Cloudflare
VercelProbá Vercel
Divulgación: Algunos enlaces en este artículo son enlaces de afiliado. Si te registrás a través de ellos, puedo recibir una comisión sin costo adicional para vos. Solo recomiendo herramientas que uso y en las que confío personalmente.
Compartir
Seguime