Antes de nada hemos de aclarar lo que es el Blending y no es otra cosa que la etapa final del pipeline gráfico, la que se encuentra entre el texturizado y la generación de la Imagen Final o uno de los Render Targets.

c2_pipeline

De esta etapa final se encargan los Raster Outputs… o ROPS. Si habéis estado atentos veréis que en todo este tiempo los ROPS no hemos visto una unidad shader asignada o se encuentran desconectados de las unidades shader por lo que su trabajo no es programable en muchas arquitecturas y se sigue realizando bajo un pipeline de función fija. En realidad esta función no ha sido implementada en ninguna API gráfica jamás de manera oficial… ¿Entonces que sentido tiene hablar de ello? Luego comentare el motivo de ello, pero os adelanto que tiene que ver con su adopción en las APIS gráficas.

En todo caso, os recomiendo leer la siguiente explicación acerca del motivo por el cual dicha etapa sigue siendo no-programable y sus posibles soluciones.

¿Como es que no tenemos mezclas completamente programables?

Todo el mundo que escribe código de cara el renderizado se pregunta lo siguiente, el pipeline convencional para el blend es un costoso de trabajar algunas veces. ¿Así como que  no tenemos un blog completamente programable? Tenemos el shading completamente programable después de todo! Bien, ahora tenemos el framework necesario para mirar a esto de manera más adecuado. Hay dos proposiciones principales por lo que he visto, vamos a verla ambas.

  1. Mezcla en el Pixel Shader: El  Pixel Shader lee el framebuffer, realiza la ecuación de mezcla, escribe un nuevo valor de salida.
  2. Unidad programable de Mezcla – “Blend Shaders”, con un subcojunto de todas las instrucciones shader si es necesario. Ocurriendo en una etapa posterior al Pixel Shader.

1. Mezcla en el Pixel Shader

Esto parece pan comido. Después de todo, ¿tenemos cargas y muestras de texturas en los shaders, cierto? ¿Así que, por qué no permitir leer el actual render target? Resulta que las lecturas sin restricciones son una idea realmente mala, debido a que cada pixel siendo. ¿Así que si yo referencio un pixel en el quad a la izquierda? Bien. un shader para ese quad se estaría ejecutando en ese instante. O podría er que estuviese sampleando la mitad de mi actual quad y la otra mitad de otro quad actualmente activo. ¿Que es lo que hago ahora? ¿Cuales serían los resultados correctos en ese sentido, sin contar que probablemente tendremos ques sombrear todos los quads de manera secuencial con tal de llegar a ellos de manera fiable? No, eso es una lata de gusanos. Las lecturas no restringindas desde el Framebuffer en los Pixel Shader están fuera.

Nota de Urian: Este es el motivo por el cual hay una cache de texturas donde están conectados los shaders. El hecho de que pudiesen acceder a la memoria principal darían muchos problemas tanto a nivel de rendimiento como arquitecturalmente.

¿Pero que ocurre si obtenemos una instrucción especial de lectura del render target que tome una muestra de uno de los render targets activos en la localización actual? Ahora, esto es un poco mejor, ahora solo tenemos que preocuparnos sobre las escrituras a la localización del quad actual, lo cual es un problema que se puede tratar mejor.

