Nintendo DS Console GPU y características interesantes


Me gustaría contarle sobre el funcionamiento de la consola GPU de Nintendo DS, sus diferencias con las GPU modernas, y también expresar mi opinión sobre por qué usar Vulkan en lugar de OpenGL en emuladores no traerá ninguna ventaja.

Realmente no conozco Vulkan, pero por lo que leí, está claro para mí que Vulkan difiere de OpenGL en que funciona en un nivel inferior, lo que permite a los programadores administrar la memoria de la GPU y cosas similares. Esto puede ser útil para emular consolas más modernas que usan API gráficas patentadas que proporcionan niveles de control no disponibles en OpenGL.

Por ejemplo, el renderizador de hardware blargSNES: uno de sus trucos es que durante algunas operaciones con diferentes colores de buffer, se usa un buffer de profundidad / stencil. En OpenGL, esto no es posible.

Además, queda menos basura entre la aplicación y la GPU, lo que significa que si se implementa correctamente, el rendimiento será mayor. Si bien los controladores OpenGL están llenos de optimizaciones para casos de uso estándar e incluso para juegos específicos, en Vulkan la aplicación en sí misma debe estar bien escrita en primer lugar.

Es decir, en esencia, "la gran responsabilidad viene con gran fuerza".

No soy un especialista en API 3D, así que volvamos a eso. Lo que sé bien: consola GPU DS.

Ya se han escrito varios artículos sobre sus partes individuales ( sobre sus cuádruples sofisticados , sobre tonterías con viewport , sobre las características divertidas del rasterizador y sobre la sorprendente implementación del suavizado ), pero en este artículo consideraremos el dispositivo como un todo, pero con todos los detalles jugosos. Al menos eso es todo lo que sabemos.

La GPU en sí es un hardware bastante antiguo y obsoleto. Está limitado a 2048 polígonos y / o 6144 vértices por cuadro. La resolución es 256x192. Incluso si cuadruplica esto, el rendimiento no será un problema. En condiciones óptimas, DS puede generar hasta 122880 polígonos por segundo, lo cual es ridículo para los estándares de las GPU modernas.

Ahora pasemos a los detalles de la GPU. En la superficie, parece bastante estándar, pero en el fondo su trabajo es muy diferente del trabajo de las GPU modernas, lo que hace que la emulación de algunas funciones sea más complicada.

La GPU se divide en dos partes: un motor de geometría y un motor de renderizado. El motor de geometría procesa los vértices resultantes, construye polígonos y los transforma para que pueda pasarlos al motor de representación, que (lo adivinó) dibuja todo en la pantalla.

Motor de geometría


Transportador geométrico bastante estándar.

Vale la pena mencionar que toda la aritmética se realiza en enteros de punto fijo, porque DS no admite números de punto flotante.

El motor de geometría se emula completamente mediante programación (GPU3D.cpp), es decir, no se aplica mucho a lo que usamos para renderizar gráficos, pero de todos modos te contaré más al respecto.

1. Transformación e iluminación. Los vértices resultantes y las coordenadas de textura se convierten utilizando conjuntos de matrices 4x4. Además de los colores de vértice, se aplica iluminación. Aquí todo es bastante estándar, lo único no estándar es cómo funcionan las coordenadas de textura (1.0 = un DS texel). También vale la pena mencionar todo el sistema de pilas de matriz, que en un grado u otro son la implementación de hardware de glPushMatrix ().

2. Configuración de polígonos. Los vértices convertidos se ensamblan en polígonos, que pueden ser triángulos, cuadrángulos (quads), franjas de triángulos o franjas de cuadrángulos. Los quads se procesan de forma nativa y no se convierten en triángulos, lo cual es bastante problemático porque las GPU modernas solo admiten triángulos. Sin embargo, parece que a alguien se le ocurrió una solución que necesito probar.

3. Drop. Los polígonos se pueden eliminar según la orientación en la pantalla y el modo de selección seleccionado. También esquema bastante estándar. Sin embargo, necesito descubrir cómo funciona esto para los quads.

