En esta entrada voy a explicar porque el simple port de un juego en cualquier plataforma a otra con una GPU más potente y sin optimizaciones supone un desaprovechamiento del hardware, aunque voy a utilizar como ejemplo el port de Wii U a Switch esto es aplicable a otros casos similares donde no hay optimizaciones por el medio.

#1 Kernels (GPU)

Dado que las GPUs tienen centenares por no decir miles de núcleos en algunos casos es importante evitar saturación en las peticiones al bus de memoria. ¿Cómo se hace esto? Pues básicamente enviando la instrucción y los datos a ejecutar de manera conjunta. Aunque eso si, hay información como las texturas que son vulnerables a encontrarse en cualquiera de los niveles de memoria, pero por el momento dejaremos las texturas y nos centraremos en lo que son los kernels, los cuales pueden contener los siguientes tipos de datos:

  • Vertice (Gráficos)
  • Primitiva (Gráficos)
  • Fragmento (2×2 pixeles) (Gráficos)
  • Computación

Los Kernels son empaquetados, procesados y enviados a las unidades shader sin que el desarrollador tenga que hacer nada en ese caso porque es un proceso automatizado dentro de la GPU. Cuando la API genera la lista de dibujado y/o las listas de computación lo que hace es… generar una larga lista que luego será dividida por pequeñas listas a nivel interno cuyo tamaño suele depender de cada fabricante:

  • Imagination (PowerVR): Listas de 32 elementos.
  • Nvidia: Listas de 32 elementos (GeForce 8800 en adelante)
  • AMD: Listas de 64 elementos (Serie Radeon HD 2000 en adelante en PC, Xbox 360, Wii U, PS4 en todas sus variantes, Xbox One y Xbox Scorpio).

Dichas listas reciben diferentes nombres según el fabricante, son Waves en el caso de Nvidia y Wavefronts en el caso de AMD. Me voy a centrar en AMD para simplificar las cosas pero el mecanismo en Nvidia es el mismo.

#2 El Planificador

Antes de entrar en la computación es importante tener en cuenta el Planificador, el cual originalmente en plena era DX9 solo podía con una sola lista de comandos que era para gráficos, no se podían utilizar dos listas por lo que una GPU que servía para gráficos no servía para computación.

dx11-to-dx12_zpsj4qm3yiy

En realidad no era importante porque el Compute Shader no existía, voy a tomar como ejemplo la GPU de la Xbox 360 la cual tiene 3 unidades Shader en su arquitectura.

360ArchCada Shader Pipe esta compuesto por 16 unidades VLIW de la siguiente naturaleza cada una:

thPor lo que realmente estaríamos hablando de esto:

VLIW5SIMDLa GPU tiene 2 ALUs distintas, una ALU se encarga de procesador las instrucciones que vienen en grupos de 4, las cuales son muy comunes en gráficos mientras que la otra solamente opera de manera escalar y para casos especiales. Dado que la GPU soporta 48 instrucciones simultaneas y por tanto una instrucción por unidad VLIW5 no siempre se utiliza todos los shaders de la GPU al mismo tiempo, la única excepción es la FP MAD (FMADD) donde se dan dos instrucciones por ciclo pero solo se usa una de las dos unidades por lo que como máximo en Xbox 360 se utilizará el 80% de la potencia de la GPU en lo que a shaders se refiere y eso si se utiliza solo la parte vectorial, si se combina con la ALU escalar esto baja, por eso la media esta en 3.4 núcleos por VLIW5 según AMD.

Con Wii U que tiene una GPU VLIW5 ocurre lo mismo, solo que en ese caso no tenemos 3 Shader Pipes sino solamente 2.

GPU7BlockDiagramGPU7SIMDDiagramPero en ambos casos nos ha de quedar claro que cada Shader Pipe se va a encargar de un Wavefront y que tiene un total de 80 ALUs (VLIW5*16) y que cada unidad VLIW5 se va a encargar de un kernel del Wavefront por lo que el tiempo mínimo de Wavefront para solventarse será de 4 ciclos ya que el tiempo mínimo por instrucción es de 1 ciclo y tenemos la lista de 64 Kernels que compone un Wavefront dividida en 4 listas de 16.

¿Pero se reparten los Wavefronts de manera simultanea? No, se reparten de manera escalonada:

Wavefront