Sin embargo, aún introduce restricciones de ordenación; tenemos que comprobar todos los quads generados por el rasterizador contra los quads actualmente en el pixel shader. Si un quad generado por el rasterizador quiere escribur una muestra que esta escrita por uno de los Pixel Shaders actualmente en vuelo, necesitamos esperar a que el PS esta completado antes de enviar un nuevo Quad. Esto no suena muy malo… ¿Pero como le hacemos seguimiento a esto? Podríamos tener simplemente un bit donde pudiesemos marcar «esta muestra esta actualmente siendo sombreada»… ¿Así que cuantos bits necesitariamos? A 1920x1080P con MSAAx8 unos 2MB de ellos (esto son bytes no bits) y esta memoria es global, compartida y determina la velocidad a la que podemos enviar nuevos quads (desde que necesitamos marcar un quad como ocupado antes de enviarlo). Lo peor es que con los bits del Hierarchical Z… Estos son solo una pista. Si nos quedamos sin ellos, aún podemos renderizar, aunque mucho más lentamente. Pero esta memoria no es opcional. ¡No podemos garantizar que todo este correcto a no ser que hagamos un seguimiento de cada muestra! ¿Que ocurre si hacemos seguimiento del estado «ocupado» del pixel (o incluso el quad), y hacemos cualquier escritura a un pixel que bloquearia todas las otras escrituras? Esto funcionaría, pero acabaría afectando muy negativamente nuestro rendimiento con el MSAA. Si hacemos un seguimiento por muestra, podemos sombrear los triangulos adyacentes que no se solapan en paralelo, no hay problema. Pero si hacemos seguimiento por pixel (o una granularidad menor), nososotros efectivamente serializamos todos los borees de los quads. ¿Y que ocurre con nuestra tasa de relleno con montones de re-dibujados? Con el pipeline que he descrito, ese render (más o menos) tan rápido comos los ROPS se puede unir a los pixeles siguientes en búfers de almacenamiento. Pero si necesitamos evitar conflictos, necesitamos sombrear las particulas que se solapan en orden. Esto no son buenas noticias para nuestras unidades shader que están diseñadas para intercambiar latencia por rendimiento, de ningún modo.

Vale, así que todo esto del seguimiento es un problema. ¿Que ocurre si forzamos al shading a ejecutarse en orden? Esto es, mantenerlo todo en pipeline y todos los shaders ejecutandose en lockstep. Ahora no necesitamos hacer seguimiento debido a que los pixeles se van a terminar en el mismo orden en el que los hemos puesto en el pipeline. Pero el problema aquí es que tenemos que asegurarnos que los shaders en un lote siempre arden el mismo tiempo, lo cual tiene consecuencias desafortunados: Siemore vas a tener que esperarte el peor caso de tiempo de espera cada cada muestra, tienes que ejecutar ambos lados en cada muestra, siempre ejecutar los mismos loops a través de la misma cantidad de interaciones, no se puede parar el shading en el descarte… No, esto no parece un ganador tampoco.

Ok, tiempo para la verdad: La mezcla en los Pixel Shaders que he describo viene con una problemas seriamente truculentos. ¿Asi que hay del segundo planteamiento?

2. “Blend Shaders”

I’ll say it right now: This can be made to work, but…

Lo dire ahora: Esto puede ser hecho para funcionar, pero…

Digamos que tiene sus propios problemas. Por una vez, necesitamos otra ALU completa+descodificador/secuenciados de instruciones etc. En los ROPS. Lo cual no es un pequeño cambio en esfuerzo de diseño, area y consumo. Segundo, como he mencionado al principio de este post. Nuestras tacticas regulares de «Justo ves a lo ancho» no funcionan muy bien para las mezclas, esto es porque esto es un lugar donde puedes tener un monton de quads que se encuentre en el mismo pixel en una fila y necesitas procesarlos en orden, así que lo que quieres es una latencia baja. Esto es un punto de diseño diferente que nuestros unidades de shader unificados. Por lo que no podemos utilizarlos para ello (también significa que el muestreo de texturas/acceso a la memoria en los Blend Shaders es un gran no, pero dudo que alguien le pille por sorpresa en este punto). Así que necesitamos convertirlo en un pipeline (dividirlo por etapas). ¡Pero para hacerlo necesitamos saber lo largo que es el pipeline! Para una unidad de mezcla habitual, la cual tiene una longitud fija, esto es fácil. Un Shader de Mezcla/Blend Shaer probablemente ería lo mismo. De hecho debido restricciones de diseño, es muy poco probable obtener un blend shader, más bien un blend register combiner, realmente con un (presumiblemente relativamente baja) limte superior en el número de instrucciones determinado por la longitud del pipeline.

