Ich möchte Sie über den Betrieb der Nintendo DS-GPU-Konsole und ihre Unterschiede zu modernen GPUs informieren und meine Meinung dazu äußern, warum die Verwendung von Vulkan anstelle von OpenGL in Emulatoren keine Vorteile bringt.
Ich kenne Vulkan nicht wirklich, aber nach dem, was ich gelesen habe, ist mir klar, dass Vulkan sich von OpenGL dadurch unterscheidet, dass es auf einer niedrigeren Ebene funktioniert und es Programmierern ermöglicht, den GPU-Speicher und ähnliche Dinge zu verwalten. Dies kann nützlich sein, um modernere Konsolen zu emulieren, die proprietäre grafische APIs verwenden, die Kontrollstufen bieten, die in OpenGL nicht verfügbar sind.
Beispiel: blargSNES-Hardware-Renderer - einer seiner Tricks besteht darin, dass bei einigen Vorgängen mit unterschiedlichen Farbpuffern ein Tiefen- / Schablonenpuffer verwendet wird. In OpenGL ist dies nicht möglich.
Darüber hinaus verbleibt weniger Müll zwischen der Anwendung und der GPU, was bedeutet, dass bei korrekter Implementierung die Leistung höher ist. Während OpenGL-Treiber voller Optimierungen für Standardanwendungsfälle und sogar für bestimmte Spiele sind, sollte in Vulkan die Anwendung selbst zunächst gut geschrieben sein.
Das heißt im Wesentlichen: "Große Verantwortung geht mit großer Kraft einher."
Ich bin kein 3D-API-Spezialist. Kommen wir also darauf zurück. Was ich gut weiß: GPU-Konsole DS.
Es wurden bereits mehrere Artikel über seine einzelnen Teile geschrieben (
über seine ausgeklügelten Quads ,
über Unsinn mit Ansichtsfenster ,
über die unterhaltsamen Funktionen des Rasterisierers und
über die erstaunliche Implementierung von Anti-Aliasing ), aber in diesem Artikel werden wir das Gerät als Ganzes betrachten, aber mit all den saftigen Details. Zumindest ist das alles, was wir wissen.
Die GPU selbst ist eine ziemlich alte und veraltete Hardware. Es ist auf 2048 Polygone und / oder 6144 Eckpunkte pro Frame begrenzt. Die Auflösung beträgt 256x192. Selbst wenn Sie dies vervierfachen, ist die Leistung kein Problem. Unter optimalen Bedingungen kann DS bis zu 122880 Polygone pro Sekunde ausgeben, was nach den Standards moderner GPUs lächerlich ist.
Kommen wir nun zu den Details der GPU. Oberflächlich betrachtet sieht es ziemlich normal aus, aber tief in seiner Arbeit unterscheidet es sich stark von der Arbeit moderner GPUs, was die Emulation einiger Funktionen komplizierter macht.
Die GPU ist in zwei Teile unterteilt: eine Geometrie-Engine und eine Rendering-Engine. Die Geometrie-Engine verarbeitet die resultierenden Scheitelpunkte, erstellt Polygone und transformiert sie, sodass Sie sie an die Rendering-Engine übergeben können, die (Sie haben es erraten) alles auf dem Bildschirm zeichnet.
Geometrie-Engine
Ziemlich normaler geometrischer Förderer.
Es ist erwähnenswert, dass alle Arithmetik in Festkomma-Ganzzahlen ausgeführt wird, da DS keine Gleitkommazahlen unterstützt.
Die Geometrie-Engine wird vollständig programmgesteuert emuliert (GPU3D.cpp), das heißt, sie gilt nicht für das, was wir zum Rendern von Grafiken verwenden, aber ich werde Ihnen trotzdem mehr darüber erzählen.
1. Transformation und Beleuchtung. Die resultierenden Eckpunkte und Texturkoordinaten werden unter Verwendung von Sätzen von 4x4-Matrizen konvertiert. Zusätzlich zu den Scheitelpunktfarben wird die Beleuchtung angewendet. Hier ist alles ziemlich normal, der einzige Nicht-Standard ist, wie Texturkoordinaten funktionieren (1.0 = ein DS-Texel). Erwähnenswert ist auch das gesamte System der Matrixstapel, bei denen es sich in gewissem Maße um die Hardware-Implementierung von glPushMatrix () handelt.
2. Polygone konfigurieren. Die konvertierten Eckpunkte werden zu Polygonen zusammengesetzt, die Dreiecke, Vierecke (Quads), Dreiecksstreifen oder Viereckstreifen sein können. Quads werden nativ verarbeitet und nicht in Dreiecke konvertiert, was ziemlich problematisch ist, da moderne GPUs nur Dreiecke unterstützen. Es sieht jedoch so aus, als hätte jemand
eine Lösung gefunden , die ich testen muss.
3. Lassen Sie fallen. Polygone können abhängig von der Ausrichtung auf dem Bildschirm und dem ausgewählten Keulungsmodus entsorgt werden. Auch ziemlich Standardschema. Ich muss jedoch herausfinden, wie dies für Quads funktioniert.
4. Kürzung. Polygone, die über den Sichtbarkeitsbereich hinausgehen, werden eliminiert. Polygone, die sich teilweise über diesen Bereich hinaus erstrecken, werden abgeschnitten. In diesem Schritt werden keine neuen Polygone erstellt, sondern vorhandene Scheitelpunkte hinzugefügt. Tatsächlich kann jede der 6 Kürzungsebenen dem Polygon einen Scheitelpunkt hinzufügen, dh als Ergebnis können bis zu 10 Scheitelpunkte erhalten werden. Im Abschnitt über die Rendering-Engine werde ich Ihnen erklären, wie wir damit umgegangen sind.
5. In Ansichtsfenster konvertieren. X / Y-Koordinaten werden in Bildschirmkoordinaten konvertiert. Z-Koordinaten werden konvertiert, um in ein 24-Bit-Tiefenpufferintervall zu passen.
Interessant ist, wie die W-Koordinaten verarbeitet werden: Sie werden „normalisiert“, um in ein 16-Bit-Intervall zu passen. Dazu wird jede W-Koordinate des Polygons genommen, und wenn sie größer als 0xFFFF ist, wird sie um 4 Positionen nach rechts verschoben, um in 16 Bit zu passen. Wenn umgekehrt die Koordinate kleiner als 0x1000 ist, bewegt sie sich nach links, bis sie in das Intervall fällt. Ich nehme an, dass dies notwendig ist, um gute Intervalle zu erhalten, was eine größere Genauigkeit während der Interpolation bedeutet.
6. Sortieren. Polygone werden so sortiert, dass zuerst durchscheinende Polygone gezeichnet werden. Dann werden sie nach ihren Y-Koordinaten (yeah) sortiert, was für undurchsichtige und optional durchscheinende Polygone erforderlich ist.
Dies ist außerdem der Grund für die Einschränkung von 2048 Polygonen: Zum Sortieren müssen sie irgendwo gespeichert werden. Es gibt zwei interne Speicherbänke zum Speichern von Polygonen und Scheitelpunkten. Es gibt sogar ein Register, das angibt, wie viele Polygone und Eckpunkte gespeichert sind.
Motor rendern
Und hier beginnt der Spaß!
Nachdem alle Polygone konfiguriert und sortiert wurden, funktioniert die Rendering-Engine.
Das erste lustige ist, wie es die Polygone füllt. Dies ist völlig anders als bei modernen GPUs, die das Füllen von Kacheln durchführen und dreieckoptimierte Algorithmen verwenden. Ich weiß nicht, wie sie alle funktionieren, aber ich habe gesehen, wie dies in der 3DS-Konsolen-GPU gemacht wird, und alles basiert auf Kacheln.
Wie auch immer, unter DS erfolgt das Rendern in Rasterzeichenfolgen. Die Entwickler mussten dies tun, damit das Rendern parallel zu den zweidimensionalen Kachel-Engines der alten Schule durchgeführt werden konnte, die das Zeichnen auf Rasterlinien ausführen. Es gibt einen kleinen Puffer mit 48 Rasterzeilen, mit dem einige Rasterzeilen angepasst werden können.
Ein Rasterizer ist ein Renderer von konvexen Polygonen, die auf Rasterzeichenfolgen basieren. Es kann eine beliebige Anzahl von Eckpunkten verarbeiten. Es kann falsch gerendert werden, wenn Sie Polygone übergeben, die nicht konvex sind oder sich schneidende Kanten haben, zum Beispiel:
Das Polygon ist ein Schmetterling. Alles ist richtig und großartig.Aber was ist, wenn wir es umdrehen?
Autsch.Was ist der Fehler hier? Zeichnen wir den Umriss des ursprünglichen Polygons, um Folgendes herauszufinden:
Ein Renderer kann nur eine Lücke pro Rasterzeile füllen. Es definiert die linken und rechten Kanten beginnend mit den höchsten Spitzen und folgt diesen Kanten, bis es auf neue Spitzen trifft.
In dem oben gezeigten Bild beginnt er am obersten Scheitelpunkt, dh oben links, und füllt sich weiter, bis er das Ende des linken Randes erreicht (unterer linker Scheitelpunkt). Er weiß nicht, dass sich die Kanten schneiden.
Zu diesem Zeitpunkt sucht er nach dem nächsten Scheitelpunkt an seiner linken Kante. Es ist interessant festzustellen, dass er weiß, dass er keine Eckpunkte nehmen muss, die höher als der aktuelle sind, und dass der linke und der rechte Rand vertauscht sind. Daher füllt es sich bis zum Ende der Deponie weiter.
Ich würde noch einige Beispiele für nicht konvexe Polygone hinzufügen, aber wir werden zu weit vom Thema abweichen.
Lassen Sie uns besser verstehen, wie Gouraud-Schattierungen und -Texturen mit einer beliebigen Anzahl von Scheitelpunkten funktionieren. Es gibt baryzentrische Algorithmen, die zum Interpolieren von Daten entlang eines Dreiecks verwendet werden, aber ... in unserem Fall sind sie nicht geeignet.
Der DS-Renderer hat hier auch eine eigene Implementierung. Noch ein paar interessante Bilder.
Die Eckpunkte des Polygons sind die Punkte 1, 2, 3 und 4. Die Zahlen entsprechen nicht der tatsächlichen Durchquerungsreihenfolge, aber Sie verstehen die Bedeutung.
In der aktuellen Rasterlinie definiert der Renderer die Scheitelpunkte, die die Kanten direkt umgeben (wie oben erwähnt, beginnt er an den obersten Scheitelpunkten und geht dann durch die Kanten, bis sie vollständig sind). In unserem Fall sind dies die Eckpunkte 1 und 2 für den linken Rand, 3 und 4 für den rechten Rand.
Die Steigungen der Kanten werden verwendet, um die Grenzen der Lücke zu bestimmen, dh die Punkte 5 und 6. An diesen Punkten werden die Attribute der Scheitelpunkte basierend auf den vertikalen Positionen in den Kanten (oder horizontalen Positionen für Kanten, deren Steigungen hauptsächlich entlang der X-Achse liegen) interpoliert.
Dann werden für jedes Pixel in der Lücke (zum Beispiel für Punkt 7) Attribute basierend auf der X-Position innerhalb der Lücke aus den zuvor an den Punkten 5 und 6 berechneten Attributen interpoliert.
Hier sind alle verwendeten Koeffizienten gleich 50%, um die Arbeit zu vereinfachen, aber die Bedeutung ist klar.
Ich werde nicht auf die Details der Attributinterpolation eingehen, obwohl es auch interessant sein wird, darüber zu schreiben. Tatsächlich ist dies aus perspektivischer Sicht eine korrekte Interpolation, weist jedoch interessante Vereinfachungen und Merkmale auf.
Lassen Sie uns nun darüber sprechen, wie DS die Polygone füllt.
Welche Füllregeln verwendet er? Hier gibt es auch viele interessante Dinge!
Erstens gibt es unterschiedliche Füllregeln für undurchsichtige und durchscheinende Polygone. Vor allem aber gelten diese Regeln
Pixel für Pixel . Durchscheinende Polygone können undurchsichtige Pixel haben und folgen denselben Regeln wie undurchsichtige Polygone. Sie können davon ausgehen, dass zum Emulieren solcher Tricks auf modernen GPUs mehrere Rendering-Durchgänge erforderlich sind.
Darüber hinaus können verschiedene Polygonattribute das Rendern auf verschiedene interessante Arten beeinflussen. Zusätzlich zu den Standard-Farb- und Tiefenpuffern verfügt der Renderer über
einen Attributpuffer , der alle möglichen interessanten Dinge verfolgt. Nämlich: die Polygon-ID (getrennt für undurchsichtige und durchscheinende Polygone), die Pixel-Transluzenz, die Notwendigkeit, Nebel anzuwenden, ob dieses Polygon zur oder von der Kamera gerichtet ist (ja, auch dies) und ob sich das Pixel am Rand des Polygons befindet. Und vielleicht noch etwas.
Die Aufgabe, ein solches System zu emulieren, wird nicht trivial sein. Eine gewöhnliche moderne GPU hat einen Schablonenpuffer, der auf 8 Bit begrenzt ist, was bei weitem nicht ausreicht für alles, was einen Attributpuffer speichern kann. Wir müssen eine schwierige Problemumgehung finden.
Lassen Sie es uns herausfinden:
* Aktualisierung des Tiefenpuffers: Erforderlich für undurchsichtige Pixel, optional für durchscheinende Pixel.
* Polygon-IDs: 6-Bit-IDs werden Polygonen zugewiesen, die für verschiedene Zwecke verwendet werden können. Undurchsichtige Polygon-IDs werden zum Markieren von Kanten verwendet. Die ID von durchscheinenden Polygonen kann verwendet werden, um zu steuern, wo sie gezeichnet werden: Ein durchscheinendes Pixel wird nicht gezeichnet, wenn die Polygon-ID mit der ID des durchscheinenden Polygons übereinstimmt, das sich bereits im Attributpuffer befindet. Außerdem werden beide Polygon-IDs in ähnlicher Weise zur Steuerung des Schatten-Renderings verwendet. Sie können beispielsweise einen Schatten erstellen, der den Boden bedeckt, nicht jedoch den Charakter.
(Hinweis: Schatten sind nur eine Implementierung des Schablonenpuffers, hier gibt es nichts Schreckliches.)
Es ist zu beachten, dass beim Rendern von durchscheinenden Pixeln die vorhandene ID des undurchsichtigen Polygons sowie die Kantenflags des letzten undurchsichtigen Polygons gespeichert werden.
* Nebelflag: Legt fest, ob für dieses Pixel ein Nebelpass angewendet werden soll. Der Aktualisierungsprozess hängt davon ab, ob das eingehende Pixel undurchsichtig oder durchscheinend ist.
* Flagge der Front: Hier gibt es Probleme damit. Schauen Sie sich den Screenshot an:
Sands of Destruction, die Bildschirme dieses Spiels sind eine Reihe von Tricks. Sie ändern nicht nur ihre Y-Koordinaten, um die Y-Sortierung zu beeinflussen. Der in diesem Screenshot gezeigte Bildschirm ist wahrscheinlich der schlechteste.
Es wird der Grenzfall des Tiefentests verwendet: Die Vergleichsfunktion "kleiner als"
nimmt gleiche Werte an, wenn das Spiel
ein Polygon zeichnet, das die Kamera über den undurchsichtigen Pixeln des von der Kamera weggerichteten Polygons betrachtet . Ja genau. Und die Z-Werte aller Polygone sind Null. Wenn Sie diese Funktion nicht emulieren, fehlen einige Elemente auf dem Bildschirm.
Ich denke, dass dies so gemacht wurde, dass die Vorderseite des Objekts immer über der Rückseite sichtbar war, selbst wenn sie so flach sind, dass die Z-Werte gleich sind. Mit all diesen Hacks und Tricks ähnelt der DS-Renderer der Hardwareversion der DOS-Renderer.
Wie dem auch sei, es war schwierig, dieses Verhalten über die GPU zu emulieren. Es gibt jedoch auch andere ähnliche Grenzfälle für Tiefenprüfungen, die ebenfalls geprüft und dokumentiert werden müssen.
* Rippenflags: Der Renderer verfolgt die Position der Kanten von Polygonen. Sie werden in den letzten Durchgängen verwendet, nämlich beim Markieren von Kanten und beim Anti-Aliasing. Es gibt auch spezielle Regeln zum Füllen undurchsichtiger Polygone mit deaktiviertem Anti-Aliasing. Das folgende Diagramm veranschaulicht diese Regeln:
Hinweis: Drahtgitter werden gerendert, indem nur die Kanten gefüllt werden! Sehr kluger Schachzug.
Ein weiterer lustiger Hinweis zur Tiefenpufferung:
Bei DS gibt es zwei mögliche Tiefenpufferungsmodi: Z-Pufferung und W-Pufferung. Dies scheint ziemlich normal zu sein, aber nur, wenn Sie nicht auf Details eingehen.
* Bei der Z-Pufferung werden Z-Koordinaten verwendet, die so konvertiert wurden, dass sie in ein 24-Bit-Tiefenpufferintervall passen. Z-Koordinaten werden linear über Polygone interpoliert (mit einigen Kuriositäten, aber sie sind nicht besonders wichtig). Auch hier gibt es nichts Ungewöhnliches.
* Bei der W-Pufferung werden W-Koordinaten "wie sie sind" verwendet. Moderne GPUs verwenden normalerweise 1 / W, aber DS verwendet nur Festkomma-Arithmetik, so dass die Verwendung von reziproken Werten nicht sehr praktisch ist. Wie auch immer, in diesem Modus werden die W-Koordinaten mit perspektivischer Korrektur interpoliert.
So sehen die endgültigen Rendering-Durchgänge aus:
* Kantenmarkierung: Pixel, für die Kantenflags gesetzt sind, erhalten eine Farbe aus der Tabelle, die anhand der ID eines undurchsichtigen Polygons bestimmt wird.
Sie sind farbige Kanten von Polygonen. Es ist anzumerken, dass die Kanten des Polygons immer noch farbig sind, wenn ein durchscheinendes Polygon über ein undurchsichtiges Polygon gezeichnet wird.
Ein Nebeneffekt des Kürzungsprinzips: Die Ränder, an denen sich Polygone mit den Rändern des Bildschirms schneiden, werden ebenfalls farbig. Dies können Sie beispielsweise in den Screenshots von Picross 3D feststellen.
* Nebel: Wird auf jedes Pixel angewendet, basierend auf den Tiefenwerten, die zum Indizieren der Nebeldichtetabelle verwendet werden. Wie Sie vielleicht erraten haben, gilt dies für Pixel, für die im Attributpuffer Nebelflags gesetzt sind.
* Antialiasing (Glättung): Wird auf die Kanten von (undurchsichtigen) Polygonen angewendet. Basierend auf den Steigungen der Kanten beim Rendern von Polygonen werden die Pixelabdeckungswerte berechnet. Im letzten Durchgang werden diese Pixel mit den Pixeln darunter gemischt, wobei der schwierige Mechanismus verwendet wird, den ich in einem früheren Beitrag beschrieben habe.
Antialiasing sollte auf der GPU nicht auf diese Weise emuliert werden (und kann es auch nicht), daher ist dies hier nicht wichtig.
Wenn Kantenmarkierung und Anti-Aliasing auf dieselben Pixel angewendet werden sollen, erhalten sie nur die Kantengröße, jedoch mit einer Deckkraft von 50%.
Ich habe den Renderprozess mehr oder weniger gut beschrieben. Wir haben uns nicht mit dem Mischen von Texturen (Kombinieren von Scheitelpunkt- und Texturfarben) befasst, aber es kann in einem Fragment-Shader emuliert werden. Gleiches gilt für Kantenmarkierung und Nebel, sofern wir mit einem Attributpuffer einen Weg um das gesamte System finden.
Aber im Allgemeinen wollte ich Folgendes vermitteln: OpenGL oder Vulkan (sowie Direct3D oder Glide oder irgendetwas anderes) werden hier nicht helfen. Unsere modernen GPUs haben mehr als genug Leistung, um mit rohen Polygonen zu arbeiten. Das Problem sind die Details und Merkmale der Rasterung. Und es geht nicht einmal um die Idealität der Pixel. Schauen Sie sich zum Beispiel den Issue-Tracker des DeSmuME-Emulators an, um zu verstehen, auf welche Probleme Entwickler beim Rendern über OpenGL stoßen. Wir müssen uns auch irgendwie mit diesen Problemen auseinandersetzen.
Ich stelle auch fest, dass wir mit OpenGL den Emulator beispielsweise auf Switch portieren können (weil ein Github-Benutzer namens Hydr8gon damit begonnen hat, einen
Port für unseren Emulator auf Switch zu erstellen).
Also ... wünsche mir viel Glück.