Cuando hay un solo procesador de comandos gráficos lo que ocurre es que este solo puede enviar un Wavefront por ciclo de reloj, por lo que primero lo enviara al Shader Pipe 0 y al siguiente ciclo al Shader Pipe 1… Esto esta bien cuando tenemos una configuración de pocos Shader Pipes… ¿Pero que ocurre cuando el número de Shader Pipes aumenta y en cambio tenemos un solo procesador de comandos… Obviamente lo siguiente:

Wavefront2

Imaginaos en sistemas con varios Shader Pipes funcionando… El nivel de desuso es enorme por lo que hay que aumentar la cantidad de procesos a ejecutar en paralelo, es ahí donde aparece la ventana para utilizar la potencia sobrante para computación y esto se consigue añadiendo procesadores de comandos adicionales para computación que pueden enviar en cualquier momento e independientemente de la situación de los Wavefronts gráficos en cada momento.

Supongamos por un momento que tenemos una unidades encargada de las listas de comandos para Computación, la cosa quedaría más o menos así:

ShaderCompute

Desde la API podemos crear muchas listas de comandos en computación para aprovechar la potencia de los Compute Shaders en gráficos, estos generalmente se utilizan en los siguientes elementos:

  • Detección de Colisiones y el resto de Físicas de la escena (antes de renderizar la escena como apoyo a la CPU).
  • Efectos de Post-Procesado (AA, Motion Blur, Calculo de la Iluminación en el Renderizado por Diferido…)

Pero para que los Compute Shaders se utilicen estos se han de marcar en el código del juego por lo que si un juego antiguo no los utiliza y no crea listas adicionales entonces no se aprovecha esa característica de las GPUs más modernas. Es decir, los juegos pensados bajo DX9 tienen un nivel de aprovechamiento menor que los juegos pensados APIs más avanzadas y lo mismo podemos decir de sus equivalentes. Es decir, los juegos no escalan de manera lineal porque a medida que vamos aumentando la potencia de la GPU el nivel de desaprovechamiento de la misma con un juego antiguo aumenta enormemente a no ser que aumentemos la carga visual y la mejor forma de hacerlo es aumentar la resolución del juego en el sistema más potente.

#3 Los Shaders han de estar siempre alimentados.

Esto es una perogrullada pero si la lista de comandos es corta entonces el planificador no tiene más elementos a repartir entre los shaders lo que acaba ocurriendo es que al final hay un desuso enorme, esto es sumamente importante para entender porque los ports sin optimizaciones no escalan en ningún momento, un ejemplo reciente de ello es Breath of the Wild.

SwitchZeldaGDC4

Tomando como referencia a Wii U y que es un hardware de AMD tenemos que esta trabaja con 3 Wavefronts (64 kernels cada uno= 192 kernels en total) pero tiene activos solo 2 al mismo tiempo y el tercero en reserva por el hecho que no hay Shader Pipe a procesarlo y por tanto el planificador no lo podrá envíar directamente a los shaders por el hecho que estos están ocupados aunque eso no es problema porque el planificador envía las listas generadas a una memoria global en la GPU que será recogida por cada Shader Pipe cuando este este libre por lo que el planificador no se detiene pero esos Wavefronts quedan a la espera. Esto se ve especialmente cuando el número de Wavefronts (o Waves) que tiene el juego es mucho mayor que la cantidad que la GPU puede procesar.

En el caso de Wii U en general ocurre esto:

WiiuWavefronts

Esto es una manera muy simplificada de representarlo, ahora si queremos trasladar el mismo juego a Nvidia (recordemos que los grupos de kernels son de 32 y no de 64) entonces estamos hablando que la lista de 192 kernels se convierte en una lista de 32 Waves y aquí hay un tema que la gente tiene que saber y es que los Shader Pipes de Nvidia no son de 64 u 80 elementos (actualmente los de AMD son de 64 y por si no os habéis dado cuenta una Compute Unit es un Shader Pipe con otro nombre) sino de 32 elementos cada uno. Por otro lado no nos olvidemos que la GPU del X1 tiene 256 unidades CUDA por lo que estamos hablando de que la GPU soporta hasta 8 Waves distintos, pero lo mejor es verlo de manera gráfica lo que ocurre en este caso:

Desaprovechamiento