Por lo que, la ejecución en seríe aquí realmente nos restringe a diseños que sean relativamente de bajo nivel; alejados de las unidades shader completamente programables que amamos. Una unidad de mezcla más buens con modos de mezcla adicionales, donde puedas definitivamente conseguir un diseño más abierto de combinación de registros, posiblemente, pese a que ni los chicos de las APIs ni tampoco los del hardware les guste mucho (los de la API porque es un bloque de funciónfija, los del hardware porque esto es grande y necesita una gran ALU y lógica de control donde ellos preferirían qje no esuviese). No vamos a ver nada que sea completamente programable (con ramas, loops, etc). En este punto tienes que morder la bala y ahcer lo que sea necesario para que el escenario «Mezcla en los Pixel Shaders» funcione correctamente.

Si, se que a muchos que han leido esto el texto la cita les ha sonado a:

MortadeloChino.PNG

Pero la informacion que contiene es sumamente valiosa. La conclusión es que basicamente implementarlo de una u otra forma es un dolor de huevo que requiere cambios importantes en la arquitectura con tal de no perder rendimiento. ¿Y en que consisten estos cambios? Pues en el hecho que para poder aplicar de manera correcta el Blend Shader necesitamos tener ordenados los datos en el búfer de imagen… ¿Acaso no sale la imagen ordenada? A vosotros os parece que sale ordenada pero los polígonos/fragmentos realmente son procesados de manera desordenada y se ordenan durante la fase de ordenación en la mayoría de GPUs, aunque esto esta cambiando.

¿Que significa esto?Esto son cantidades ingentes de puertas lógicas y cableado para solventar el problema en las GPUs que no renderizan por tiles sino a pantalla completa, los Tile Renders como es obvió operan con búfers de imagen mucho más pequeños por lo que aplicar un Blend Shader en ellos es mucho más sencillo.

¿Pero a que viene el interés? Simple y llanamente que los Blend Shaders se encuentran en el mapa de ruta del Shader Model 6.0 y esto tendrá consecuencias en futuras GPUs.

PromBlend.PNG

Fijaos como no es descrito como Blend Shader sino como «Programmable Blending» y nos lo coloca como la capacidad de leer los Render Targets. Esto que puede parecer una tontería tiene una utilidad muy grande ya que ciertos efectos dejarían de requerir de formar parte del post-procesado para aplicarse con lo que el tiempo de renderizado de las escenas aumentaría.

¿Pero como podemos solventar este problema? Transformando las GPUs que no son Tile Renderers en eso para poder aplicarlo al menos a nivel Fragment/Pixel Shader. Por el momento ya existe una API gráfica que aplica el Blending Programmable desde el Fragment/Pixel Shader y esto significa poder aplicar efectos de post-procesado sin que haya un impacto en la memoria y obviamente tiene que ver con un Tile Renderer.

Dicha API es Metal de Apple, antes de la implementación en su API propietaria dejaron bajo el nombre de  GL_APPLE_shader_framebuffer_fetch una extensión al OpenGL (y por tanto al GLSL) de esa misma funcionalidad aunque dicha extensión no se ha oficializado en ninguna version de OpenGL no me extrañaría que una version Direct3D de esto fuese lo que vamos a ver en el Shader Model 6.0, especialmente por el hecho de que la implementación a nivel de hardware que parece la más adecuada para ello es la de convertir en Tile Renderers las GPUs que no lo son y eso conlleva una serie de cambios importantes.

En primer lugar hemos de asegurarnos que la GPU rasterice por tiles y mantenga el texturizado del tile en una memoria a donde los ROPS tengan acceso. En el caso de las GPUs de AMD nos encontramos conque los datos que llegan a los RBEs/ROPS no pueden ser accedidos por los shaders y estos una vez descargados se descargan sobre la memoria principal, esto es por como esta cableado el acceso a la memoria y a la Cache L2 en dichas GPUs y esto incluye todas las consolas hasta PS4 pro (Xbox One X la dejamos de lado hasta que se nos revele más de ella).

