Para esto nos basaremos en el mejor y mas lento de los algoritmos de rendering que se han creado: Ray Tracing. Este algoritmo tiene algunas caracteristicas importantes: realiza una gran cantidad de calculo por cada punto de la pantalla (2d) que va a generar a partir del un universo 3D dado, y que el calculo de cada punto es totalmente independiente del resto de los puntos. Esto implica que podemos paralelizar el proceso de rendering tanto como queramos. Entonces, las características que queremos (inicialmente) para nuestra máquina son:
La zona que contiene las definiciones de objetos será constantemente accesada por las RPU's, por lo que a éstos últimos les agregaremos cachés de objetos. Como sabemos que la información es solo leída por las RPU's no debemos complicarnos mucho con las posibles colisiones de modificación (no necesitamos semaforos ni nada por el estilo para mantener integridad, por lo menos en lo que se refiere a las RPU's). En el unico caso en que esto cambia es cuando el programa principal necesita actualizar los objetos en la RAM (ie: leer desde la unidad de I/O) en cuyo caso se detiene el funcionamiento de las RPU's.
Otra área de la memoria (que puede ser independiente del área de memoria de objetos) se encargará de contener al "programa" de esta máquina, el cual será ejecutado por la CPU. Necesitamos, tambien, memoria para almacenar la imagen ya procesada (o las imagenes en caso de crear animaciones). Obviamente necesita ser accesada por el módulo de I/O.
Asumiremos que el tiempo traspaso de información (de un pixel) de RPU a memoria es despreciable con respecto al tiempo de calculo, por lo que usaremos un solo bus de salida para todas las placas de RPU's, controlado por un administrador, y llevado hacia un Image Buffer previo al pre-proceso a que será sometida la imagen (para generar la version final).
El aplicar filtros "pesados" podria formar un cuello de botella en el sistema. Esto depende en gran medida de la cantidad y calidad de RPU's y de la cantidad y complejidad de los filtros. Por lo tanto es bastante deseable que nuestro procesador gráfico sea suficientemente poderoso. De hecho, es el pedazo de hardware mas poderoso y complicado del sistema: necesita acceder al Image Buffer para detectar los pixeles que han llegado, hay que calcular los algoritmos de los filtros, en donde se puede necesitar mucho calculo de punto flotante. Debe ser programable y controlable por la CPU del sistema. Debe tener acceso a la RAM de la máquina para almacenar la imagen definitiva, etc.
A este procesador le llamaremos nuestro GPU ("Graphics Processor Unit"), y su diseño depende
en grán medida de pruebas empíricas, las cuales, obviamente, no podemos realizar todavía.
Un analisis de los algoritmos que deseamos aplicar podría llevarnos a decidir agregar
otro procesador poderoso que ayude en el calculo. Por ejemplo: En las imágenes 2D que se
generan a partir de mundos 3D se genera un efecto indeseable llamado "Aliasing". Existe un
algoritmo que lo elimina (llamado anti-aliasing) pero que requiere gran cantidad de calculo.
En particular, es complicado decidir en que parte de la imagen se debe aplicar. Por lo tanto
podríamos tener un procesador calculando donde aplicar anti-aliasing, y otro ejecutando el
algoritmo paralelamente.
Otro punto importante es que el diseño del image buffer debe ayudar a la GPU a detectar los pixeles que han cambiado desde el ciclo anterior. Un método posible es "organizar" el buffer como una matriz de pixels (que equivalen a la imagen) y agregar checksums en las columnas y filas, para poder detectar cuando y donde se produjo algún cambio.
Nuestro (o nuestros) GPUs estarán conectados (como veremos mas adelante) al bus de datos
en que cuelgan la CPU, el módulo de I/O y obviamente la RAM.
Aqui podemos ver un diagrama completo del sub-sistema de post-procesamiento.
La CPU debe coordinar el correcto funcionamiento del sistema. Debe decirle a los RPU's cuales pixeles renderear, y debe decirle a la GPU cuales efectos calcular. Por lo tanto, su misión es cada cierto tiempo dar "ordenes", lo cual practicamente no hace uso del bus de datos. En ciertos momentos debe hacerlos: para traer a su caché de instrucciones el trozo de programa que se está ejecutando, para modificar los objetos con el paso del tiempo (para hacer animaciones), y prácticamente nada mas. Como tenemos tanto tiempo libre existe la posibilidad de aprovechar la CPU ayudando a la GPU a realizar ciertos calculos, por ejemplo interceptar los pixeles que van hacia la RAM, hacer un procesamiento de color, y luego escribirlos en la RAM. Con estos procesos simples (filtros de color sencillos) es posible coordinarse sin muchos problemas y no agota demasiado el bus de datos.
El módulo de I/O puede compartir el mismo bus de datos en el caso que no necesitemos
aplicaciones real-time. Es decir, si no nos molesta que la generación de imagenes para
una animación se tome su tiempo al escribir a disco, entonces podemos dejar que el módulo de I/O use
el bus de datos para leer de la memoria la imagen procesada y la almacene (y/o la muestre
en pantalla). En caso de necesitar aplicaciónes en real-time (juegos, y algunos otros)
exigiendonos mostrar en pantalla los resultados apenas se generan, entonces
necesitaremos un bus dedicado (una especie de DMA) para acceder a la memoria.
Para estos casos debemos tener un doble buffer en RAM para poder mostrar por pantalla
la última imagen generada, mientras que calculamos la siguiente. Esto significa
una RAM mas grande y un bus dedicado. Hay que destacar que para este tipo de
aplicaciones el módulo de I/O solo haria lectura en la RAM, por lo tanto
seguiriamos teniendo pocos conflictos de coordinación (solo la CPU y la GPU escriben).
En caso que el módulo I/O quiere escribir (cosa poco frecuente) podemos detener
temporalmente el acceso a la RAM. Otra alternativa es que el módulo de I/O se cuelgue
del bus de datos y que agreguemos otro periférico adicional que se encargue del
despliegue: una tarjeta de video con acceso directo a la memoria.
A continuación tenemos los diagramas con y sin DMA del módulo de I/O. Podemos ver la versión con tarjeta de video y además un pequeño ejemplo de double buffering.
El Diagrama
Luego de ver como funciona cada parte, podemos hacernos una idea
de como se ve mas seriamente nuestra máquina de rendering. Notar las
distintas versiones, correspondientes a optimizaciones que,
aunque mejoran el performance, incrementan el costo y la complejidad.
Comportamiento Esperado
Con la tecnologia actual no seria dificil construir esta máquina. De hecho
son pocas las cosas que tendríamos que "crear".
Las RPU's pueden ser procesadores multi propósito que se coordinan bien denstro de
las placas. La CPU también puede ser cualquiera en el mercado (no necesitamos
que sea poderosa). Incluso la GPU puede ser un chip multipropósito
suficientemente poderoso, utilizando el bus de direcciones para detectar los
cambios en el Image Buffer.
Prácticamente lo único que necesitamos es ensamblar todo y crear nuestra
memoria "matricial" para el image buffer, usando para la memoria central
cosas como SDRAM con una buena asministración.
PERO eso nos llevaría a un deterioro del performance, y si necesitamos aplicaciones en real-time, tendremos que optar por componentes especificos (ver optimizaciones). Si utilizamos componentes suficientemente rápidos, mas una buena cantidad de memoria y una tarjeta de video dedicada, podríamos llegar a cumplir el sueño de obtener animaciones generadas por Ray Tracing en tiempo real, es decir por lo menos 30 fps (30 imagenes por segundo).
En caso que solo nos interese generar las animaciones para almacenarlas en disco (como sucede con la mayoria del harware gráfico serio (silicon graphics, etc)), podemos acelerar mucho el proceso con respecto a las máquinas existentes, y obviamente con una mejor calidad. El poder probablemente pueda compensar el costo que significa una arquitectura especializada.
Un punto importante en cuanto a desempeño es el hecho de que nuestra máquina sea escalable: esto nos permite tener máquinas con un decente precio de entrada, a la cual se le puede aumentar el poder sin muchos preoblemas. En el siguiente punto veremos esto.
Algunas Optimizaciones
Inevitablemente en el momento en que se diseñó esta nueva arquitectura se tuvieron
en consideracion las posibles optimizaciones y se aplicaron de inmediato, ej: el doble
bus de las placas de RPU's, o sus cachés. Por lo tanto no queda mucho por decir.
Sin embargo hay algunos aspectos que tienen que ver mas con la implementación que con
el diseño. Aquí nombraremos algunos:
El costo de la coordinación haria que el desempeño logrado no sea tan bueno como esperamos. Ahora, siempre existe la alternativa de crear harware específico para cada filtro, y que estos se monten en una línea de post-procesamiento (serialmente) con memorias intermedias. Esta solucion es MUY cara, pero poderosa.