4. Truncamiento. Se eliminan los polígonos más allá del alcance de la visibilidad. Los polígonos que se extienden parcialmente más allá de esta región están truncados. Este paso no crea nuevos polígonos, pero agrega vértices a los existentes. De hecho, cada uno de los 6 planos de truncamiento puede agregar un vértice al polígono, es decir, como resultado, podemos obtener hasta 10 vértices. En la sección sobre el motor de renderizado, te diré cómo lidiamos con esto.

5. Convertir a ventana gráfica. Las coordenadas X / Y se convierten en coordenadas de pantalla. Las coordenadas Z se convierten para ajustarse en un intervalo de búfer de profundidad de 24 bits.

Lo interesante es cómo se procesan las coordenadas W: están "normalizadas" para ajustarse en un intervalo de 16 bits. Para esto, se toma cada coordenada W del polígono, y si es mayor que 0xFFFF, entonces se desplaza hacia la derecha 4 posiciones para caber en 16 bits. Por el contrario, si la coordenada es menor que 0x1000, se mueve hacia la izquierda hasta que cae en el intervalo. Supongo que esto es necesario para obtener buenos intervalos, lo que significa una mayor precisión durante la interpolación.

6. Clasificación. Los polígonos se ordenan para que los polígonos translúcidos se dibujen primero. Luego se ordenan por sus coordenadas Y (sí), lo cual es necesario para los polígonos opacos y opcionalmente translúcidos.

Además, esta es la razón de la restricción de 2048 polígonos: para la clasificación, deben almacenarse en algún lugar. Hay dos bancos de memoria interna asignados para almacenar polígonos y vértices. Incluso hay un registro que informa cuántos polígonos y vértices están almacenados.

Motor de renderizado


¡Y aquí comienza la diversión!

Después de que todos los polígonos se hayan configurado y ordenado, el motor de renderizado comienza a funcionar.

La primera cosa divertida es cómo llena los polígonos. Esto es completamente diferente al trabajo de las GPU modernas que realizan el llenado de mosaicos y utilizan algoritmos optimizados con triángulos. No sé cómo funcionan todos, pero vi cómo se hace esto en la GPU de la consola 3DS, y todo se basa en mosaicos allí.

Sea como fuere, en DS, el renderizado se realiza en cadenas ráster. Los desarrolladores tuvieron que hacer esto para que el renderizado se pudiera realizar en paralelo con los motores de mosaicos bidimensionales de la vieja escuela, que realizan dibujos en líneas de trama. Hay un pequeño búfer con 48 líneas de trama que se pueden usar para ajustar algunas líneas de trama.

Un rasterizador es un renderizador de polígonos convexos basados ​​en cadenas de trama. Puede manejar un número arbitrario de vértices. Puede renderizarse incorrectamente si le pasa polígonos que no son convexos o tienen bordes de intersección, por ejemplo:


El polígono es una mariposa. Todo es correcto y magnífico.

Pero, ¿y si le damos la vuelta?


Ouch

¿Cuál es el error aquí? Dibujemos el contorno del polígono original para descubrir:


Un renderizador solo puede llenar un espacio por línea de trama. Define los bordes izquierdo y derecho comenzando en los picos más altos, y sigue estos bordes hasta que encuentra nuevos picos.

En la imagen que se muestra arriba, comienza desde el vértice superior, es decir, la esquina superior izquierda, y continúa llenándose hasta llegar al final del borde izquierdo (vértice inferior izquierdo). No sabe que los bordes se cruzan.

En este punto, busca el siguiente vértice en su borde izquierdo. Es interesante notar que él sabe que no necesita tomar vértices que son más altos que el actual, y también sabe que los bordes izquierdo y derecho se han intercambiado. Por lo tanto, continúa llenándose hasta el final del vertedero.

Añadiría algunos ejemplos más de polígonos no convexos, pero nos desviaremos demasiado del tema.

Comprendamos mejor cómo funcionan las sombras y texturas de Gouraud con un número arbitrario de vértices. Hay algoritmos barcéntricos utilizados para interpolar datos a lo largo de un triángulo, pero ... en nuestro caso, no son adecuados.

El procesador DS aquí también tiene su propia implementación. Algunas imágenes más interesantes.


Los vértices del polígono son los puntos 1, 2, 3 y 4. Los números no corresponden al orden transversal real, pero usted comprende el significado.

