El coste real del multimuestreo: memoria, ancho de banda y densidad de triángulos
Por qué la memoria crece exactamente por el número de muestras y el ancho de banda no, qué hace la compresión de color, y en qué escenas el coste de sombreado sí se acerca al del supersampling.
La pregunta que decide si activas el multimuestreo no es si mejora la imagen, que evidentemente sí, sino cuánto cuesta en tu contenido y en tu hardware. La respuesta tiene tres términos que se comportan de forma muy distinta: uno crece exactamente por cuatro, otro casi no crece gracias a mecanismos de compresión que nadie documenta, y el tercero depende de una propiedad de tu geometría que probablemente no has medido nunca.
- Calcular la memoria adicional de un pass multimuestreado.
- Explicar por qué el ancho de banda no crece proporcionalmente al número de muestras.
- Relacionar la densidad de triángulos con el número de ejecuciones del fragment shader.
- Diseñar una medición honesta del coste del multimuestreo en una escena concreta.
La memoria, que sí es lineal
No tiene truco: una textura con cuatro muestras ocupa cuatro veces lo que la misma textura con una.
| Attachments a 1920 por 1080 | 1 muestra | 4 muestras |
|---|---|---|
Color bgra8unorm |
8,3 MB | 33,2 MB |
Profundidad depth32float |
8,3 MB | 33,2 MB |
Color rgba16float |
16,6 MB | 66,4 MB |
| Total de un pass HDR con profundidad | 24,9 MB | 99,6 MB |
Y hay que sumar el destino de la resolución, que es de una muestra pero existe igualmente. En un dispositivo con memoria dedicada de 8 GB, cien megabytes no son nada. En un móvil que comparte memoria con el sistema y donde el navegador tiene un presupuesto de unos pocos cientos de megabytes para todo, sí lo son, y en 4K con rgba16float la cuenta sube a 265 MB solo en el attachment de color.
Esa es la restricción dura y la que decide primero. Si no cabe, el resto de la discusión sobra.
El ancho de banda, que no lo es
La intuición dice que si hay cuatro veces más datos, hay cuatro veces más tráfico. En la práctica el factor está mucho más cerca de uno, y por dos mecanismos distintos según la arquitectura.
En una GPU de modo inmediato, el hardware comprime el búfer multimuestreado. La observación que lo hace posible es que en el interior de un triángulo las cuatro muestras de un píxel tienen exactamente el mismo color, porque el fragment shader se ejecutó una sola vez. La GPU guarda entonces un color y unos metadatos que dicen «las cuatro muestras valen esto», y solo materializa cuatro valores distintos en los píxeles de borde, que son una fracción pequeña de la imagen. El tráfico real acaba siendo del orden de un 10 a un 30% por encima del caso sin multimuestreo, no un 300%.
En una GPU de renderizado por tiles, el mecanismo es todavía más favorable: las cuatro muestras viven en la memoria interna del tile durante todo el pass, la resolución ocurre dentro del chip al terminar cada tile, y lo único que sale a memoria principal es el resultado ya promediado, de una muestra por píxel. Si el attachment multimuestreado lleva storeOp: 'discard', el tráfico a memoria de un pass con cuatro muestras es idéntico al de uno sin multimuestreo.
Eso convierte una intuición muy extendida en falsa: el multimuestreo no es una técnica de escritorio que el móvil no se puede permitir. Es al revés. En hardware de tiles, cuatro muestras es el antialiasing más barato que existe, y por eso viene activado por defecto en tantas aplicaciones nativas de móvil.
El precio que sí se paga en un tiler es el espacio en la memoria del tile: cuatro muestras de color y profundidad ocupan cuatro veces más, y como el tamaño del tile lo decide el driver para que quepa, el tile se hace más pequeño y la fase de reparto de geometría se encarece. El mecanismo completo está en la lección de varios attachments de color, y su consecuencia aquí es directa: multimuestreo y muchos attachments compiten por el mismo recurso, así que un G-buffer gordo con cuatro muestras es lo peor que se le puede pedir a un móvil.
El sombreado, que depende de tu geometría
Queda el tercer término y es el que nadie calcula. El fragment shader se ejecuta una vez por píxel y por triángulo que cubra al menos una muestra de ese píxel. Sin multimuestreo, solo se ejecuta para el triángulo que cubre el centro del píxel.
En un píxel del interior de una superficie eso da lo mismo: un triángulo, una ejecución. En un píxel atravesado por un borde entre dos triángulos, sin multimuestreo hay una ejecución y con cuatro muestras hay dos. El coste añadido es por tanto proporcional a la fracción de píxeles que son píxeles de borde, y esa fracción depende del tamaño en pantalla de tus triángulos:
| Contenido | Triángulos por píxel de borde | Coste de fragmento añadido |
|---|---|---|
| Arquitectura, superficies grandes | pocos bordes | entre 5 y 15% |
| Personajes con detalle medio | bordes frecuentes | entre 20 y 40% |
| Vegetación densa, geometría subpíxel | casi todo son bordes | se acerca al 300% |
El último caso merece subrayarse: cuando los triángulos son más pequeños que un píxel, cada píxel es un borde y el multimuestreo degenera hacia el supersampling con todo su coste. Es la razón por la que las mallas con nivel de detalle exagerado castigan dos veces, y el argumento cuantitativo a favor de tener niveles de detalle agresivos aunque la GPU parezca sobrarte de geometría.
Las guías de rendimiento dan cifras del tipo «MSAA 4x cuesta entre el 10 y el 20%», y esas cifras son medias sobre un conjunto de escenas que no es el tuyo. Lo que hemos visto en esta lección es que el coste tiene tres términos con comportamientos radicalmente distintos —memoria lineal, ancho de banda casi constante, sombreado proporcional a la densidad de bordes— y que el peso relativo de los tres depende por completo de tu contenido y de tu hardware. Un porcentaje único no puede describir eso. Peor todavía: la forma habitual de medirlo, activar el multimuestreo y comparar tiempos de fotograma, es engañosa en las dos direcciones. Si tu aplicación está limitada por CPU, activarlo no cambiará el tiempo de fotograma en absoluto y concluirás que es gratis; el día que optimices la CPU y pases a estar limitada por GPU, aparecerá un coste que llevaba ahí desde el principio. Y si está limitada por ancho de banda a memoria, el multimuestreo puede empeorar mucho más de lo que su coste de sombreado sugiere, porque la textura extra desaloja de la caché cosas que estaban usándose. La medición correcta tiene tres reglas. Primera: mide tiempo de GPU por pass con consultas de marca temporal, no tiempo de fotograma, para saber en qué pass se va el coste. Segunda: mide en el dispositivo objetivo más débil, porque la relación entre potencia de sombreado y ancho de banda varía en un orden de magnitud entre un portátil integrado y una tarjeta dedicada, y esa relación es justo la que decide aquí. Y tercera, la más útil de las tres: mide con la escena real y en el peor plano, no con un modelo de prueba. La diferencia entre un plano general de vegetación y un primer plano de un objeto liso puede ser de un 5% a un 200% del mismo ajuste. Si solo puedes hacer una medición, hazla en el plano que más te preocupe y decide con ese número.
Un apunte sobre mask
El bloque multisample tiene un tercer campo del que casi nunca se habla: mask, un entero de 32 bits que vale 0xFFFFFFFF por defecto y que se combina con la cobertura calculada. Poner 0b0101 con cuatro muestras hace que solo se escriban las muestras 0 y 2.
Su uso interesante no es desactivar muestras porque sí, sino renderizar la misma escena en dos pasadas complementarias —una con las muestras pares y otra con las impares— para técnicas de reconstrucción a resolución variable, o para depurar visualmente qué contribuye cada muestra. Es una herramienta de nicho, pero conviene saber que el campo existe y qué hace, porque su valor por defecto es el único que la mayoría de la gente ve.