Pero en Wii U Nintendo hizo una optimización que no fue otra que la de poder crear varias listas de comandos utilizando los núcleos adicionales de la CPU, en Wii U hay un núcleo reservado para el SO y tenemos 3 núcleos por lo que por descarte tenemos dos listas de comandos distintas funcionando y es aquí donde entramos en una particularidad de Wii U qu aprovecha muy bien Breath of the Wild por la capacidad de poder crear listas múltiples.

zeldaugif2nhb4z

Me estoy refiriendo a la físicas, la GPU de Wii U soporta Compute Shaders aunque de manera muy limitada.

CSWiiU.png

Es decir, tanto PS3 como 360 tendrían problemas para mover el motor de físicas de Breath of the Wild tal cual sin cambios (aunque PS3 con el Cell no tendría problemas si se adaptase bien) pero el limite de 192 hilos sigue estando solo que uno de los Wavefronts se puede dirigir a la computación como mínimo, esto significa que al menos dos Waves en Switch se pueden utilizar para los Compute Shaders que tendrían su propia lista aparte pueden ser enviados desde la CPU de manera simultanea… ¿Cuál es el problema? En el caso de Wii U que esta funciona con una sola lista de comandos a nivel interno pero no es el caso de Switch por lo que la cosa quedaría en teoría de la siguiente manera:

Desaprovechamiento5.png

El Wave 4 y el Wave 5 no han desaparecido sino que son ahora el Wave C0 y el Wave C1.

encuentran activos durante la ejecución desde el primer ciclo, por lo que hemos hecho que la lista de 192 elementos se resuelva en 9 ciclos a que se resuelva en 7 ciclos solamente pero seguimos teniendo un nivel de desaprovechamiento enorme, si Nintendo hubiese optimizado el código para utilizar el núcleo adicional para una lista adicional entonces vemos como dicha optimización no reduce el número de ciclos pero si de aprovechamiento de la potencia de la GPU.

Desaprovechamiento3.png

Pero de un tiempo a esta parte las APIs más avanzadas permiten dividir la pantalla en sub-bloques para tener varias listas de dibujado simultaneas y no solo una, aquí el rendimiento cambia por completo, supongamos que tenemos 2 listas para gráficos y 1 lista para computación:

Desaprovechamiento4.png

Seguimos teniendo el problema de los Shader Pipe 6 y 7 sin ser aprovechados pero ahora se necesitan solo 5 ciclos en total para resolver la lista de 192 elementos por lo que las optimizaciones se hacen importantes de cara a futuros juegos y esto demuestra como los prots de Wii U en Switch no son referencia para la potencia de absolutamente nada por el simple hecho que no ha habido ninguna optimización en el proceso.

#4 1080P

Supongamos que queremos ejecutar el Breath of the Wild a una resolución mucho más alta, como por ejemplo pasar de los 720P a los 1080P, esto es un salto unas 2.25 veces en resolución. Si miramos la configuración de los 192 hilos de Wii U el máximo de hilos para los Fragment/Pixel Shaders es de 124 si se utiliza el Geometry Shader.

captura-de-pantalla-2016-04-29-a-las-13-50-19.png

Suponiendo que mantenemos la geometría (32 kernels/hilos para Vertices y 8 kernels/hilos para primitivas) enonces lo único que aumenta con la resolución son los hilos/kernels de los pixel/fragment shaders.

124*2,25= 279

279+40= 319 aprox 320

Esto son unos 10 Waves distintos a ejecutar en la lista, pero tenemos solo 8 unidades… ¿Cómo lo hacemos? En realidad hay truco y es que una vez todos los grupos de 32 unidades tienen sus listas de 32 kernels se vuelve a empezar desde el principio como si se recorriese un anillo por lo que Wave 8 y el Wave 9 se cargan sobre la primera y la segunda unidad quedando la cosa de la siguiente manera sin optimizaciones de ningún tipo:

10Desp.png

Por lo que pasamos de unos 9 ciclos para resolver a unos 13 ciclos para resolver en el caso que nos ocupa. ¿Y que ocurre si optimizamos para dos listas de comandos gráficos 1 de computación por ciclo? Pues lo siguiente:

10Optimized.png

Pasamos de los 13 ciclos a los 8 ciclos por lo que se hace posible reproducir Breath of the Wild a 1080P sin un cambio profundo del hardware respecto a lo que hay en Swith. Por lo que es muy posible que juegos futuros bajo el mismo motor se puedan reproducir a 1080P (al menos en modo dock ya que en modo portátil por la pantalla no se puede) o en su defecto juegos con mejores gráficos en modo portátil.