En la línea ráster actual, el renderizador define los vértices que rodean directamente los bordes (como se mencionó anteriormente, comienza desde los vértices superiores y luego atraviesa los bordes hasta que se completan). En nuestro caso, estos son vértices 1 y 2 para el borde izquierdo, 3 y 4 para el borde derecho.

Las pendientes de los bordes se utilizan para determinar los límites del espacio, es decir, los puntos 5 y 6. En estos puntos, los atributos de los vértices se interpolan en función de las posiciones verticales en los bordes (o posiciones horizontales para los bordes, cuyas pendientes se encuentran principalmente a lo largo del eje X).

Luego, para cada píxel en el espacio (por ejemplo, para el punto 7), los atributos basados ​​en la posición X dentro del espacio se interpolan de los atributos previamente calculados en los puntos 5 y 6.

Aquí, todos los coeficientes utilizados son iguales al 50% para simplificar el trabajo, pero el significado es claro.

No entraré en detalles sobre la interpolación de atributos, aunque también será interesante escribir sobre esto. De hecho, esta es una interpolación correcta desde el punto de vista de la perspectiva, pero tiene simplificaciones y características interesantes.

Ahora hablemos sobre cómo DS llena los polígonos.

¿Qué reglas de relleno usa? ¡También hay muchas cosas interesantes aquí!

En primer lugar, existen diferentes reglas de relleno para polígonos opacos y translúcidos. Pero lo más importante, estas reglas se aplican píxel por píxel . Los polígonos translúcidos pueden tener píxeles opacos y seguirán las mismas reglas que los polígonos opacos. Puede suponer que para emular tales trucos en las GPU modernas, se requieren varios pases de renderizado.

Además, diferentes atributos de polígono pueden influir en el renderizado de varias maneras interesantes. Además de los búferes de profundidad y color bastante estándar, el procesador también tiene un búfer de atributos que rastrea todo tipo de cosas interesantes. A saber: la identificación del polígono (por separado para polígonos opacos y translúcidos), translucidez de píxeles, la necesidad de aplicar niebla, si este polígono se dirige hacia o desde la cámara (sí, esto también) y si el píxel está en el borde del polígono. Y tal vez algo más.

La tarea de emular dicho sistema no será trivial. Una GPU moderna ordinaria tiene un búfer de plantilla limitado a 8 bits, que está lejos de ser suficiente para todo lo que puede almacenar un búfer de atributos. Necesitamos encontrar una solución difícil.

Vamos a resolverlo:

* Actualización del búfer de profundidad: se requiere para píxeles opacos, opcional para los translúcidos.

* ID de polígono: las ID de 6 bits se asignan a polígonos, que se pueden usar para varios propósitos. Las ID de polígono opacas se usan para marcar bordes. La ID de los polígonos translúcidos se puede usar para controlar dónde se dibujarán: un píxel translúcido no se dibujará si la ID del polígono coincide con la ID del polígono translúcido que ya está en el búfer de atributos. Además, ambas ID de polígono se usan de manera similar para controlar la representación de sombras. Por ejemplo, puede crear una sombra que cubra el piso, pero no el personaje.

(Nota: las sombras son solo una implementación del búfer de la plantilla, aquí no hay nada terrible).

Vale la pena señalar que cuando se procesan píxeles translúcidos, se guarda la ID existente del polígono opaco, así como las banderas de borde del último polígono opaco.

* bandera de niebla: determina si se aplicará un pase de niebla para este píxel. El proceso de actualización depende de si el píxel entrante es opaco o translúcido.

* bandera de la primera línea: aquí hay problemas con ella. Echa un vistazo a la captura de pantalla:


Arenas de la destrucción, las pantallas de este juego son un conjunto de trucos. No solo cambian sus coordenadas Y para afectar la clasificación Y. La pantalla que se muestra en esta captura de pantalla es probablemente la peor.

Utiliza el caso límite de la prueba de profundidad: la función de comparación "menor que" toma valores iguales si el juego dibuja un polígono mirando a la cámara encima de los píxeles opacos del polígono que se aleja de la cámara . Si exactamente. Y los valores Z de todos los polígonos son cero. Si no emula esta característica, faltarán algunos elementos en la pantalla.