rbe480x

rbebig

¿La solución a ello? Hacer que los ROPS/RBE estén conectados a los Shaders, pero la sobrecomplicación de ello sería enorme por lo que todo se resume en volcar el resultado de los ROPS en la cache L2 y acceder desde allí, pero volcar un búfer de imagen entero a las resoluciones que se usan hoy en día y se usaran en el futuro es algo practicamente inviable, de ahí a que se haya empezado a hablar de Tile Rendering pero dicha evolución ha empezado con la rasterizacion por Tiles.

¿La primera arquitectura en implementarlo? Lo hizo la arquitectura Maxwell de Nvidia hace un par de años y lo ha continuado la arquitectura Pascal de la misma compañía.

tiledcaching1tiledcaching2tiledcaching3

Sabemos que AMD implementa esto a partir de la arquitectura Vega.

l2vega

¿El problema de esta implementación? No es lo mismo que un Tile Rendering por el hecho de que carece de un Tile Buffer, es un primer paso a ello. ¿Que es lo que hace un Tile Buffer? Basicamente en un Tile Renderer lo primero que hacemos es un renderizado de la escena sin tener en cuenta el texturizado, tras el cual hacemos el test de ordenamiento y almacenamos el resultado en memoria para re-invocarlo de nuevo para esta vez con la escena dividida en Tiles y con una lista de pantalla de que hacer con esa parte de la escena. Dichos Tiles son cargados en una memoria interna llamada Tile Buffer y son procesados desde alli con la ventaja del enorme ahorro en el ancho de banda.

Los Tile Renderers son diferentes  a la hora de ordenar la informacion de la escena, ya que en vez de hacerlo al final del renderizado…

sortlast

… lo hacen en medio.

sortmiddle

En un Tile Renderer la ordenacion realmente se hace al final, lo que ocurre es que la primera etapa del renderizado es para generar el Tile Buffer y de ahí se aprovecha para ordenar la geometría y eliminar toda aquella no-visible. ¿El único problema con ello? La geometría detrás de un elemento con una textura transparente o semi-transparente, pero esto se arregla con un simple bit tag.  En cambios en los rasterizadores por Tiles no se esta aplicando la ordenación por el hecho que antes necesitamos tener la escena ordenada… ¿Entonces?

tilevega

Lo que hay en Maxwell en adelante y en Vega es el hecho de que al rasterizar por Tiles se puede aplicar dentro del Tile actual la comprobación de la geometría y eliminar la que esta dentro de cámara pero que se puede ver en el fotograma, reduciendo así la carga en las etapas posteriores del pipeline.

¿Cual es el próximo paso lógico? Pues aplicar un Tile Buffer y la capacidad por tanto de crear un mapa de la geometría de la escena para luego ir recuperando. ¿Y como se haría? Hay una consola que tiene en su hardware la clave de cara al futuro, pero esta consola carece en su hardware de la otra mitad (el rasterizador por tiles, el acceso a la Cache L2 u otra cache en la que los ROPS puedan volcar su resultado).

ps4pro

En los Tile Renderers una vez almacenada nos permite no tener que recalcular la geomeria de la escena, en el caso de PS4 Pro tenemos una solución salomónica en forma de la implementación para poder realizar el Checkerboard Rendering para el re-escalado al 4K, la generacion de un ID Buffer.

El ID Buffer en realidad no deja de ser un Z-Buffer extendido, la evolución a un Tile Renderer lo va a implementar y basicamente el renderizado será de la siguiente manera:

  1. Renderizado de la escena sin tener en cuenta la etapa de renderizado de manera convencional, se pasa de la geometría al rasterizado y de ahí al bufer de imagen.
  2. Durante el proceso de rasterización el búfer de imagen que se crea va siendo ordenado el vuelo y se elimina la geometría sobrante.
  3. La escena se almacena en forma de ID Buffer colocando no solo un tag en la geometría sino también en cada Tile.
  4. Se va recuperando cada Tile con su lista de pantalla correspondiente para renderizar.

