NVIDIA hat kürzlich seine neueste GPU-Architektur mit der Bezeichnung Turing vorgestellt. Obwohl das Headlining-Feature hardwarebeschleunigtes Raytracing ist, beinhaltet Turing auch einige andere Enwicklungen, die für sich genommen recht faszinierend aussehen.
Wenn Sie der englischen Sprache mächtig sind, empfehlen wir ihnen, sich das folgende Video anzusehen:
Eines davon ist das neue Konzept der Mesh Shader, dessen Details vor ein paar Wochen veröffentlicht wurden – und die Community der Grafikprogrammierer war aggressiv, mit vielen begeisterten Diskussionen auf Twitter und anderswo. Was sind Mesh-Shader (oder Task-Shader), warum sind Grafikprogrammierer so begeistert von ihnen und was können wir mit ihnen machen?
Die GPU-Geometrie-Pipeline ist überladen.
Der Prozess der Überlagerung von Geometriedreiecken, die an die GPU gezeichnet werden sollen, hat ein einfaches zugrunde liegendes Paradigma: Sie setzen ihre Nodes in einen Puffer, richten die GPU auf sie aus und geben einen Draw-Aufruf aus, um zu sagen, wie viele Primitive gerendert werden sollen. Die Nodes werden linear aus dem Puffer geschlürft, jeder wird von einem Vertex-Shader verarbeitet und die Dreiecke sind gerastert und schattiert.
Aber im Laufe der jahrzehntelangen GPU-Entwicklung wurden verschiedene zusätzliche Funktionen im Namen einer höheren Leistung und Effizienz an die Basis-Pipeline angeschraubt. Indexierte Dreiecke und Vertex-Caches wurden erstellt, um die Wiederverwendung von Vertexen zu nutzen. Komplexe Beschreibungen des Vertex-Stream-Formats sind erforderlich, um Daten für die Schattierung vorzubereiten. Die Instanzierung und später die Mehrfachziehung ermöglichten es, bestimmten Mengen von Draw-Aufrufen miteinander zu kombinieren. Indirekte Ziehungen konnten auf der GPU selbst erzeugt werden. Dann kamen die zusätzlichen Shaderstufen: geometrische Shader, um programmierbare Operationen auf Primitiven zu ermöglichen und sogar Primitive spontan einzufügen, zu löschen und anschließend Tessellierungs-Shader, mit denen Sie ein niedrigauflösendes Mesh einreichen und es dynamisch auf eine programmierbare Ebene unterteilen können.
Während diese und weitere Funktionen alle aus guten Gründen hinzugefügt wurden (oder zumindests aus dem, was damals als gute Gründe galten), ist die Verbindung von ihnen allen unhandlich geworden. Welche Teilmenge der vielen verfügbaren Optionen greifen Sie in einer bestimmten Situation an? Wird ihre Wahl über alle GPU-Architekturen hinweg effizient sein, auf denen ihre Software laufen muss?
Außerdem ist diese aufwändige Pipeline immer noch nicht so flexibel, wie wir es und manchmal wünschen – oder, wenn sie flexibel ist, ist sie nicht performant. Die Instanzierung kann nur Kopien eines einzelnen Meshes auf einmal zeichnen. Multi-Draw ist für eine große Anzahl kleinerer Draws immer noch effizient. Das Programmiermodell der Geometrie-Shader ist für eine effiziente Implementierung auf breiten SIMD-Cores in GPUs nicht geeignet und auch das Buffering von Ein- und Ausgängen bereitet Schwierigkeiten. Hardware-Tessellierung, obwohl für bestimmte Dinge sehr praktisch, ist oft schwierig zu handhaben, da die Granularität, mit der Sie die Tessellationsfaktoren einstellen können, begrenzt ist, die Anzahl der eingebauten Tessellierungsmodi und Performance-Probleme bei einigen GPU-Architekturen.
Einfachheit ist goldwert.
Mesh-Shader stellen eine radikale Vereinfachung der Geometrie-Pipeline dar. Wenn ein Mesh-Shader aktiviert ist, wenden alle oben beschriebenen Shader-Stufen und Funktionen mit fester Funktion weggefegt. Stattdessen erhalten wir eine saubere, unkomplizierte Pipeline mit einem Compute-Shader-ähnlichen Programmiermodell. Wichtig ist, dass diese neue Pipeline sowohl hochflexibel genug ist, um die bestehenden Geometrieaufgaben in einem typischen Spiel zu bewältigen, als auch neue Techniken zu ermöglichen, die heute auf dem Grafikprozessor schwierig sind – und es sieht so aus, als ob sie sehr leistungsfreundlich sein sollte, ohne sichtbare architektonische Barrieren für eine effiziente Grafikprozessorausführung.
Wie ein Compute Shader definiert ein Mesh Shader Arbeitsgruppen von parallel laufenden Threads und Sie können über On-Chip Shared Memory sowie Wave Instrinsics kommunizieren. Anstelle eines Draw-Aufrufs startet die App eine Reihe von Mesh-Shader-Arbeitsgruppen. Jede Arbeitsgruppe ist dafür verantwortlich, einen kleinen, in sich geschlossenen Teil der Geometrie, ein „Meshlet“ genannt, auszuarbeiten, das in Arrays von Vertex-Attributen und entsprechenden Indizes ausgedrückt wird. Diese Meshlets werden dann direkt in den Rasterizer geworfen.
Das Attraktive an diesem Modell ist, wie datengesteuert und freeform es ist. Die Mesh-Shader-Pipeline hat sehr entspannte Erwartungen an die Form ihrer Daten und die Art der Dinge, die Sie tun. Alles liegt in der Hand des Programmierers: Sie können die Node- und Indexdaten aus den Puffern ziehen, algorithmisch generieren oder beliebig kombinieren.
Gleichzeitig umgeht das Mesh-Shader-Modell die Probleme, die Geometrie-Shaders behindert haben, indem es die SIMD-Ausführung explizit übernimmt (in Form der Rechenabstraktion „Arbeitsgruppe“). Statt dass jeder Shader-Thread eine eigene Geometrie erzeugt – was zu Divergenz und In-/Outputdatengrößen führt – lässt man die gesamte Arbeitsgruppe ein Meshlet gemeinsam ausgeben. Das bedeutet, dass wir Tricks im Compute-Stil verwenden können, wie z.B. zuerst etwas parallel an den Nodes arbeiten, dann eine Barriere haben und anschließend parallel an den Dreiecken arbeiten. Es bedeutet auch, dass die Anforderungen an die Ein-/Ausgabebandbreite viel vernünftiger sind. Und da Meshlets indizierte Dreieckslisten sind, brechen sie die Wiederverwendung von Nodes nicht, wie es Geometrie-Shader oft taten.
Ein Upgrade-Pfad.
Die andere wirklich nette Sache über Mesh-Shader ist, dass sie nicht verlangen, dass Sie drastisch überarbeiten, wie ihre Spiel-Engine mit Geometrie umgeht, um sie zu nutzen. Es sieht so aus, als ob es ziemlich einfach sein sollte, die gängigsten Geometrietypen in Mesh-Shader zu konvertieren, was es zu einem zugänglichen Upgrade-Pfad für Entwickler macht.
(Sie müssen jedoch nicht sofort alles in Mesh-Shader umwandeln. Es ist möglich, zwischen der alten Geometrie-Pipeline und der neuen Mesh-Shader-basierten an verschiedenen Stellen im Rahmen zu wechseln.)
Angenommen, Sie haben ein gewöhnliches, verfasstes Mesh, das Sie laden und rendern möchten. Sie müssen es in Meshlets aufteilen, die eine statische Maximalgröße haben, die im Shader angegeben ist – NVIDIAs empfiehlt standardmäßig 64 Nodes und 126 Dreiecke.
Was gemacht werden muss.
Glücklicherweise führen die meisten Spiele-Engines derzeit eine Form der Vertex-Cache-Optimierung durch, die die Primitive bereits nach Lokalitäten organisiert – Dreiecke, die einen oder zwei Nodes teilen, werden im Indexpuffer eher eng beieinander liegen. Eine recht brauchbare Strategie für die Erstellung von Meshlets ist also: Scannen Sie einfach den Indexpuffer linear ab und sammeln Sie die Menge der verwendeten Nodes, bis Sie entweder 64 Nodes oder 126 Dreiecke erreichen. Setzen Sie sie zurück und wiederholen Sie sie, bis Sie das gesamte Mesh durchgegangen sind. Dies könnte während der Art-Build-Zeit geschehen oder es ist auch ausreichend, dass es während der Level-Ladezeit in der Engine erfolgt.
Alternativ können Vertex-Cache Optimierungsalgortihmen wahrscheinlich modifiziert werden, um Meshlets direkt zu erzeugen. Für GPUs ohne Mesh-Shader-Support können Sie alle Meshlet Vertex-Puffer zusammenfügen und schnell einen traditionellen Index-Puffer erzeugen, indem Sie alle Meshlet Index-Puffer kompensieren und verketten. Es ist ziemlich einfach, hin und her zu gehen.
In beiden Fällen würde der Mesh-Shader meist nur als Vertex-Shader fungieren, mit etwas zusätzlichem Code, um Vertex- und Index-Daten aus ihren Puffern zu holen und in die Mesh-Outputs zu stecken.
Was ist mit anderen Arten von Geometrie, die man in Spielen findet?
Instanziierte Draws sind einfach: Multiplizieren Sie die Anzahl der Meshlets und setzen Sie ein wenig Shader-Logik ein, um Instanzparameter auszuschließen. Ein interessanter Fall ist das Multi-Draw, bei dem wir viele Meshes zeichnen wollen, die nicht alle Kopien derselben Sache sind. Dazu können wir Task-Shader einsetzen – ein sekundäres Merkmal der Mesh-Shader-Pipeline. Task-Shader fügen eine zusätzliche Schicht von Arbeitsgruppen im Compute-Stil hinzu, die vor dem Mesh-Shader laufen und sie steuern, wie viele Mesh-Shader-Arbeitsgruppen gestartet werden sollen. Sie können auch Outputvariablen schreiben, die vom Mesh-Shader verwendet werden sollen. Ein sehr effizientes Multi-Draw sollte möglich sein, indem Task-Shader mit einem Thread pro Draw gestartet werden, die wiederum die Mesh Shader für alle Einzelziehungen starten.
Wenn wir viele sehr kleine Meshes zeichnen müssen, wie z.B. Quads für Partikel/Imposter/Text/Punktbasiertes Rendering oder Boxen für Occlusion-Tests/Projektionsaufkleber und was nicht, dann können wir einen Haufen davon in jede Mesh-Shader-Arbeitsgruppe packen. Die Geometrie kann vollständig in Shader generiert werden, anstatt sich auf einen vorinitialisierten Indexpuffer der CPU zu verlassen. Dies war einer der ursprünglichen Anwendungsfälle, von denen man sich erhofft hatte, dass sie mit Geometrie-Shadern durchgeführt werden könnten z.B. durch das Einreichen von Punkt-Primitiven und deren Erweiterung durch die GS in Quads). Es gibt auch eine Menge Flexibilität, um Dinge mit variabler Topologie zu bewerkstelligen, wie Partikelstrahlen/Streifen/Bänder, die ansonsten entweder auf der CPU oder in einem separaten Rechenvorlauf erzeugt werden müssten.
(Übrigens, der andere originelle Anwendungsfall, der hoffentlich mit Geoemtrie-Shadern gemacht werden konnte, war das Multi-View-Rendering: die gleiche Geometrie auf mehrere Flächen einer Cubemap oder auf Scheiben einer kaskadierenden Shading-Map innerhalb eines einzogen Draw-Aufrufs zeichnen. Sie könnten das auch mit Mesh-Shadern tun – aber Turing hat tatsächlich eine separate Hardware-Multi-View-Funktion für diese Anwendungen).
Was ist mit tessellierten Meshes?
Die zweischichtige Struktur von Aufgaben- und Mesh-Shadern ähnelt weitgehend der von Mosaikhüllen und Domain-Shadern. Es scheint zwar nicht, dass Mesh-Shader irgendeinen Zugang zur Tessellator-Einheit mit fester Funktion haben, aber es ist auch nicht allzu schwer vorstellbarm dass wir Code in Task/Mesh-Shader schreiben könnten, um die Tessellierungsfunktionalität zu reproduzieren. Die Details herauszufinden, wäre ein kleines Forschungsprojekt, denn vielleicht hat jemand bereits daran gearbeitet – und Perfektion wäre sehr fragwürdig. Wir würden jedoch den Vorteil haben, dass wir die Funktionsweise der Tessellierung ändern können, anstatt an dem festzuhalten, was Microsoft in den späten 2000er Jahren beschlossen hat.
Neue Möglichkeiten.
Es ist großartig, dass Mesh-Shader unsere aktuellen Geometrieaufgaben zusammenfassen und in einigen Fällen effizienter gestalten können. Aber Mesh-Shader eröffnen auch Möglichkeiten für neuartige Geometrieverarbeitung, die bisher auf dem Grafikprozessor nicht möglich gewesen wäre oder teure Rechenvorgänge erfordert hätte, bei denen Daten im Speicher gespeichert und anschließend über die traditionelle Geometrie-Pipeline wieder eingelesen wurden.
Da unsere Meshes bereits in Meshlet-Form vorliegen, können wir feinere Körnungen auf der Meshlet-Ebene und sogar auf der Dreiecksebene innerhalb jedes Meshlets durchführen. Mit Task-Shadern können wir möglicherweise eine Mesh-LOD-Auswahl auf dem Grafikprozessor durchführen und wenn wir Lust auf mehr haben, könnten wir sogar versuchen, sehr kleine Draws (von groben LODs) dynamisch zusammenfassen, um eine bessere Meshletauslastung zu erreichen.
Anstelle der kachelbasierten Vorwärtsbeleuchtung oder als Erweiterung dazu könnte es sinnvoll sein, Lichter pro Meshlet zu entfernen, vorausgesetzt, es gibt eine gute Möglichkeit, die Lichtliste mit variabler Größe von einem Mesh- bis zu einem Fragment-Shader zu übergeben.
Der Zugriff auf die Topologie im Mesh-Shader sollte es uns ermöglichen, dynamische Normals, Tangenten und Krümmungen für ein Mesh zu berechnen, das sich aufgrund von komplexem Skinning, Displacement Mapping oder prozeduralen Vertex-Animationen verformt. Wir können auch Voxel-Meshing oder Isosurface-Extraktion-Marching-Würfel oder Tetraeder durchführen, sowei Normals usw. für das Isosurface generieren – direkt in einem Mesh-Shader, zur Darstellung von Flüssigkeiten und volumetrischen Daten.
Geometrie für Haare/Pelz, Laub oder andere Oberflächenbedeckungen kann im Handumdrehen generiert werden, mit ansichtsabhängigen Details.
3D-Modellierung und CAD-Anwendungen können Mesh-Shader auf dynamisch triangulierte Quad- oder n-Gon-Meshes anwenden, sowie auf Dinge wie das dynamsiche Einfügen/Aussetzen von Geometrien für Visualisierungen.
Für die Darstellung von verschobenem Gelände, Wasser usw. können wir Mesh-Shader bei Geometrie-Clipmaps und Geomorphing unterstützen. Sie könnten auch für progressive Meshing-Systeme interessant sein.
Und last but not least könnten wir in der Lage sein, Catmull-Clarks Subdivision Surfaces oder andere Subdivision Schemata einfacher und effizienter zu rendern, als es heute auf der GPU möglich ist.
Sicher sind alle diese Dinge mit der neuen Mesh- und Task-Shader-Pipeline machbar. Es wird sicherlich algorithmische Schwierigkeiten und architektonische Hindernisse geben, die sich ergeben werden, wenn Grafikprogrammierer die Chance haben, sich damit auseinanderzusetzen. Dennoch sind wir sehr gespannt, was die Leute in den nächsten Jahren mit dieser Funktion machen werden und wir hoffen und erwarten, dass es nicht allzu lange ein exklusives NVIDIA-Feature bleiben wird.
Damit sind wir soweit mit unserem Beitrag zum Mesh-Shader durch. Wenn Sie noch Fragen oder Anmerkungen haben sollten, hinterlassen Sie uns unten einen Kommentar.
Vielen Dank für ihren Besuch.