Creo que esto se hizo para que la parte frontal del objeto siempre fuera visible sobre la parte posterior, incluso cuando son tan planas que los valores Z son los mismos. Con todos estos trucos y trucos, el renderizador DS es similar a la versión de hardware de los renderizadores de la era DOS.

Sea como fuere, emular este comportamiento a través de la GPU fue difícil. Pero hay otros casos límite similares de pruebas de profundidad, que también deben ser probados y documentados.

* banderas de costilla: el renderizador rastrea la ubicación de los bordes de los polígonos. Se utilizan en las últimas pasadas, es decir, al marcar bordes y suavizar. También hay reglas especiales para rellenar polígonos opacos con suavizado desactivado. El siguiente diagrama ilustra estas reglas:


Nota: ¡los wireframes se procesan rellenando solo los bordes! Muy inteligente movimiento.

Otra nota divertida sobre el buffer de profundidad:

Hay dos modos posibles de almacenamiento en profundidad en DS: almacenamiento en Z y almacenamiento en W. Esto parece ser bastante estándar, pero solo si no entra en detalles.

* Z-buffering utiliza coordenadas Z convertidas para ajustarse en un intervalo de búfer de profundidad de 24 bits. Las coordenadas Z se interpolan linealmente sobre polígonos (con algunas rarezas, pero no son particularmente importantes). No hay nada no estándar aquí tampoco.

* En el almacenamiento en W, las coordenadas W se usan "tal cual". Las GPU modernas generalmente usan 1 / W, pero DS solo usa aritmética de punto fijo, por lo que usar valores recíprocos no es muy conveniente. Sea como fuere, en este modo, las coordenadas W se interpolan con la corrección de la perspectiva.

Así es como se ven los pases de renderizado final:

* marca de borde: los píxeles que tienen marcas de borde configuradas se les asigna un color tomado de la tabla y se determina en función de la ID de un polígono opaco.

Serán bordes coloreados de polígonos. Vale la pena señalar que si se dibuja un polígono translúcido encima de un polígono opaco, entonces los bordes del polígono seguirán coloreados.

Un efecto secundario del principio de truncamiento: los bordes en los que los polígonos se cruzan con los bordes de la pantalla también serán coloreados. Por ejemplo, puede notar esto en las capturas de pantalla de Picross 3D.

* niebla: se aplica a cada píxel en función de los valores de profundidad utilizados para indexar la tabla de densidad de niebla. Como puede suponer, se aplica a aquellos píxeles que tienen banderas de niebla establecidas en el búfer de atributos.

* antialiasing (suavizado): se aplica a los bordes de los polígonos (opacos). En función de las pendientes de los bordes al representar polígonos, se calculan los valores de cobertura de píxeles. En la última pasada, estos píxeles se mezclan con los píxeles debajo de ellos usando el mecanismo complicado que describí en una publicación anterior.

Antialiasing no debe (y no puede) emularse de esta manera en la GPU, por lo que esto no es importante aquí.

Excepto que si la marca del borde y el suavizado se deben aplicar a los mismos píxeles, solo obtienen el tamaño del borde, pero con un 50% de opacidad.

Parece que describí el proceso de renderizado más o menos bien. No profundizamos en la mezcla de texturas (combinando colores de vértices y texturas), pero se puede emular en un sombreador de fragmentos. Lo mismo se aplica al marcado de bordes y la niebla, siempre que encontremos una forma de evitar todo este sistema con un búfer de atributos.

Pero en general, quería transmitir lo siguiente: OpenGL o Vulkan (así como Direct3D, Glide o cualquier otra cosa) no ayudarán aquí. Nuestras GPU modernas tienen potencia más que suficiente para trabajar con polígonos sin procesar. El problema son los detalles y las características de la rasterización. Y ni siquiera se trata de la idealidad de los píxeles, por ejemplo, solo mire el rastreador de problemas del emulador DeSmuME para comprender qué problemas encuentran los desarrolladores al renderizar a través de OpenGL. También tenemos que lidiar con estos mismos problemas de alguna manera.

También noto que usar OpenGL nos permitirá portar el emulador, por ejemplo, a Switch (porque un usuario de Github llamado Hydr8gon comenzó a crear un puerto para nuestro emulador en Switch ).

Entonces ... deséame suerte.

Source: https://habr.com/ru/post/es429160/


All Articles