Pero lo que tendríamos sería una especie de TBR.¿Que es lo que nos interesa para el Blending Programable? Pues el hecho de que sea un TBDR y por tanto pueda renderizar por diferido y por Tiles. ¿El motivo? Una de las ventajas de los TBDR es de cara al renderizado por diferido por el hecho de que pueden calcular la segunda etapa que es la iluminación sin tener que re-escribir en memoria y volver a recuperar los datos. Precisamente esa es una de las ventajas que vamos a ver del «Programmable Blending», la enorme reduccion sobre el ancho de banda que va a tener el renderizado en diferido y con ello la reducción en el tiempo de renderizado.

¿El problema? Hasta ahora la solucíón es volcar los Tiles sobre la Cache L2 general, el problema de dicha cache L2 es que sirve tanto para los datos del pipeline de computación como los del pipeline gráfico. Y he aquí un elemento que mucha gente no sabe, el camino de datos del pipeline de computación y el pipeline gráfico no es el mismo. Como os lo explico… Ah si…

ShaderEngine

Esto es el diagrama de un shader engine de una GPU de AMD, la linea vertical violeta al lado de cada grupo de 3 o 4 CUs es la Cache L1.  Esto se ve mejor en el siguiente diagrama:

GCNL1

Como se puede ver, dicha cache esta conectada a la Cache L2 del sistema, la misma cache a la que NO ESTÁN conectados los ROPS/RBE pero lo estarán en Vega por lo que esta en camino de datos del pipeline de computación. Pero resulta que tenemos en el sistema otra cache L1 que esta conectada a las unidades de texturas (estas no actuan en el pipeline de computación) y por tanto forman parte del pipeline gráfico.

kaveri-14b

La primera solución que han propuesto es añadir un nivel de cache L2 que no es otra cosa que conectar los ROPS a la cache L2 pero realmente desde el punto de vista tecnico no es una Cache L2 de la cache de texturas. Lo ideal sería que debajo de la Cache L1 de texturas hubiese una cache L2 con el mecanismo de los ROPS/RBEs asociado. Dicha Cache L2 sería una cache completamente privada del pipeline gráfico, no sería general y se aplicaría a nivel de cada RBE/ROP y si tenemos en cuenta que cada RBE/ROP esta asociado a 4 CUs y en total son 16 unidades de texturas entonces estaríamos hablando de procesar un Tile de 16×16 pixeles en total desde el cual se podría aplicar sin problemas el Blending Programable.

Y no, no es ninguna locura.

tbdr-pipeline-1

Basicamente sería el On-Chip Color Buffer+On Chip Depth Buffer. algo que en Vega se hace bajo la Cache L2 general sin llegar a tener que compartir el acceso con ella y con la posibilidad de poder recuperar mucho más facilmente los datos de cara al Tile Rendering, pero hay varios motivos para ese cambio:

  1. La Cache L2 general esta mucho más alejada en la topología del chip que la solución que estoy proponiendo por lo que la aplicación de los algoritmos tiene un retraso.
  2. No nos podemos asegurar que todas las Tiles y la información para computación estén en la Cache L2 General.
  3. Demasiados Clientes y demasiada contención en la Cache L2 general.

Es por ello que pienso que vamos a ver estos cambio a nivel de hardware en un sistema del futuro y muy probablemente en una siguiente generación de consolas de sobremesa.

Todos estos cambios llevarán a la implementación del Tile Rendering como forma de renderizado estandar de las GPUs, siendo el Blending Programable una de las funciones más interesantes que vamos a ver.

Si, si… Esto esta muy bien pero… ¿Que utilidad tiene?

