Je voudrais vous parler du fonctionnement de la console GPU Nintendo DS, de ses différences avec les GPU modernes, et également exprimer mon opinion sur pourquoi l'utilisation de Vulkan au lieu d'OpenGL dans les émulateurs n'apportera aucun avantage.
Je ne connais pas vraiment Vulkan, mais d'après ce que j'ai lu, il est clair pour moi que Vulkan diffère d'OpenGL en ce qu'il fonctionne à un niveau inférieur, permettant aux programmeurs de gérer la mémoire du GPU et des choses similaires. Cela peut être utile pour émuler des consoles plus modernes qui utilisent des API graphiques propriétaires qui fournissent des niveaux de contrôle non disponibles dans OpenGL.
Par exemple, le rendu matériel blargSNES - l'une de ses astuces est que lors de certaines opérations avec des tampons de couleurs différentes, un tampon de profondeur / gabarit est utilisé. En OpenGL, ce n'est pas possible.
De plus, il reste moins de déchets entre l'application et le GPU, ce qui signifie que s'il est correctement implémenté, les performances seront plus élevées. Alors que les pilotes OpenGL regorgent d'optimisations pour les cas d'utilisation standard et même pour des jeux spécifiques, dans Vulkan, l'application elle-même doit d'abord être bien écrite.
Autrement dit, "une grande responsabilité s'accompagne d'une grande force".
Je ne suis pas un spécialiste de l'API 3D, alors revenons à cela. Ce que je sais bien: la console GPU DS.
Plusieurs articles ont déjà été écrits sur ses parties individuelles (
sur ses quads sophistiqués ,
sur les bêtises avec fenêtre d'affichage ,
sur les fonctionnalités amusantes du rasterizer et
sur la mise en œuvre étonnante de l'anti-aliasing ), mais dans cet article, nous considérerons l'appareil dans son ensemble, mais avec tous les détails juteux. Du moins, c'est tout ce que nous savons.
Le GPU lui-même est un matériel assez ancien et obsolète. Elle est limitée à 2048 polygones et / ou 6144 sommets par image. La résolution est de 256x192. Même si vous quadruple cela, les performances ne seront pas un problème. Dans des conditions optimales, DS peut produire jusqu'à 122880 polygones par seconde, ce qui est ridicule par rapport aux normes des GPU modernes.
Passons maintenant aux détails du GPU. En apparence, il semble assez standard, mais au fond de son travail est très différent du travail des GPU modernes, ce qui rend l'émulation de certaines fonctions plus compliquée.
Le GPU est divisé en deux parties: un moteur de géométrie et un moteur de rendu. Le moteur de géométrie traite les sommets résultants, construit des polygones et les transforme pour que vous puissiez les transmettre au moteur de rendu qui (vous l'avez deviné) dessine tout sur l'écran.
Moteur de géométrie
Convoyeur géométrique assez standard.
Il convient de mentionner que toute l'arithmétique est effectuée en nombres entiers à virgule fixe, car DS ne prend pas en charge les nombres à virgule flottante.
Le moteur de géométrie est entièrement émulé par programmation (GPU3D.cpp), c'est-à-dire qu'il ne s'applique pas beaucoup à ce que nous utilisons pour le rendu des graphiques, mais je vous en dirai quand même plus à ce sujet.
1. Transformation et éclairage. Les sommets et les coordonnées de texture résultants sont convertis à l'aide d'ensembles de matrices 4x4. En plus des couleurs des sommets, l'éclairage est appliqué. Tout est assez standard ici, le seul non standard est le fonctionnement des coordonnées de texture (1.0 = un texel DS). Il convient également de mentionner l'ensemble du système de piles matricielles, qui sont à un degré ou à un autre l'implémentation matérielle de glPushMatrix ().
2. Configuration des polygones. Les sommets convertis sont assemblés en polygones, qui peuvent être des triangles, des quadrangles (quads), des bandes de triangles ou des bandes de quadrangles. Les quads sont traités en mode natif et ne se convertissent pas en triangles, ce qui est assez problématique car les GPU modernes ne prennent en charge que les triangles. Cependant, il semble que quelqu'un ait
trouvé une solution que je dois tester.
3. Laissez tomber. Les polygones peuvent être éliminés en fonction de l'orientation sur l'écran et du mode d'abattage sélectionné. Également régime assez standard. Cependant, je dois comprendre comment cela fonctionne pour les quads.
4. Troncature. Les polygones au-delà du champ de visibilité sont éliminés. Les polygones s'étendant partiellement au-delà de cette région sont tronqués. Cette étape ne crée pas de nouveaux polygones, mais ajoute des sommets aux polygones existants. En fait, chacun des 6 plans de troncature peut ajouter un sommet au polygone, ce qui signifie que nous pouvons obtenir jusqu'à 10 sommets. Dans la section sur le moteur de rendu, je vais vous expliquer comment nous avons traité cela.
5. Convertissez en fenêtre. Les coordonnées X / Y sont converties en coordonnées d'écran. Les coordonnées Z sont converties pour tenir dans un intervalle de tampon de profondeur de 24 bits.
Ce qui est intéressant, c'est la façon dont les coordonnées W sont traitées: elles sont «normalisées» pour tenir dans un intervalle de 16 bits. Pour cela, chaque coordonnée W du polygone est prise, et si elle est supérieure à 0xFFFF, elle est alors décalée vers la droite de 4 positions pour tenir sur 16 bits. Inversement, si la coordonnée est inférieure à 0x1000, elle se déplace vers la gauche jusqu'à ce qu'elle tombe dans l'intervalle. Je suppose que cela est nécessaire pour obtenir de bons intervalles, ce qui signifie une plus grande précision lors de l'interpolation.
6. Tri. Les polygones sont triés de sorte que les polygones translucides soient dessinés en premier. Ensuite, ils sont triés par leurs coordonnées Y (oui), ce qui est nécessaire pour les polygones opaques et éventuellement translucides.
De plus, c'est la raison de la restriction de 2048 polygones: pour le tri, ils doivent être stockés quelque part. Deux banques de mémoire interne sont affectées au stockage des polygones et des sommets. Il existe même un registre indiquant le nombre de polygones et de sommets stockés.
Moteur de rendu
Et ici, le plaisir commence!
Une fois tous les polygones configurés et triés, le moteur de rendu commence à fonctionner.
La première chose amusante est de savoir comment il remplit les polygones. Ceci est complètement différent du travail des GPU modernes qui effectuent le remplissage des tuiles et utilisent des algorithmes optimisés pour les triangles. Je ne sais pas comment ils fonctionnent tous, mais j'ai vu comment cela se fait dans le GPU de la console 3DS, et tout est basé sur des tuiles là-bas.
Quoi qu'il en soit, sur DS, le rendu se fait en chaînes raster. Les développeurs ont dû le faire pour que le rendu puisse être effectué en parallèle avec les moteurs de tuiles bidimensionnelles à l'ancienne, qui effectuent le dessin sur des lignes raster. Il y a un petit tampon avec 48 lignes raster qui peuvent être utilisées pour ajuster certaines lignes raster.
Un rasterizer est un rendu de polygones convexes basé sur des chaînes raster. Il peut gérer un nombre arbitraire de sommets. Le rendu peut être incorrect si vous lui passez des polygones qui ne sont pas convexes ou qui ont des bords qui se croisent, par exemple:
Le polygone est un papillon. Tout est correct et magnifique.Et si on le retournait?
Ouch.Quelle est l'erreur ici? Dessinons le contour du polygone d'origine pour comprendre:
Un rendu ne peut remplir qu'un seul espace par ligne raster. Il définit les bords gauche et droit en commençant par les pics les plus élevés et suit ces bords jusqu'à ce qu'il rencontre de nouveaux pics.
Dans l'image ci-dessus, il part du sommet supérieur, c'est-à-dire en haut à gauche, et continue de se remplir jusqu'à ce qu'il atteigne la fin du bord gauche (sommet inférieur gauche). Il ne sait pas que les bords se croisent.
À ce stade, il recherche le sommet suivant sur son bord gauche. Il est intéressant de noter qu'il sait qu'il n'a pas besoin de prendre des sommets supérieurs à celui actuel, et sait également que les bords gauche et droit se sont interchangés. Par conséquent, il continue de se remplir jusqu'à la fin de la décharge.
J'ajouterais quelques exemples supplémentaires de polygones non convexes, mais nous nous éloignerons trop du sujet.
Comprenons mieux comment l'ombrage et la texturation de Gouraud fonctionnent avec un nombre arbitraire de sommets. Il existe des algorithmes barycentriques utilisés pour interpoler les données le long d'un triangle, mais ... dans notre cas, ils ne conviennent pas.
Le rendu DS ici a également sa propre implémentation. Quelques images plus intéressantes.
Les sommets du polygone sont les points 1, 2, 3 et 4. Les nombres ne correspondent pas à l'ordre de déplacement réel, mais vous en comprenez la signification.
Dans la ligne raster actuelle, le rendu définit les sommets entourant directement les bords (comme mentionné ci-dessus, il part des sommets les plus élevés, puis passe par les bords jusqu'à ce qu'ils soient complets). Dans notre cas, ce sont les sommets 1 et 2 pour le bord gauche, 3 et 4 pour le bord droit.
Les pentes des arêtes sont utilisées pour déterminer les limites de l'écart, c'est-à-dire les points 5 et 6. À ces points, les attributs des sommets sont interpolés en fonction des positions verticales des arêtes (ou des positions horizontales pour les arêtes, dont les pentes sont principalement le long de l'axe X).
Ensuite, pour chaque pixel de l'espace (par exemple, pour le point 7), les attributs basés sur la position X à l'intérieur de l'espace sont interpolés à partir des attributs précédemment calculés aux points 5 et 6.
Ici, tous les coefficients utilisés sont égaux à 50% pour simplifier le travail, mais le sens est clair.
Je n'entrerai pas dans les détails de l'interpolation d'attributs, bien qu'il soit également intéressant d'écrire à ce sujet. En fait, c'est une interpolation correcte du point de vue de la perspective, mais elle a des simplifications et des caractéristiques intéressantes.
Parlons maintenant de la façon dont DS remplit les polygones.
Quelles règles de remplissage utilise-t-il? Il y a aussi beaucoup de choses intéressantes ici!
Premièrement, il existe différentes règles de remplissage pour les polygones opaques et translucides. Mais surtout, ces règles s'appliquent
pixel par pixel . Les polygones translucides peuvent avoir des pixels opaques et ils suivront les mêmes règles que les polygones opaques. Vous pouvez deviner que pour émuler de telles astuces sur des GPU modernes, plusieurs passes de rendu sont nécessaires.
De plus, différents attributs de polygone peuvent influencer le rendu de diverses manières intéressantes. En plus des tampons de couleur et de profondeur assez standard, le moteur de rendu dispose également d'
un tampon d'attributs qui suit toutes sortes de choses intéressantes. À savoir: l'ID du polygone (séparément pour les polygones opaques et translucides), la translucidité du pixel, la nécessité d'appliquer du brouillard, si ce polygone est dirigé vers ou depuis la caméra (oui, cela aussi), et si le pixel est sur le bord du polygone. Et peut-être autre chose.
La tâche d'émuler un tel système ne sera pas anodine. Un GPU moderne ordinaire a un tampon de gabarit limité à 8 bits, ce qui est loin d'être suffisant pour tout ce qui peut stocker un tampon d'attribut. Nous devons trouver une solution de contournement délicate.
Voyons cela:
* Mise à jour du tampon de profondeur: requise pour les pixels opaques, facultative pour les pixels translucides.
* ID de polygone: des ID de 6 bits sont attribués aux polygones, qui peuvent être utilisés à plusieurs fins. Les ID de polygone opaques sont utilisés pour marquer les bords. L'ID des polygones translucides peut être utilisé pour contrôler où ils seront dessinés: un pixel translucide ne sera pas dessiné si l'ID du polygone correspond à l'ID du polygone translucide déjà dans le tampon d'attributs. En outre, les deux ID de polygone sont utilisés de la même manière pour contrôler le rendu des ombres. Par exemple, vous pouvez créer une ombre qui couvre le sol, mais pas le personnage.
(Remarque: les ombres ne sont qu'une implémentation du tampon de gabarit, il n'y a rien de terrible ici.)
Il convient de noter que lors du rendu des pixels translucides, l'ID existant du polygone opaque est enregistré, ainsi que les drapeaux de bord du dernier polygone opaque.
* indicateur de brouillard: détermine s'il faut appliquer une passe de brouillard pour ce pixel. Le processus de mise à jour dépend de l'opacité ou de la translucidité du pixel entrant.
* drapeau de première ligne: ici il a des problèmes. Jetez un œil à la capture d'écran:
Sands of Destruction, les écrans de ce jeu sont un ensemble d'astuces. Ils modifient non seulement leurs coordonnées Y pour affecter le tri Y. L'écran montré dans cette capture d'écran est probablement le pire.
Il utilise le cas limite du test de profondeur: la fonction de comparaison "moins que"
prend des valeurs égales si le jeu
dessine un polygone en regardant la caméra au-dessus des pixels opaques du polygone dirigé loin de la caméra . Oui, exactement. Et les valeurs Z de tous les polygones sont nulles. Si vous n'émulez pas cette fonctionnalité, certains éléments seront manquants à l'écran.
Je pense que cela a été fait pour que la face avant de l'objet soit toujours visible sur la face arrière, même lorsqu'ils sont si plats que les valeurs Z sont les mêmes. Avec tous ces hacks et astuces, le rendu DS est similaire à la version matérielle des rendus de l'ère DOS.
Quoi qu'il en soit, il était difficile d'émuler ce comportement via le GPU. Mais il existe d'autres cas limites similaires de tests de profondeur, qui doivent également être testés et documentés.
* drapeaux de côtes: le moteur de rendu suit l'emplacement des bords des polygones. Ils sont utilisés dans les dernières passes, notamment lors du marquage des bords et de l'anticrénelage. Il existe également des règles spéciales pour le remplissage de polygones opaques avec l'anticrénelage désactivé. Le diagramme ci-dessous illustre ces règles:
Remarque: les wireframes sont rendus en remplissant uniquement les bords! Mouvement très intelligent.
Une autre note amusante sur la mise en mémoire tampon de la profondeur:
Il existe deux modes de tampon de profondeur possibles sur DS: le tampon Z et le tampon W. Cela semble être assez standard, mais seulement si vous n'entrez pas dans les détails.
* La mise en mémoire tampon Z utilise les coordonnées Z converties pour tenir dans un intervalle de tampon de profondeur de 24 bits. Les coordonnées Z sont interpolées linéairement sur des polygones (avec quelques bizarreries, mais elles ne sont pas particulièrement importantes). Il n'y a rien de non standard ici non plus.
* Dans la mise en mémoire tampon W, les coordonnées W sont utilisées «telles quelles». Les GPU modernes utilisent généralement 1 / W, mais DS n'utilise que l'arithmétique à virgule fixe, donc l'utilisation de valeurs réciproques n'est pas très pratique. Quoi qu'il en soit, dans ce mode, les coordonnées W sont interpolées avec correction de perspective.
Voici à quoi ressemble le rendu final:
* Marquage des bords: les pixels pour lesquels des drapeaux de bords sont définis se voient attribuer une couleur tirée du tableau et déterminée en fonction de l'ID d'un polygone opaque.
Ce seront des bords colorés de polygones. Il convient de noter que si un polygone translucide est dessiné au-dessus d'un polygone opaque, les bords du polygone seront toujours colorés.
Un effet secondaire du principe de troncature: les bordures auxquelles les polygones se croisent avec les bordures de l'écran seront également colorées. Vous pouvez par exemple le remarquer dans les captures d'écran de Picross 3D.
* brouillard: il est appliqué à chaque pixel en fonction des valeurs de profondeur utilisées pour indexer la table de densité de brouillard. Comme vous pouvez le deviner, cela s'applique aux pixels dont les indicateurs de brouillard sont définis dans le tampon d'attributs.
* anticrénelage (lissage): il est appliqué sur les bords des polygones (opaques). En fonction des pentes des bords lors du rendu des polygones, les valeurs de couverture en pixels sont calculées. Dans la dernière passe, ces pixels sont mélangés avec les pixels en dessous d'eux en utilisant le mécanisme délicat que j'ai décrit dans un post précédent.
L'anticrénelage ne doit pas (et ne peut pas) être émulé de cette manière sur le GPU, donc ce n'est pas important ici.
Sauf que si le marquage des bords et l'anticrénelage doivent être appliqués aux mêmes pixels, ils n'obtiennent que la taille du bord, mais avec une opacité de 50%.
Il me semble avoir décrit le processus de rendu plus ou moins bien. Nous ne nous sommes pas plongés dans le mélange des textures (combinant les couleurs des sommets et des textures), mais il peut être émulé dans un fragment shader. La même chose s'applique au marquage des bords et au brouillard, à condition que nous trouvions un moyen de contourner tout ce système avec un tampon d'attributs.
Mais en général, je voulais transmettre ce qui suit: OpenGL ou Vulkan (ainsi que Direct3D, ou Glide, ou toute autre chose) n'aidera pas ici. Nos GPU modernes ont plus que suffisamment de puissance pour fonctionner avec des polygones bruts. Le problème réside dans les détails et les fonctionnalités de la pixellisation. Et il ne s'agit même pas de l'idéalité des pixels, par exemple, regardez simplement le suivi des problèmes de l'émulateur DeSmuME pour comprendre quels problèmes les développeurs rencontrent lors du rendu via OpenGL. Nous devons également faire face à ces mêmes problèmes d'une manière ou d'une autre.
Je note également que l'utilisation d'OpenGL nous permettra de porter l'émulateur, par exemple, sur Switch (car un utilisateur Github nommé Hydr8gon a commencé à créer un
port pour notre émulateur sur Switch ).
Alors ... souhaite-moi bonne chance.