Haciendo la entrada sobre los TMS340x0 me he acordado que había una función que tenían los ROPS de la época, los llamados Blitters de poder manipular los datos realizando una serie de operaciones booleanas muy simples.

BooleanOpsBooleanOps2

Obviamente esto es algo muy simple pero es algo que los ROPS no pueden hacer, son basicamente operaciones donde teniendo un bitmap origen y un bitmap destino los podemos combinar para efectuar ciertos efectos. Esto en el fondo es lo que podemos hacer con las texturas y es lo que hacen los shaders con ellas pero con una aritmética mucho más compleja que la Booleana de los viejos Blitters.

¿Y donde podemos encontrar una version más avanzada de esto? Pues en los modos de mezcla (fusión)/Blend Modes de Photoshop, estos son una buena muestra de lo que se podría hacer a nivel de fotograma con los «Blend Shaders». Por lo que va a suponer un aumento en la calidad de imagen de la «fotografia» de la escena. Os recomiendo leer la siguiente entrada al completo donde explican como funcionan dichos modos para que os hagáis una idea de lo que estoy hablando. La conclusión a la que llegaréis es que para aplicar esto hace faltan las unidades shader.

¿Otras aplicaciones? Desde el momento en que el Tile Rendering lo que hace es poder realizar lo que llamamos los efectos de post-procesado en chip entonces acelera enormemente todos aquellos efectos visuales basados en el búfer de acumulación. En realidad es una forma sutil de hablar de efectos de «post-procesado», solo que dejarían de serlo en este caso dado que no se tocaría la memoria gráfica en el proceso. Precisamente se intento hacer esto (aunque con un pipeline de función fija) ampliando la capacidad de los ROPS, la tecnología era propietaria de 3Dfx y recibió el nombre de T-Buffer aunque luego se estandarizo

¿Las cosas que podía hacer? Basicamente eran efectos sobre un búfer de acumulación (post-procesado) pero es una version muy primitiva de lo que podemos esperar hacer con los Blend Shaders, no lo veáis como un ejemplo de futuro sino como un ejemplo histórico como referencia. La idea del T-Buffer era la misma que luego veriamos en forma de los multiple render targets.

T-Buffer.PNG

Dado que hablamos de Tile Rendering por Diferido para poder aplicar el Blend Shader es un hecho de que esta funcionalidad podría volver con mucha más fuerza. ¿Efectos que se pueden beneficiar de ello?

  • Efectos de Antialiasing espacial, pero aplicados a nivel de Tile… Por ejemplo versiones más eficientes del MSAA.
  • Efectos de Motion Blur y derivados. (En realidad esto no es más que un tipo de Antialiasing)
  • Efectos de Sombras Suaves, el siguiente video donde se ve una Voooo 5 6000 aplicando Soft Shadows en el motor de Quake 3 para simular la iluminación de Doom 3 es impresionante si tenemos en cuenta el tiempo en que fue lanzado y la perspectiva (el procesador no soportaba shaders).
  • Efectos de Reflejos Suaves. donde se nos permite controlar la forma en la que se refleja la luz según el material variando ciertos valores según la naturaleza del mismo. Un ejemplo de su aplicación lo podemos ver en la primera versión de Mirror’s Edge como efecto de post-procesado. MESoftReflections.PNG

Los «Blend Shaders nos permitirían calcular este tipo de efectos sobre el pipeline gráfico como parte final del mismo y no como parte posterior del mismo. Esto aceleraría el tiempo de ejecución de dichos algoritmos pero al mismo tiempo alargaría el pipeline con lo que sería una forma de enmascarar por un lado aún más la latencia con la memoria (mantener la GPU lo maximamente ocupada) y por otro que estas puedan alcanzar mayores velocidades de reloj al alargar más el pipeline.

Y con esto termino, me gustaría saber de vuestras opiniones sobre el tema, que el post me ha costado mucho montarlo.