Grafikkarten - Programmierung

Transcrição

Grafikkarten - Programmierung
RST-Labor WS06/07
Grakkarten - Programmierung
GPGPU
Version 1.3
20.02.2006
Marc Blunck 97129 <[email protected]>
Diese Ausarbeitung befasst sich mit der Programmierung von Grakprozessoren.
GPGPU meint General Purpose Computation On Graphics Processing Units. Hierbei
wird die GPU für Aufgaben verwendet, für die sie eigentlich nicht entwickelt wurde. Moderne programmierbare GPUs weisen heute eine hohe Komplexität auf und erreichen in
bestimmten Aufgabenbereichen höhere Rechenleistungen als CPUs. Angetrieben durch
die Computerspieleindustrie werden sie in einem höheren Tempo weiterentwickelt. Nachdem ihre Programmierung nicht mehr ausschlieÿlich durch Assembler-ähnliche Sprachen
erfolgt, erönen sich stetig weitere attraktive Anwendungsfelder, welche sich nicht nur auf
graphische Probleme beschränken. Gerade die Nutzung der GPU zur Programmierung
allgemeiner rechenintensiver Vorgänge ist ein neues und attraktives Feld.
RST-Labor WS06/07
GPGPU
Inhaltsverzeichnis
1 Einführung
4
2 Die GPU
6
2.1
Die GPU im Computersystem . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.2
Aufbau einer GPU (Architektur)
8
2.3
. . . . . . . . . . . . . . . . . . . . . . .
2.2.1
Die Geometrieeinheit . . . . . . . . . . . . . . . . . . . . . . . . . .
9
2.2.2
Die Rastereinheit . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10
2.2.3
Die Textur- und Fragmenteinheit . . . . . . . . . . . . . . . . . . .
11
2.2.4
Der Speicher
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
2.2.5
Streams
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
GPU-CPU Analogien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
3 Die Programmierung
16
3.1
Datenstrukturen auf der GPU . . . . . . . . . . . . . . . . . . . . . . . . .
18
3.2
Multidimensionale Arrays
19
3.3
Programmiersprache C for Graphics
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . .
19
4 Experimenteller Teil
Mandelbrotmenge in OpenGL
21
4.1
Einführung
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
4.2
OpenGL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
4.2.1
OpenGL Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . .
23
4.2.2
OpenGL Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
4.2.3
Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
4.2.4
Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
4.2.5
4.3
Bibliotheken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
25
Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
4.3.1
Konguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
4.3.2
main.cpp
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28
4.3.3
Vertex Shader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
4.3.4
Fragment Shader . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
4.3.5
Juliamenge
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
4.3.6
ergänzende Literatur . . . . . . . . . . . . . . . . . . . . . . . . . .
40
5 Weitere Programme
Marc Blunck
41
2
RST-Labor WS06/07
GPGPU
5.1
ATI SDK
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
41
5.2
FFFF Fast Floating Fractal Fun . . . . . . . . . . . . . . . . . . . . . . .
43
5.3
GPULab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
5.4
ATI Avivo Xcode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
5.5
GPUSort
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
5.6
BrookGPU
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
6 Fazit
48
Literaturverzeichnis
49
Abbildungsverzeichnis
51
Tabellenverzeichnis
52
A Quellcode
53
A.1
main.cpp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Marc Blunck
53
3
RST-Labor WS06/07
GPGPU
1 Einführung
Die Weiterentwicklung von CPUs und Speicher bereitet viele Probleme. Trotz der Erweiterung an Transistoren in CPUs gehen Geschwindigkeitssteigerungen immer langsamer voran. Um Berechnungen eektiv durchführen zu können, sollten möglichst viele
Transistoren verwendet werden und man sollte versuchen Parallelität zu nutzen. Beim
Betrachten der Funktionsweise eines Grakprozessors (Graphics Processing Unit, GPU),
stellt man fest, dass hierbei genau die genannten Kriterien erfüllt werden.
Es gibt hauptsächlich zwei Gründe, warum Grakkarten für die Durchführung von allgemeinen Rechenaufgaben verwendet werden sollten:
1. Performance
2. Lastenverteilung
Abbildung 1.1: Vergleich GFlops [2]
1. Performance:
Die neuen Dual-Core CPUs etwa besitzen 2 Prozessoren und unterstützen 1-2 Hardware Threads. Eine aktuelle GPU besitzt mehr als 24 Pipelines und kann hunderte von
Hardware Threads bewältigen. Auÿerdem sind GPUs allgegenwärtig und billig. Grakprozessoren benden sich in jedem Desktop PC, in jedem Notebook, in PDAs und Mobiltelefonen. Als Vergleich: die GeForce 6 Series GPUs erreichen Peak- Werte von über 100
GFlops, der nahezu gleichzeitig erschienene Pentium 4 erreicht Werte von gerade einmal
12 GFlops [1], [2], [5]. Siehe auch Abbildung 1.1. Der Vorteil der Verwendung der GPU
gegenüber der CPU liegt also hauptsächlich in deren gröÿeren Geschwindigkeit.
Marc Blunck
4
RST-Labor WS06/07
GPGPU
2. Lastenverteilung:
Bei modernen Rechnern tritt auÿerdem oft der Fall ein, dass die GPU auf die CPU
warten muss. Es gibt daher keinen Grund, warum nicht allgemeine Rechenaufgaben von
der CPU auf die GPU ausgelagert werden sollten.
Marc Blunck
5
RST-Labor WS06/07
GPGPU
2 Die GPU
Um aktuellen Computerspiele oder aufwendigen CAD-Anwendungen genügend Grakrechenleistung zur Seite stellen zu können, sind in den letzten Jahren immer Leistungsfähigere GPUs entwickelt worden. Die GPU berechnet dreidimensionale Szenen, um daraus
zweidimensionale Projektionen zu berechnen, welche auf dem PC-Monitor dargestellt
werden. GPUs sind echte Prozessoren auf dem Niveau der CPUs, denen sie entsprechen.
Beispielsweise enthält Nvidias Geforce 3 57 Mio. Transistoren bei einem 0,15 Mikrometer
Fertigungsprozess, während Intels Pentium 4 mit Willamette Kern zu diesem Zeitpunkt
42 Mio. Transistoren bei 0,18 Mikrometer enthielt. Da Fertigungsprozesse mit höherer
Dichte schnellere Transistoren ermöglichen, war zumindest physikalisch betrachtet die
Geforce 3 der leistungsstärkere Prozessor.
3D-Anwendungen arbeiten im Grakbereich objektorientiert. Die Objekte bestehen vereinfacht gesprochen lediglich aus einer Ansammlung von Zahlen - den Koordinaten der
Eckpunkte der Dreiecke, aus denen das Objekt zusammengesetzt ist. Dreiecke sind die
typischerweise eingesetzten Primitive bei 3D-Berechnungen. Ein Dreieck besteht aus drei
Punkten, die wiederum jeweils vier Koordinaten (x, y, z, w) besitzen. Die Koordinaten x,
y, z beschreiben die räumliche Lage der Punkte, wobei die w-Koordinate die homogene
Koordinate ist. Sie ist nötig, um bestimmte Operationen, wie zum Beispiel Matrixmultiplikation, schneller ausführen zu können. Ein Objekt wird als Ganzes bearbeitet und
kann manipuliert werden.
Mögliche Manipulationen sind:
•
Veränderung der Gröÿe
•
Drehung um eine (mehrere) Achse(n)
•
Verschiebung an eine andere Stelle
Die Operationen werden durch eine Vektor-Matrix-Multiplikation durchgeführt. Der neue
Dreieckeckpunkt entsteht, indem der alte Dreieck Eckpunkt mit einer
4x4
Matrix multi-
pliziert wird. Alle Punkte eines Objekts werden mit derselben Matrix multipliziert. Die
Berechnung dieser
4x4
Matrix wird in der CPU des PCs durchgeführt. Anschlieÿend
wird diese Matrix an die GPU übergeben. Die GPU kann die Berechnung nun durchführen. Pro Dreieck Eckpunkt wird eine Multiplikation eines Vektors mit einer
4x4
Matrix
durchgeführt. Das ergibt 16 Multiplikationen und 12 Additionen - also 28 Floating-Point-
Marc Blunck
6
RST-Labor WS06/07
GPGPU
Operationen. Typische Computerspielszenen bestehen aus 100.000 oder mehr Dreiecken.
Bei für Computerspielen typischen 50 Bildern pro Sekunde kommt man auf 100.000 * 3 *
28 * 50 =420 Mio. FP-Operationen/sek. Die GPU GeForce 6800 Ultra schat laut NVidia 600 Millionen Dreieck Eckpunkte pro Sekunde. Zu den durchgeführten Operationen
sind keine Angaben gemacht.
2.1 Die GPU im Computersystem
Ältere Grakkarten besaÿen sehr unausgeglichene Kommunikationskanäle. Der Systembus war nur dafür konzipiert Daten an den Monitor auszugeben. Für das Programmieren
auf der GPU war dies natürlich sehr unvorteilhaft. Daten mussten schnellst möglich wieder zurückieÿen können. Die GPU ist heutzutage über den PCI Express oder dem AGP
Slot mit dem Motherboard verbunden. Da über diese Slots alle Daten von der CPU zur
GPU übertragen werden müssen, wurden diese Technologien in den letzten Jahren immer
weiter entwickelt. Der einfache AGP Slot weist einen Takt von 66 MHz auf und war 32 Bit
breit und bringt es auf eine Übertragungsrate von 264 MB/sek. Bei den Weiterentwicklungen AGP 2X, AGP 4X und AGP 8X sind diese Werte zu multiplizieren. 2004 wurde
PCI Express mit einer maximalen theoretischen Bandbreite von 4 GB/sek eingeführt. Es
folgt eine Übersicht über die verschiedenen Komponenten und deren Bandbreite. [1]
Abbildung 2.1: Architektur eines PC, [5]
Marc Blunck
7
RST-Labor WS06/07
GPGPU
Komponente
GPU Memory Interface
PCI Express Bus
(×16)
CPU Memory Interface (800 MHz Front-Side Bus)
Bandbreite
35GB/sek
8GB/sek
6.4GB/sek
Tabelle 2.1: Übersicht Bandbreite
Algorithmen, die also auf so einer GPU ausgeführt werden, können eine drastische Leistungsverbesserung erzielen.
2.2 Aufbau einer GPU (Architektur)
Abbildung 2.2: Aufbau GPU [5]
Die Abbildung 2.2 zeigt den Aufbau einer modernen GPU der GeForce 6 Serie. Grakkarten besitzen zwei programmierbare Prozessortypen. Dies sind zum einen die VertexProzessoren und zum anderen die Fragment-Prozessoren. Beide sind auf der GPU mehrfach vertreten. Sie sind freiprogrammierbar und können auf GPUs auch nicht-graphische
rechenintensive Anwendungen ausführen. Die NVIDIA GeForce 6800 Ultra und die ATI
Marc Blunck
8
RST-Labor WS06/07
GPGPU
Radeon X800 XT haben beispielsweise jeweils 6 Vertex-Prozessoren und 16 FragmentProzessoren.
Der Host (CPU) schickt Befehle, Texturen und Vertex-Daten an die GPU. Die VertexProzessoren transformieren die Dreieckpunkte in einem Objekt so, wie es von dem Benutzer gewünscht ist. Die Cull/ Clip/ Setup-Einheit entfernt Primitive, die in dem Objekt
überhaupt nicht sichtbar sind, formt Kanten und bereitet die Daten für die Rastereinheit vor. An diesem Punkt ist die Grak jedoch erst ein so genanntes Drahtgittermodell.
Die Dreiecke werden nun in der Rastereinheit gerastert (abgetastet und in Bildschirmpunkte überführt). Anschlieÿend ieÿen die Daten in die Textur- und Fragmenteinheiten.
Hier werden die Bildpunkte mit zusätzlichen Farbkomponenten, Eekten usw. versehen.
Ehe ein fertig gerendertes Objekt in dem Frame-Buer abgelegt wird, muss es noch den
Tiefentest (Z-Compare-and-Blend) bestehen. Das heiÿt, wenn zwei Fragmente von zwei
Objekten sich überlappen, werden ihre Tiefen verglichen. Dargestellt wird das Fragment
mit der geringsten Tiefe. Die Objekte werden eins nach dem anderen in dem FrameBuer abgespeichert, der sich in dem Speicher bendet. Wenn die Szene fertig berechnet
ist, holt das RAMDAC das Bild ab und stellt es auf dem Monitor dar. [1]
2.2.1 Die Geometrieeinheit
Abbildung 2.3: Vertex-Prozessor einer GeForce 6 Serie [5]
Vertex-Prozessoren nutzen SIMD- oder MIMD-Parallelisierung, und verarbeiten Streams
(s. 2.2.5) von Vertices mithilfe von Vertex-Programmen. Nach der Verarbeitung wird
aus jeweils 3 Ausgabe-Vertices ein Dreieck errechnet. Der Vertex-Prozessor hat einen
eigenen Befehlssatz und ist wie erwähnt frei programmierbar. Der typische Befehlssatz
umfasst mehrere Operationen der linearen Algebra, die in der Grakprogrammierung
häug Verwendung nden, unter anderem das Skalarprodukt. Die meisten Befehle sind
Marc Blunck
9
RST-Labor WS06/07
GPGPU
parallel ausgelegt, operieren also auf mehreren Werten gleichzeitig (SIMD, Single Instruction Multiple Data). um die Performance zu steigern. Die Länge des Programms, dass
er ausführen kann, ist begrenzt, da der Speicher begrenzt ist. Bei der GPU der GeForce
6 Serie beträgt sie 512 statische und 65536 dynamische Instruktionen. Statische Instruktionen sind nichts anderes als ein Programmcode, der nach dem Kompilieren entsteht,
dynamische Instruktionen sind die Instruktionen, die tatsächlich ausgeführt werden. Jeder Vertex-Prozessor hat ein eigenes Instruktionsregister und ist in der Lage, pro Takt
eine Vector4 MAD (multiply add) und eine Spezialfunktion (EXP, LOG, SIN, usw.) auf
Skalare auszuführen. Auÿerdem steht ein Registersatz mit 32 Registern mit je vier 32 Bit
breiten Werten zur Verfügung. Ferner hat der weist der Prozessor einen Vertex-Cache
auf, in dem er beide (Originale und von ihm veränderte) Vertex-Daten speichert. So
wird der Rechenaufwand für den Fall, dass dieselben Vertex-Daten noch ein Mal für die
Berechnung anstehen, reduziert. In diesem Fall benutzt er einfach die Daten aus dem
Cache. Sprünge und Schleifen gehören jetzt auch zu dem
Shader-Modell.
Sie benötigen
nur zwei Takte. Das Neue an dem Vertex-Shader ist Möglichkeit die Texturen aus dem
Textur-Cache zu holen und zu verarbeiten. Es sind meistens mehrere parallel arbeitende
Vertex-Prozessoren in einer GPU vorhanden. Bei der GeForce 6 Serie kann die Anzahl
zwischen zwei (low-end) und sechs (hi-end) variieren. [5]
2.2.2 Die Rastereinheit
Abbildung 2.4: Rasterisierung und früher Z-Test
Nach der Errechnung eines Dreiecks im Vertex-Prozessor wird daraus ein Stream von
Fragmenten generiert. Diese Arbeit wird vom Rasterizer übernommen. Es ndet der
Übergang von Dreieckeckpunkten zu Bildpunkten statt. Je nach gewählter Bildschirmauösung wird das Dreieck durch unterschiedlich viele Punkte repräsentiert. Zusätzlich
wird es berechnet, welche Pixel von anderen Primitiven verdeckt sind. Diese Pixel werden
dann ausrangiert (s. Abbildung 2.4).
Marc Blunck
10
RST-Labor WS06/07
GPGPU
2.2.3 Die Textur- und Fragmenteinheit
Abbildung 2.5: Fragment-Prozessor und Texel-Pipeline [5]
Der Fragment.Prozessor ist analog zum Vertex-Prozessor ein nach dem SIMD-Prinzip
(SIMD-Parallelisierung) arbeitender Spezialprozessor in der GPU. Er hat einen eigenen
(zum Vertex-Shader identischen) Befehlssatz und Register. Er belegt die Bildpunkte, die
aus der Raster-Einheit kommen, mit Eigenschaften wie Farbe, Transparenz, Reexionsverhalten, usw. Genau wie der Vertex-Prozessor hat er einen Block (Texture-Prozessor),
um Texturdaten aus dem Speicher zu holen und einen Textur-Cache, um Speicherzugrie
zu beschleunigen. Fragment-Prozessoren können Daten in Form von Texturen lesen. Die
Textur-Einheit kann also als ein Read-Only Memory Interface angesehen werden. Der wesentliche Unterschied besteht darin, dass der Pixel-Shader zwei fp32 (Floating-Point mit
32 Bit) Shader-Einheiten pro Pipeline hat. Die erste hat vier MUL-Kanäle und zusätzlich eine Einheit für Spezialfunktionen (SFU). Die zweite hat vier MUL-Kanäle gefolgt
von vier ADD-Kanälen, Parallel und unabhängig dazu kann eine SFU ihre Berechnungen
durchführen.
Allgemein erlauben die zwei Einheiten bis zu acht mathematischen Operationen pro Takt.
Am Ende der Pipeline bendet sich die Fog-ALU, die dazu genutzt wird, Nebel in das
Bild einzublenden, ohne dabei an Leistung zu verlieren. Das ist nur dadurch möglich,
weil diese Aktion nur Festkommaoperationen benötigt, deren Realisierung in Hardware
ezient und einfach realisiert werden kann. Das Rendering in der GPU der GeForce 6
Serie wird gleichzeitig auf vier Pixel (Quads) durchgeführt. Deswegen hat jede Texturund Fragmenteinheit (Quad-Pipline) vier Pixel- Shader (Abbildung 2.6). Genau wie die
Vertex-Prozessoren sind die Quad-Piplines skalierbar. [5]
Marc Blunck
11
RST-Labor WS06/07
GPGPU
Abbildung 2.6: Eine Textur- und Fragmenteinheit hat 4 Pixel-Shader [5]
2.2.4 Der Speicher
Abbildung 2.7: Speicheraufteilung bei der GPU der GeForce 6 Serie [5]
Der Speicher der GPU ist in vier unabhängigen Partitionen aufgeteilt. Jede Partition ist
64 Bit breit und hat ihre eigene DRAMs (Abbildung 2.7.). Aus Kostengründen verwenden
die GPUs Standard DRAMs eher als Spezial-RAM. Die Breite des Speicher beträgt 256
Bit (64 Bit x 4) und bei 550 MHz Taktfrequenz erreicht der Datentransfer phänomenale
35 GB/s. Dadurch, dass die Partitionen klein und unabhängig sind, können sie ezient
einen Datenblock holen, egal welche Gröÿe er hat. Die Speichermenge wird hauptsächlich für Texturen benötigt. Häug wird die gleiche Textur mehrfach in unterschiedlichen
Auösungen im Speicher vorgehalten, um dem sog. Level of Detail gerecht zu werden.
Bei dieser Technik werden Objekte, die sich weiter entfernt benden, nicht so detailliert
dargestellt wie Objekte, die sich sehr nah am Betrachter benden. Um Rechenleistung
beim Runterrechnen der Textur zu sparen bzw. um nicht benötigte (weil nicht sichtbare)
Details auch wirklich weglassen zu können, wird diese Technik eingesetzt. Der Speicher
beinhaltet des Weiteren zwei wichtige Buer:
•
Frame-Buer und
•
Z-Buer
Im Z-Buer wird zu jedem Bildpunkt, der im späteren Bild dargestellt wird, eine Tie-
Marc Blunck
12
RST-Labor WS06/07
GPGPU
feninformation vorgehalten. Wenn nun ein neues Objekt berechnet worden ist und ein
Bildpunkt dieses Objekts auf demselben Bildpunkt des Bildschirms dargestellt werden
soll, wie ein Bildpunkt eines anderen bereits berechneten Objekts, entscheidet der im
Z-Buer gespeicherte Tiefenwert, welcher Bildpunkt genommen wird. Der Frame-Buer
besteht aus zwei Buern: Front-Buer und Back-Buer. Im Front-Buer wird eine Kopie des aktuell auf dem Bildschirm dargestellten Bildes gespeichert. Aus ihm holt der
RAMDAC sich die Bildinformationen. In den Back-Buer wird das sich gerade in der
Berechnung bendliche Bild gespeichert. Wenn das ganze Bild fertig berechnet ist, werden die beiden Buer umgelabelt. Der alte Front-Buer wird zum neuen Back-Buer und
der alte Back-Buer wird neuer Front- Buer. [5],[6]
2.2.5 Streams
Innerhalb einer GPU existieren vier unterschiedliche Arten von Streams:
•
Vertex-Streams
•
Frame-Buer-Streams
•
Texture-Streams
•
Fragment-Streams
Der Textur-Stream nimmt insofern eine Sonderrolle ein, als er der einzige Stream ist,
der den Fragment-Prozessoren und Vertex Shader 3.0 vorausgesetzt, auch den VertexProzessoren einen wahlfreien Zugri erlaubt. Fragment-Programme sind daher in der
Lage Rechenergebnisse (Output-Streams) in den Textur-Buer zu schreiben und direkt
wieder als Eingabe für neue Berechnungen zu verwenden. Daraus ergibt sich auch gleich
ein Vorteil der Fragment-Prozessoren gegen über den Vertex-Prozessoren für den general
purpose-Einsatz, denn wie aus Abbildung 2.8 ersichtlich, können Fragment-Prozessoren
mehr oder weniger direkt in den Textur-Stream schreiben, wohingegen Daten vom VertexProzessor erst noch den Rasterizer und dann selber den Fragment-Prozessor durchlaufen
müssen, bevor sie im Textur-Buer abgespeichert werden können. Die GPU selber kann
mit Ausnahme der Fragment-Streams auf alle anderen Streams schreibend und lesen zugreifen. Wobei auch hier Daten vorteilhafterweise direkt in den Textur-Buer geschrieben
werden sollten, damit sie direkt als Eingabedaten für die Fragment-Programme zur Verfügung stehen. Der Fragment-Stream nur innerhalb der GPU verwendet wird und daher
für den Programmierer nicht sichtbar ist. [7]
Marc Blunck
13
RST-Labor WS06/07
GPGPU
Abbildung 2.8: GPU Streams [7]
2.3 GPU-CPU Analogien
Texturen = Arrays
Die grundlegende Array-Datenstruktur auf der GPU sind Texturen. Da Texturen 4 Farbkanäle haben, bilden sie eine natürliche Datenstruktur für Vektordatentypen. Das Lesen
eines Elements des Arrays würde bei der CPU mittels eines Speicherzugri geschehen,
auf der GPU entspricht dies nun einem Texture-Lookup.
Fragment Programme = Das Innere der Schleifen
Wenn man auf der CPU auf alle Elemente eines Arrays zugreifen will, so benutzt man im
allgemeinen Schleifen und iteriert sich so durch alle Elemente. Auf der GPU können wir
dies parallelisieren und erreichen dadurch einen Performancegewinn. Dies wird auf der
GPU durch den Einsatz eines Fragment-Programmes (auch Kernels genannt), welches
auf alle Elemente eines Streams angewendet wird, erreicht. Der erzielbare Performancegewinn, hängt von der Anzahl eingesetzter Fragmentprozessoren ab.
Render-to-Texture = Feedback
Bei iterativen Algorithmen wird immer der Wert der vorhergehenden Rechnung an die
neue Rechnung übergeben. Dies kann man auf der CPU recht einfach über einen Speicherzugri lösen. Um auf der GPU dieses Feedback zu erreichen, benutzen wir Render-to
Texture. Auf diese Art können wir die Ergebnisse einer Rechnung in den Speicher schreiben, so dass sie von zukünftigen Programmen gelesen werden können.
Geometrische Berechnungen = allgemeine Berechnungen
Um ein Programm zum Laufen zu bringen, müssen wir wissen, wie wir unsere Berechnungen aufrufen können. Wir wissen, wie Fragmentprogramme aussehen, aber wie erzeugen
wir daraus einen Stream von Fragmentprogrammen? Wir benutzen dazu die Geometrie.
Während in der CPU Daten in eindimensionalen Arrays verarbeitet werden, ist das Da-
Marc Blunck
14
RST-Labor WS06/07
GPGPU
tenformat in der GPU ein zweidimensionales Array. Daher muss das eindimensionale
Array in ein zweidimensionales konvertiert werden. Die Vertex-Prozessoren transformieren dabei die Geometrie und der Rasterizer bestimmt, welche Pixel im Output Buer
betroen sind und generiert für jeden Pixel ein Fragment. Bei GPGPU rechnen wir viel
auf Gitternetzen, deswegen benutzen wir zum Aufrufen der Berechnungen oft ein Viereck.
[8]
Marc Blunck
15
RST-Labor WS06/07
GPGPU
3 Die Programmierung
Anfangs waren Grakkarten nur dazu da, grasche Anwendungen auszuführen. Damals
war es unmöglich andere Arten von Daten auf dem Grakprozessor zu verarbeiten. Mit
der Einführung von programmierbaren Shader-Pipelines auf modernen GPUs änderte
sich dies jedoch. Das Stream-Modell, welches die Basis für die Programmierung auf GPUs
darstellt, erreicht durch Strukturierung der Programme hohe Ezienz für Berechnungen
und Kommunikation. Daten werden hierbei durch Streams repräsentiert. Alle Operationen, die auf Streams ausgeführt werden können, werden durch Kernel ausgeführt, die auf
kompletten Streams arbeiten. Die Eingabe sowie die Ausgabe eines Kernels können aus
einem oder mehreren Streams bestehen. Programme im Stream-Modell sind so geschrieben, dass mehrere Kernel aneinander gekettet sind. Auf der Grak-Pipeline muss hierfür
ein Vertex-Kernel geschrieben werden, so dass das Ergebnis eines Kernels als Eingabe
des Nächsten genutzt werden kann. In Abbildung 3.1 kann man sehen, dass zum einem
Taskparallelität genutzt wird, d.h. die Kernel arbeiten parallel an unterschiedlichen Daten. Weiterhin wird die Datenparallelität genutzt, d.h. ein Kernel führt parallel die gleiche
Aufgabe auf unterschiedlichen Streamelementen aus (z.B. 6 parallele Vertexprozessoren
bzw. 16 Fragmentprozessoren).
Abbildung 3.1: Stream-Modell [6]
Eziente Berechnungen
Durch Streams wird Parallelität in die Berechnungen gebracht. Da Kernel auf kompletten
Streams arbeiten, können mehrere davon, auf einer Hardware, die dies erlaubt, parallel
verarbeitet werden. Bei sehr langen Streams, die viele Elemente enthalten, kann dies noch
ezienter durchgeführt werden, wie z.B. Mandelbrotmenge.
Marc Blunck
16
RST-Labor WS06/07
GPGPU
Eziente Kommunikation
O-Chip-Kommunikation ist sehr viel ezienter, wenn komplette Streams, anstatt von
einzelnen Elementen, vom oder zum Hauptspeicher transferiert werden, da die Initialisierungszeit für einen Datentransfer nur einmal auftritt. Durch die Strukturierung von
Programmen als Kernel-Ketten können Resultate zwischen Kernels direkt ausgetauscht
werden, ohne dass diese zuerst in den Hauptspeicher geladen werden müssen.
Auf der GPU werden so genannte Shader-Programme eingesetzt. Sie wenden visuelle Effekte auf ein Pixel oder ein geometrisches Muster an. Jede Pipeline einer GPU kann mit einem Shader-Programm belegt werden. Auf den 24 Pipelines des NVIDIA 7800GTX Grakprozessors zum Beispiel können Programme mit ca. 65000 Shader-Instruktionen/Sek.
pro Pipeline ausgeführt werden. [3]
Der Grund dafür, dass die CPU nicht komplett durch die GPU ersetzt wird, liegt darin,
dass die GPU nur auf einer bestimmten Art von Daten besonders schnell Operationen
ausführen kann. Die Abarbeitung dieser Daten muss zum einen mit einer hohen Parallelität und zum anderen mit einer hohen arithmetischen Intensität möglich sein. Das
bedeutet, dass die Daten unabhängig von einander berechnet werden müssen und dass
die Anzahl der zur Abarbeitung der Daten benötigten mathematischen Operationen im
Vergleich zu den benötigten Speicherzugrien wesentlich höher sein sollte. Der Grund
dafür, dass Grakkarten gerade auf dieser Art von Daten besonders schnell Operationen
ausführen können, liegt darin, dass die GPU natürlich hauptsächlich zur Bearbeitung von
Grakdaten entwickelt wurde, die naturgemäÿ diese Eigenschaften aufweisen. Soll z.B.
die Helligkeit eines Bildes erhöht werden, so kann natürlich die Helligkeit jedes einzelnen
Pixels unabhängig von jedem anderen Pixel erhöht werden. Um eine möglichst eziente Abarbeitung solcher Daten zu ermöglichen, wurden GPUs als so genannte StreamProzessoren konzipiert. Stream-Prozessoren arbeiten, im Gegensatz zu normalen seriellen
Prozessoren, immer auf kompletten Datenströmen (Stream = Array gleicher Daten, s.
2.2.5.). Wobei die GPU etwas von diesem Modell abweicht und auch in der Lage ist, auf
bestimmte Speicherbereiche wahlfrei zuzugreifen. Der groÿe Unterschied zwischen den
seriellen- und den Stream-Prozessoren besteht also darin, dass serielle Prozessoren auf
beliebigen Datenelementen arbeiten und daher auf eine geringe Latenz in den Berechnungen optimiert wurden. Stream-Prozessoren hingegen arbeiten auf groÿen homogenen
Datenmengen und sind auf einen hohen Durchsatz optimiert. [7]
Eine Grakkarte besitzt also zwei unterschiedliche programmierbare Prozessoren: Die
Vertex- und die Fragment-Prozessoren (siehe 2.2). Beide Prozessoren sind voll programmierbar (die auszuführenden Instruktionen können also durch den Programmierer angegeben werden und sind nicht durch die Hardware festgelegt) und auf die Bearbeitung
von Vektoren mit vier Komponenten spezialisiert. Der Unterschied zwischen beiden Prozessoren besteht darin, dass Fragment-Prozessoren, im Gegensatz zu Vertex-Prozessoren,
in der Lage sind, aus beliebigen Speicherbereichen innerhalb des Textur-Buers zu lesen
(Gather), da jedoch die Output- Adresse schon vom Vertex-Prozessor festgelegt wird,
sind sie nicht in der Lage, in selber bestimmte Speicherbereiche zu schreiben (Scat-
Marc Blunck
17
RST-Labor WS06/07
GPGPU
ter). Bei Vertex-Prozessoren verhält es sich genau umgedreht. Zwar können auch VertexProgramme in neueren Grakkarten mittels Vertex Shader 3.0 lesend und schreibend
auf den Textur-Buer zugreifen, allerdings sind die so geschriebenen Daten nur dazu da
die bearbeiteten Vertex-Daten aufzunehmen und können sonst nicht frei verwendet werden. Das liegt daran, dass Texturen entweder nur Lesend oder nur Schreibend verwendet
werden können. Ein weiterer Unterschied besteht darin, dass Fragment-Prozessoren ausschlieÿlich im SIMD-Modus arbeiten, wohingegen Vertex-Prozessoren auch im MIMDModus arbeiten können. Der Fragment-Prozessor muss also auf allen Elementen eines
Streams immer die exakt gleichen Instruktionen ausführen. Bei moderneren GPUs wird
dieser Nachteil aber durch Pixel Shader 3.0 etwas abgeschwächt, wodurch z.B. Schleifen
variabler Länge möglich sind. Trotzdem werden für GPGPU hauptsächlich die FragmentProzessoren verwendet. Das hat mehrere Gründe. Zum einen enthalten moderne Grakkarten in der Regel mehr Fragment- als Vertex-Prozessoren und zum anderen sind
Fragment-Prozessoren in der Lage Daten nach der Berechnung innerhalb eines weiteren
Renderpass in den Textur-Buer zu schreiben, was den Vorteil hat, dass Ausgabedaten
direkt wieder als Eingabedaten verwendet werden können.
3.1 Datenstrukturen auf der GPU
Bei der CPU-Programmierung ist man gewohnt, eine Vielzahl von Datentypen wie z.B.
Integers, Floats, Booleans usw. zur Verfügung zu haben. Obwohl einige High-Level GPUSprachen auch Datentypen wie Integer und Boolean anbieten, arbeiten aktuelle GPUs
intern ausschlieÿlich mit reellen Zahlen in Form von Fest- oder Flieÿkommazahlen (NVIDIA verwendet aktuell ein 16 und ein 32-Bit Format. ATI verwendet 24-Bit). Werden also
Datentypen wie z.B. Integer verwendet, so müssen diese immer in eine Gleitkommzahl
konvertiert werden, was durchaus nicht immer problemlos möglich ist. So kann z.B. der
Wertebereich eines 32-Bit-Integer Wertes nicht komplett in eine 32-Bit Flieÿkommazahl
abgebildet werden, da diese nur über 23 Mantissen-Bits verfügt. (Das standardisierte 32Bit-Format besteht aus einem Vorzeichen-, 23-Mantissen und 8-Exponenten-Bits, wobei
die Anzahl der Mantissen-Bits die Genauigkeit festlegt, mit der eine Zahl dargestellt werden kann.) Hat man nur das 16-Bit-Format (10 Mantissen-Bits) oder das 24-Bit-Format
(16 Mantissen-Bits) zur Verfügung, so ergibt sich aufgrund des sehr beschränkten IntegerWertebereichs, der mit diesen Formaten dargestellt werden kann, ein weiteres Problem.
Steht z.B. nur das 16-Bit-Format zur Verfügung, so ist es, einfach weil überhaupt nicht so
groÿe Integer-Werte dargestellt werden können, nicht möglich, vollständig auf ein Array
(also auf einen Textur-Stream innerhalb des Textur-Buers) mit z.B. 100000 Elementen
zuzugreifen. Des Weiteren muss man sich bewusst sein, dass Flieÿkommazahlen mit dem
16- und dem 24-Bit-Format nicht mit derselben Genauigkeit, wie man es von der normalen CPU-Programmierung her gewöhnt ist, berechnet werden können, was aber für die
Grak-Programmierung ohnehin ausreicht. Für genauere Berechnungen muss man neue
Datentypen denieren, welche die Genauigkeit erhöhen. [7]
Marc Blunck
18
RST-Labor WS06/07
GPGPU
3.2 Multidimensionale Arrays
Die GPU stellt sowohl ein-, zwei- als auch dreidimensionale Texturen (Arrays) als Datenformate zu Verfügung. Da es sich bei einem Textur-Stream praktisch um ein Array
gleicher Daten handelt, dürfte die Repräsentation von Arrays dieser Dimensionen innerhalb der GPU eigentlich kein Problem darstellen. In der Praxis gibt es allerdings zwei
Gründe warum ein-, drei- und höherdimensionale Arrays nicht in ein- und dreidimensionale Texturen abgebildet werden sollten. Zum einen stellen zurzeit aktuelle GPUs nur
eine zweidimensionale Rasterisierung und nur zweidimensionale Frame-Buer zur Verfügung und zum anderen besitzen die Texturen eine festgelegte maximale Gröÿe von
4.096 Elementen pro Dimension. Sollen also Arrays beliebiger Dimensionen oder andere
beliebige Datenstrukturen innerhalb der GPU verwendet werden, so ist es sinnvoll sie
in zwei dimensionale Texturen abzubilden. Dadurch ist zum einen eine wesentlich einfachere Verarbeitung der Texturen möglich und zum anderen können im Vergleich zu
eindimensionalen Texturen wesentlich mehr Array-Elemente abgespeichert werden. Wird
ein eindimensionales Array in einer zweidimensionalen anstatt in einer eindimensionalen
Textur abgespeichert kann es anstatt 4096 bis zu 4096·4096 Elemente enthalten. [7]
3.3 Programmiersprache C for Graphics
Cg , oder C for Graphics wurde von NVIDIA entwickelt. Cg basiert auf der Programmiersprache C und besitzt die gleiche Syntax wie C. Allerdings wurden einige Eigenschaften
von C modiziert und C wurde durch einige Datentypen, wie z.B. half, xed u. sampler,
ergänzt, um Cg besser für GPGPU anzupassen. Das Toolkit [13] bietet die Möglichkeit,
Code für verschiedene Plattformen, darunter Windows und Linux, zu erstellen. Anfangs
wurden Shader Programme in einer Assembler Sprache geschrieben. Die Verwendung von
Cg bringt jedoch viele Vorteile mit sich. High-Level Code ist einfacher zu erlernen, zu programmieren, zu lesen und zu verstehen. Auÿerdem ist Cg -Code auf sehr viele verschiedene
Plattformen portierbar, und der Code kann von Compilern optimiert werden, was bei Assembler Code nicht immer möglich ist. Cg besitzt sechs Basis-Datentypen. Einige davon
stammen aus der Programmiersprache C, andere wurden extra für die Programmierung
auf der GPU hinzugefügt. Die sechs Datentypen sind in Tabelle 3.1 zusammengefasst.
Marc Blunck
19
RST-Labor WS06/07
GPGPU
Datentyp Beschreibung
oat
32 Bit Flieÿpunktzahl
half
16 Bit Flieÿpunktzahl
int
32 Bit Integer
xed
12 Bit Fixpunktzahl
bool
Boolean Variable
sampler
Repräsentiert ein Textur-Objekt
Tabelle 3.1: Cg Datentypen
Auÿerdem gibt es noch Vektor- und Matrix- Datentypen, die aber auf den Basis-Datentypen basieren. Cg unterstützt viele Operatoren, einschlieÿlich der üblichen arithmetischen Operatoren aus C. Des Weiteren gibt es noch Operatoren für die Vektor- und
Matrix- Datentypen und die üblichen logischen Operatoren. C for Graphics teilt die
Basis-Kontroll-strukturen, wie if/else, while und for, mit C. Auch die Denition von
Funktionen ist ähnlich wie in C. [4] Neben der umfangreichen mitgelieferten Dokumentation und Beispielprogrammen existiert noch ein Tutorial des Mathematikprofessors Herrn
Göddeke der Uni Dortmund. [13]
Marc Blunck
20
RST-Labor WS06/07
GPGPU
4 Experimenteller Teil
Mandelbrotmenge in OpenGL
4.1 Einführung
Berechnet man die Mandelbotmenge komplett auf der GPU, so hat man einen groÿen
Performancegewinn, weil die Berechnung auf vielen Pixeln parallel stattnden kann. Um
die Manldebrotmenge zu verstehen folgt hier nochmal eine kleine Wiederholung:
Keine reelle Zahl, die mit sich selbst multipliziert wird, kann einen negativen Wert ergeben. Die imaginäre Zahl
j
wird deniert mit der Quadratwurzel aus
−1.
Mit
j
kann
3j
−9 und andererseits ist die Quadratwurzel von −9 gleich 3j . Zahlen, die
realen Zahl und einer imaginäre Zahl bestehen, wie 6 + 4j , werden komplexe
die Quadratwurzel jeder negativen Zahl leicht beschrieben werden. Zum Beispiel ist
quadriert gleich
aus einer
Zahlen genannt. Arithmetische Operationen können an den komplexen Zahlen, ebenso
wie auf reelen Zahlen gerade durchgeführt werden. Das Resultat des Multiplizierens von
zwei komplexen Zahlen ist wie folgt:
x = a + bj
y = c + dj
xy = ac + adj + cbj − bd
= (ac − bd) + (ad + bc)j
Komplexe Zahlen enthalten also zwei Teile und können daher zweidimensional abgebildet werden. Auf der
x-Achse
wird der Realteil und auf der
y -Achse
der Imaginärteil
abgebildet.
Marc Blunck
21
RST-Labor WS06/07
GPGPU
Abbildung 4.1: Komplexe Zahlen
In der Abbildung 4.1 sieht man drei Symbole, die auf der Fläche der komplexen Zahl
abgebildet werden. Ein kleines Quadrat wird an der komplexen Zahl 2 + j abgebildet,
ein kleiner Kreis bei -1 + 2j und ein Dreieck wird bei -1.5 - 0.5j abgebildet.
Ein Mathematiker, der Benoit Mandelbrot genannt wurde, entwarf eine rekursive Funktion, die komplexe Zahlen mit einbezieht:
Z0 = c
Zn+1 = Zn2 + c
Für jede Iteration wird der Wert von
Wert für
Z
Z
quadriert und zu
c
hinzugefügt, um einen neuen
zu erhalten. Diese erstaunlich einfache Formel produziert das, was als das
komplexeste Objekt in der Mathematik gilt, die Mandelbrotmenge.
Abbildung 4.2: Mandelbrotmenge [23]
Marc Blunck
22
RST-Labor WS06/07
GPGPU
4.2 OpenGL
Die OpenGL Shading Language (OGSL) hat ihre Wurzeln in C. Ein OpenGL Code beabsichtigt die Ausführung auf einem der programmierbaren Prozessoren, den Shadern. Die
Bezecihnung OpenGL Shader wird dabei benutzt um zu unterscheiden, ob der Shader in
OGSL oder in einer anderen Sprache, wie z.B. RenderMan geschrieben ist. Da es zwei
programmierbare Prozessoren gibt, sind in OpenGL zwei Typen von Shadern implementiert: Fragment Shader (*.frag ) und Vertex Shader (*.vert ).
OpenGL stellt Mechanismen um Shader zu kompilieren zur Verfügung und um diese von
Ausführbaren Code zu verlinken.
4.2.1 OpenGL Datentypen
OpenGL arbeitet intern mit spezischen Datentypen für ganze Zahlen und Gleitkommenzahlen. Es gibt zwar inzwischen eine IEEE-Norm für Gleitkommaarithmetik, aber eine
Variable vom Typ
int
ist manchmal eine 16-Bit Gröÿe, manchmal eine 32-Bit Gröÿe, je
nach Betriebssytem und Compiler. Tabelle 4.3 enthält die einzelnen Datentypen. [21]
Abbildung 4.3: OpenGL Datentypen und Suxes [21]
Marc Blunck
23
RST-Labor WS06/07
GPGPU
4.2.2 OpenGL Variablen
Einfache Variablen können wie gewohnt deklariert werden.
float foo, bar;
int baz = 5;
bool bla = false;
Allerdings gibt es in GLSL, anders als zum Beispiel in C, keine automatischen Typecasts
beim Initialisieren von Variablen, stattdessen wird stark auf das Konzept von Konstruktoren zurückgegrien, dem von C++ nicht unähnlich:
float a = 2; //falsch! kein automatische typecasting
int b = 2; //richtig
Für Variablen gibt es auÿerdem einige Qualier, die hier kurz erläutert werden sollen:
const
- Kompilierzeitkonstante, kann nicht geändert werden.
uniform
- globale Variable welche von der Applikation an den Shader übergeben wird,
kann in Vertex und Fragment Shadern gelesen, aber nicht geschrieben werden.
attribute - globale Variable welche von OpenGL an den Vertex Shader übergeben wird,
kann nur in Vertex Shadern gelesen, aber nicht verändert werden.
varying
- Variable zum Austausch interpolierter Daten vom Vertex Shader zum Frag-
ment Shader.
4.2.3 Vektoren
Vektoren der Typen
float, int
oder
bool
sind built-in Basistypen. Sie können aus
zwei, drei oder vier Komponenten bestehen und werden wie folgt benannt:
vec2
vec3
vec4
ivec2
ivec3
Vektor von zwei Gleitkommazahlen
Vektor von drei Gleitkommazahlen
Vektor von vier Gleitkommazahlen
Vektor von zwei Ganzzahlen
Vektor von drei Ganzzahlen
Marc Blunck
24
RST-Labor WS06/07
ivec4
bvec2
bvec3
bvec4
GPGPU
Vektor von vier Ganzzahlen
Vektor von zwei Booleans
Vektor von drei Booleans
Vektor von vier Booleans
Vektoren sind sehr nützlich für das Speichern und Manipulieren von Farben, Positionen
und Koordinaten (s. Programmbeispiel).
4.2.4 Matrizen
Matrizen aus Gleitkommazahlen sind ebenfalls für als Built-in Typen vorhanden. Es
gibt
2x2, 3x3
mat2
mat3
mat4
2x2
3x3
4x4
und
4x4
Gröÿen.
Matrix einer Gleitkommazahlen
Matrix einer Gleitkommazahlen
Matrix einer Gleitkommazahlen
4.2.5 Bibliotheken
OpenGL bietet nur eine sehr eingeschränkte Menge von Methoden mit, die alle nur
sehr grundlegende Funktionen abbilden. Damit nicht jeder Benutzer aufbauend auf diesen Methoden Standardfunktionalität implementieren muss, wurden eine Vielzahl von
Bibliotheken entwickelt, welche das Benutzen von OpenGL vereinfachen sollen. Auch
stellt OpenGL keinerlei Methoden zur Erzeugung von Fenstern, des Kontextes oder zur
Behandlung von Benutzereingaben an. Im Folgenden sollen drei dieser Bibliotheken vorgestellt werden:
GLUT
OpenGL enthält keine Funktionen, die in irgendeiner Weise mit dem verwendeten Fenstersystem kommunizieren. Es gibt natürlich die Möglichkeit, OpenGL mit dem jeweils
verwendeten Oberächen-System zu verheiraten. Doch hierfür wurde GL Utility Toolkit
oder kurz GLUT entwickelt. Diese Bibliothek hat das Ziel, eine einheitliche Schnittstelle zum verwendeten Fenstersystem anzubieten. Der Schwerpunkt liegt hier nicht auf der
Performanz, sondern auf Portabilität. GLUT steht auf alle relevanten UNIX-Plattformen,
Marc Blunck
25
RST-Labor WS06/07
GPGPU
insbesondere LINUX, MacOS und Microsoft Windows zur Verfügung. Wie für OpenGL
gibt es eine Bibliothek und eine Header-Datei.
#include <GL/glut.h>
Diese Datei stellt sicher, dass die OpenGL Header-Dateien ebenfalls hinzugefügt werden.
Glew
Die GLew (The OpenGL Extension Wrangler Library) ist eine plattformunabhängig
C/C++ Bibliothek, die das Laden und Verwalten von Extensions vereinfacht. Sie bietet eziente Methoden um zur Laufzeit zu ermitteln, welche Extensions vom System
unterstützt werden.
GLU
GLU ist die OpenGL Utility Library. Sie soll Grakfunktionen höherer Ebene, wie die
Abbildung zwischen Bildschirm- und Weltkoordinaten, die Generierung von TextureMipmaps, das Zeichnen von Quadriken sowie Transformationen zur einfacheren Erstellung und Verwaltung von Projektionsebenen und Kamera anbieten. GLU wird, wie auch
OpenGL selbst, vom ARB speziziert und ist Teil eines vollständigen OpenGL-Paketes.
Marc Blunck
26
RST-Labor WS06/07
GPGPU
4.3 Programm
4.3.1 Konguration
Das Programm erfordert eine Grakkarte mit OpenGL 2.0 Unterstützung. Ausserdem
müssen Glow [16] und OpenGL [18] Bibliotheken installiert sein. Für die Entwicklung
wurde Microsoft Visual Studio 2003 (v7.1.6030) mit Microsoft .NET Framework 1.1
(v1.1.4322 SP1) verwendet. Die erforderlichen Bibliotheken sind ins Verzeichnis:
../Microsoft Visual Studio.NET 2003/Vc7/PlatformSDK/Lib
zu kopieren. Die entsprechenden OpenGL Headerdateien in folgendes Verzeichnis:
../Microsoft Visual Studio.NET 2003/Vc7/PlatformSDK/Include/gl
Die dll sind ins Verzeichnis: Windows/system zu kopieren. Diese Dateien
werden meist
standardmäÿig mit anderen Programmen wie z.B. der NVIDIA SDK [19] oder ATI SDK
[20] mit installiert. Die ATI SDK beinhaltet ebenfalls eine Implemtierung der Mandelbrotmenge, worin die Unterschiede in Geschwindikeit und Genauigkeit der GPU - CPU
visualisiert werden (siehe 5.1).
Unter den Projekteigenschaften sind die zu benutzenden Bibiotheken auszuwählen.
Abbildung 4.4: Visual Studio Projekteigenschaften
Marc Blunck
27
RST-Labor WS06/07
GPGPU
4.3.2 main.cpp
Die Datei
main.cpp dient Hauptsächlich dafür die Shader zu linken. Es gibt Programme,
welche das direkete Linken von Shadern ermöglichen (siehe 4.3.6). Es folgt eine kleine
Übersicht der verwendeten Funktionen.
Quellcode siehe Anhang A.1
glMatrixMode
glLoadIdentity
glViewport
gluPerspective
glClear
- Legt fest, welche Matrix gerade aktiv ist.
- Ersetzt die aktuelle Matrix durch die Identitätsmatrix.
- Beschreibt das aktuelle Betrachtungsfenster.
- Erstellt eine perspektivische Projektionsmatrix.
- Leert die im Parameter festgelegten Buer, indem sie mit einen Leerwert gefüllt
werden.
gluLookAt
glPushMatrix, glPopMatrix
- Deniert eine Betrachtertransformation.
- Legen die aktuelle Matrix auf den Stack bzw. nehmen
sie wieder herunter.
glBegin, glEnd
- Umschliessen die Eckpunkte (Vertices) einer Primitiven, oder eine
Gruppe gleicher Primitiven.
glTexCoord
glCreateShader
glShaderSource
glCompileShader
glCreateProgram
glAttachShader
glLinkProgram
- setzen die aktuellen Texturkoordinaten.
- Erstellt ein Shaderobjekt.
- Ersetzt den Quellcode eines Shaderobjektes.
- Kompiliert ein Shaderobjekt.
- Erstellt ein Programmobjekt.
- Hängt ein Shaderobjekt an ein Programmobjekt.
- linkt die momentan an ein Programmobjekt gebundenen Shader, um
daraus einen Satz ausführbarer Shader zu erstellen.
Marc Blunck
28
RST-Labor WS06/07
glUseProgram
GPGPU
- Aktiviert Shader und ersetzt die passenden Teile der festen Funkti-
onspipeline durch diese.
glGetUniformLocation
glClearColor
- liefert den Ablageort einer uniform- Variable.
- Legt die Farbe fest welche ein Farbpuer nach seiner Leerung mit glClear
enthält.
Ausserdem wird hier z.B. das Fenster erzeugt, OpenGL Fehler abgefangen und LogDateien geschrieben. Diese Datei soll hier nicht weiter beschrieben werden, da das Hauptaugenugenmerk auf die Shaderprogramme gerichtet werden soll.
Marc Blunck
29
RST-Labor WS06/07
GPGPU
4.3.3 Vertex Shader
Wegen seiner universellen Programmierbarkeit kann der Vertex-Prozessor auch benutzt
werden, um eine Vielzahl anderer Berechnungen durchzuführen. Shader, die auf diesen
Prozessor laufen, werden Vertex Shader genannt. Der Vertex Shader stellt die Operationen zur verfügung, die auf jedem Vertex angewandt werden. Um den Vertex Shader zu
denieren, muss man sich drei Fragen stellen:
1. Welche Daten müssen zum Vertex Shader für jeden Vertex (d.h. Attributvariablen)
übermittelt werden?
2. Welcher globaler Zustand wird durch den Vertex Shader angefordert (d.h. konstante
Variablen)?
3. Welche Werte werden durch den Vertex Shader berechnet (d.h. unterschiedliche
Variablen)?
Es ist nicht möglich Geometrie zu zeichnen, ohne einen Wert für jede Vertexposition zu
denieren. Ebenso kann man keine Beleuchtung denieren, solange man kein Normal
für jede Position hat, die man beleuchten möchte. Also brauch man eine Vertexposition
und ein Normal für jeden ankommenden Vertex. Diese Attribute sind bereits als Teil von
OpenGL deniert und die OpenGL Shading Language liefert eingebaute Variablen, um
sich auf sie zu beziehen (gl_Vertex und
gl_Normal).
Man benötigt die Position einer einzelnen Lichtquelle. Dazu wird die Lichtquelleposition
als konstante Variable deniert:
uniform vec3 LightPosition;
Man benötigt ebenfalls Werte für die Lichtberechnungen, welche die Spiegelrefexionen
und der Diusereexion darstellen. Diese werden als konstante Variablen deniert, damit
sie durch die Anwendung dynamisch geändert werden konnten:
uniform float SpecularContribution;
uniform float DiffuseContribution;
Schlieÿlich müssen die Werte deniert werden, die an den Fragment Shader weitergeleitet
werden. Jeder Vertex Shader muÿ die homogene Vertexposition berechnen und den Wert
im die Standardvariable
gl_Position
speichern.
Die meisten der Beleuchtungberechnungen werden im Vertex Shader durchgeführt, nachdem die Farbe der Mandelbrotmenge im Fragment Shader berechnet wurde, wird die
Berechnung der Lichtintensität mit der Variablen
LightIntensity
zum Fragment Sha-
der geschickt. Diese zwei unterschiedlichen Variablen werden so deniert:
varying float LightIntensity;
varying vec2 MCposition;
Marc Blunck
30
RST-Labor WS06/07
GPGPU
Weiter geht es mit der Hauptfunktion des Vertex Shaders:
void main()
{
vec3 ecPosition = vec3(gl_ModelViewMatrix * gl_Vertex);
In dieser ersten Zeile des Codes wird die Variable
ecPosition
deniert, um die Koor-
dinatenposition des ankommenden Vertex zu halten. Die Koordinateposition wird durch
die Vertexposition (gl_Vertex) und die gegenwärtige Modelview-Matrix
(gl_ModelViewMatrix) berechnet. Weil eine der Rechengröÿen eine Matrix ist und die
andere ein Vektor führt der * Operator eine Matrixmultiplikation anstatt einer
Kompnentenweise-Multiplikation durch. Das Ergebnis der Matrixmultiplikation ist vom
Typ
vec4,
aber
ecPosition
wird als
vec3
deniert. Da es keine automatische Umwand-
lung zwischen Variablen von unterschiedlichen Typen in OpenGL gibt, wird das Ergebnis
mit einem Konstruktor in
vec3
umgewandelt.
Um die Diusereexion zu berechnen, muss man den Winkel zwischen dem ankommenden
Licht und der Oberäche berechnen. Um die Spiegelreexion zu berechnen, muss man
den Winkel zwischen der Reexions Richtung und der Betrachtungs Richtung berechnen.
Zuerst wird das ankommenden Normal umgewandelt:
vec3 tnorm = normalize(gl_NormalMatrix * gl_Normal);
Diese Zeile deklaiert eine neue Variable
tnorm,
um das transformierte Normal zu spei-
chern. Das ankommende Oberächen Normal (gl_Normal, eine eingebaute Variable um
auf die Werte des Normals Zugang zu erhalten) wird durch die Matrix
gl_NormalMatrix
umgewandelt. Der resultierende Vektor wird (umgewandelt in einen Vektor der Maÿeinheit Länge) durch die eingebaute Funktion
wird im
tnorm
normalize
normalisiert, und das Resultat
gespeichert.
Als nächstes wird ein Vektor vom gegenwärtigen Punkt auf der Oberäche des dreidimensionalen Gegenstandes berechnet, der in die Lichtquelleposition übertragen wird. Beide
von denen sollten in Form von Koordianten sein, was bedeutet, dass der Wert für die
konstante Variable
LightPosition
von der Anwendung in Koordinaten zur Verfügung
gestellt werden muÿ.
vec3 lightVec = normalize(LightPosition - ecPosition);
Die Gegenstandposition in Koordinaten wurde vorher berechnet und in
ecPosition
ge-
speichert. Um den Lichtrichtungs Vektor zu berechnen, subtrahiert man die Objektposition von der Lichtposition. Der resultierende Lichtrichtungsvektor wird in der lokalen
Variablen
lightVec
Marc Blunck
gespeichert.
31
RST-Labor WS06/07
GPGPU
Mit den umgewandelten Oberächennormalen und dem Ereignisvektor kann man jetzt
einen Reexionsvektor an der Oberäche des Gegenstandes berechnen. Allerdings erfordert
reflect
noch einen weiteren Vektor (die Richtung vom Licht zur Oberäche) und
wir haben die Richtung zur Lichtquelle berechnet. Das Negieren von
lightVec
gibt uns
den korrekten Vektor:
vec3 reflectVec = reflect(-lightVec, tnorm);
Weil beide Vektoren, die in dieser Berechnung verwendet wurden, Einheitsvektoren waren, ist der resultierende Vektor ebenfalls ein Einheitsvektor. Um unsere Beleuchtungberechnung abzuschlieÿen, benötigt man einen weiteren Einheitsvektor in der Richtung der
Betrachtungsposition. Weil per Denition die Betrachtungsposition im Ursprung liegt
(d.h. (0.0.0)) kann man
ecPosition
einfach negieren und normalisieren:
vec3 viewVec = normalize(-ecPosition);
Mit diesen vier Vektoren kann man eine Beleuchtungberechnung durchführen. Das Verhältnis dieser Vektoren ist in Abbildung 4.5 zu sehen.
Abbildung 4.5: Vektoren für Lichtberechnung [22]
Marc Blunck
32
RST-Labor WS06/07
GPGPU
mandel.vert:
vec3 LightPosition = vec3(0.0, 0.0, 3.0);
float SpecularContribution = 0.1;
float DiffuseContribution = 0.9;
float Shininess = 0.2;
varying float LightIntensity;
varying vec3 Position;
void main()
{
vec3 ecPosition
vec3 tnorm
vec3 lightVec
vec3 reflectVec
vec3 viewVec
float spec
spec
LightIntensity
}
Position
gl_Position
Marc Blunck
=
=
=
=
=
=
=
=
vec3 (gl_ModelViewMatrix * gl_Vertex);
normalize(gl_NormalMatrix * gl_Normal);
normalize(LightPosition - ecPosition);
reflect(-lightVec, tnorm);
normalize(-ecPosition);
max(dot(reflectVec, viewVec), 0.0);
pow(spec, Shininess);
DiffuseContribution *
max(dot(lightVec, tnorm), 0.0) +
SpecularContribution * spec;
= vec3(gl_MultiTexCoord0 - 0.5) * 5.0;
= ftransform();
33
RST-Labor WS06/07
GPGPU
4.3.4 Fragment Shader
Der typische Zweck eines Fragment Shaders ist die Farbe oder den Tiefenwert zu berechnen, welche auf ein Fragment angewendetet werden.
Der Fragment Shader implementiert den bereits vorgestellten Algorithmus der Mandelbrotmenge. Konstante Variable stellen die maximale Anzahl von Wiederholungen und
den Ausgangspunkt bzw. Startpunkt Mandelbrotmenge dar. Die Anwendung verwendet
konstante Variablen für eine Farbe innerhalb und für zwei Farben ausserhalb der Menge. Für Werte ausserhalb der Menge werden die Farben
OuterColor2
OuterColor1
aufsteigend bis
verwendet. Sie sind in 20 Schritten unterteilt, dieser Zyklus wird wieder-
holt, falls die Anzahl der Iterationen über 20 geht.
Der Shader bildet die
x-Koordinate der berechneten Position in die Fläche der komplexen
Position.x) zur realen Zahl in der wiederholenden Funktion
Zahlen ab (d.h. der Wert in
und zur
y -Koordinate
zur imaginären Zahl. Nachdem die Ausgangsbedingungen initiali-
siertt worden sind, geht der Shader in eine Schleife mit zwei Ausgangskriterien: Erreichen
der Höchstzahl der erlaubten Wiederholungen oder wenn sich der Punkt ausserhalb der
Menge bendet. Innerhalb der Schleife wird die Funktion
Z2 + c für die nächste Iteration
berechnet. Nach der Schleife wird die Farbe des Fragments berechnet.
mandel.frag:
varying vec3 Position;
varying float LightIntensity;
uniform float MaxIterations;
uniform float Zoom;
uniform float Xcenter;
uniform float Ycenter;
vec3 InnerColor = vec3(1.0, 1.0, 0.0);
vec3 OuterColor1 = vec3 (0.0, 0.0, 0.0);
vec3 OuterColor2 = vec3(0.0, 1.0, 0.0);
void main(void)
{
float real
float imag
float Creal
float Cimag
=
=
=
=
Position.x * Zoom + Xcenter;
Position.y * Zoom + Ycenter;
real;
//
imag;
//
float r2 = 0.0;
Marc Blunck
34
RST-Labor WS06/07
GPGPU
float iter;
for (iter = 0.0;iter < MaxIterations && r2 < 4.0;++iter)
{
float tempreal = real;
real = (tempreal * tempreal) - (imag * imag) + Creal;
imag = 2.0 * tempreal * imag + Cimag;
r2 = (real * real) + (imag * imag);
}
vec3 color;
if (r2 < 4.0)
color = InnerColor;
else
color = mix(OuterColor1, OuterColor2, fract(iter * 0.05));
color *= LightIntensity;
}
gl_FragColor = vec4 (color, 1.0);
Marc Blunck
35
RST-Labor WS06/07
GPGPU
Abbildung 4.6: Mandelbrotmenge
Abbildung 4.7: Mandelbrotmenge mit verändertem Zoomfaktor
Marc Blunck
36
RST-Labor WS06/07
GPGPU
4.3.5 Juliamenge
Die Juliamenge hängt mit der Mandelbrotmenge zusammen. Jeder Punkt der Mandelbrotmenge kann benutzt werden, um die Juliamenge zu berechen, welche ebenso interessant ist. Die Mandelbrotmenge ist gewissermaÿen eine Beschreibungsmenge der Juliamengen. Wenn man die Mandelbrotmenge graphisch darstellt, entspricht jedem Punkt
C
in der komplexen Zahlenebene eine Juliamenge. Eigenschaften der Julia-Menge las-
sen sich an der Lage des Punktes innerhalb der Mandelbrot-Menge abschätzen. Wenn
der Punkt
auch
C
Teil der Mandelbrot-Menge ist, dann sind sowohl die Julia-Menge
Jc
als
Kc zusammenhängend. Andernfalls sind beide Cantormengen unzusammenhängenC in der Nähe des Randes der Mandelbrotmenge liegt, dann ähnelt die
der Punkte. Falls
entsprechende Juliamenge den Strukturen der Mandelbrot-Menge in der näheren Umgebung von
C.
[24]
Hierzu müssen im Quellcode des Fragment Shaders lediglich zwei Zeilen angepasst werde:
varying vec3 Position;
varying float LightIntensity;
uniform float MaxIterations;
uniform float Zoom;
uniform float Xcenter;
uniform float Ycenter;
vec3 InnerColor = vec3(1.0, 1.0, 0.0);
vec3 OuterColor1 = vec3 (0.0, 0.0, 0.0);
vec3 OuterColor2 = vec3(0.0, 1.0, 0.0);
void main(void)
{
float real = Position.x * Zoom + Xcenter;
float imag = Position.y * Zoom + Ycenter;
float Creal = real;
float Cimag = imag;
float r2 = 0.0;
float iter;
for (iter = 0.0;iter < MaxIterations && r2 < 4.0;++iter)
{
float tempreal = real;
real = (tempreal * tempreal) - (imag * imag) + Creal;
Marc Blunck
37
RST-Labor WS06/07
GPGPU
imag = 2.0 * tempreal * imag + Cimag;
r2 = (real * real) + (imag * imag);
}
vec3 color;
if (r2 < 4.0)
color = InnerColor;
else
color = mix(OuterColor1, OuterColor2, fract(iter * 0.05));
color *= LightIntensity;
}
gl_FragColor = vec4 (color, 1.0);
Die folgenden Abbildungen zeigen die Juliamenge mit dem Mandelbrot Shader.
float Creal = 0.32;
float Cimag = 0.043;
Abbildung 4.8: Juliamenge
Marc Blunck
38
RST-Labor WS06/07
GPGPU
float Creal = -1.36;
float Cimag = 0.11;
Abbildung 4.9: Juliamenge
Marc Blunck
39
RST-Labor WS06/07
GPGPU
Abbildung 4.10: Juliamenge mit verändertem Zoomfaktor
Für dieses Kapitel wurden Informationen aus folgenden Quellen verwendet: [17], [18],
[21], [22], [23]
Weitere Informationen entnehme man bitte der OpenGL-Funktionsübersicht bzw. der
Spezikationen.
4.3.6 ergänzende Literatur
Shaderanalyse mit CaradNext - IDE zur Bearbeitung von GLSL Shaderprogrammen
Glow Tutorial u. Installationshinweise
OpenGL Tutorial - Sehr gutes Einstiegstutorial
ATI Rendermonkey - Shader programmieren + testen
TycoonLabs ShaderDesigner
Marc Blunck
- Ebenfalls ein gutes Shaderwerkzeug.
40
RST-Labor WS06/07
GPGPU
5 Weitere Programme
Es gibt eine Vielzahl an Programmen, die die GPU nutzen. Eine kleine Auswahl folgt:
5.1 ATI SDK
ATIs Software Developer's Kit beinhaltet einige Programmbeispiele darunter ebenso die
Mandelbrotmenge. Hier werden die Unterschiede zwischen der GPU und der CPU deutlich sichtbar, da diese jeweils nebeneinander gerechnet werden.
Abbildung 5.1: ATI SDK Mandelbrotmenge
Marc Blunck
41
RST-Labor WS06/07
GPGPU
Es ist möglich in das Bild zu zoomen, wobei man schon rein subjektiv das Gefühl hat,
dass die GPU leichte Geschwindigkeitsvorteile gegenüber der CPU hat. Zoomt man immer weiter in das Bild herein sieht man bald die begrentze Rechengenauigkeit der GPU.
Abbildung 5.2: ATI SDK Mandelbrotmenge
Marc Blunck
42
RST-Labor WS06/07
GPGPU
5.2 FFFF Fast Floating Fractal Fun
[10] FFFF ist der weltweit schnellste Generator der Mandelbrotmenge. Es werden verschiedene Optimierungen von Prozessoren genutzt, unter anderem SSE2 und 3DNow!,
sowie Multiprozessorunterstützung. Leider ist auf der Projektseite der Quellcode nicht
verfügbar, so dass sich zu der Arbeitsweise des Programms leider keine Aussagen treen
lassen. Die Änderung der Berechnung von normalem C Code zur Berechnung in der GPU
führt zu einer Änderung der Farben, warum das so ist, lässt sich allerdings nicht klären,
da der Quelltext nicht vorhanden ist.
Die eigentliche Farbgebung der Mandelbrot-Menge und ihrer Strukturen im Randbereich
ist nur mittels Computer möglich. Dabei entspricht jedem Bildpunkt ein Wert c der
komplexen Ebene. Der Computer ermittelt für jeden Bildpunkt, ob die zugehörige Folge
divergiert oder nicht. Sobald der Betrag eines Folgengliedes den Wert R=2 überschreitet,
divergiert die Folge. Die Zahl der Iterationsschritte N gemäÿ obiger Rekursionsformel,
nach denen das erfolgt, kann als Maÿ für den Divergenzgrad herangezogen werden. Über
eine zuvor festgelegte Farbtabelle, die jedem Wert N eine Farbe zuordnet, wird in diesem
Fall dem Bildpunkt eine Farbe zugewiesen.
Abbildung 5.3: FFFF Hauptbildschirm
Das ist der Bildschirm, nachdem das Programm gestartet wurde. Das Programm önet
ein DOS-Fester, in dem die Tastaturbefehle aufgeführt sind, mit dem man die Genauigkeit
der Berechnung verändern kann, man kann hinein- und hinauszoomen sowie die Art der
Berechnung ändern, ob diese in C, Assembler oder mit den unterstützten Erweiterungen
der Prozessoren erfolgen soll (SSE 1/2, 3DNow!).
Marc Blunck
43
RST-Labor WS06/07
GPGPU
So sieht die Ausgabe aus, wenn man die Berechnungsart auf Fragment Shader umgestellt
hat:
Abbildung 5.4: FFFF Fragment Shader Berechnung
Bei einer Umstellung auf Vertex Shader sieht die Ausgabe folgendermaÿen aus:
Abbildung 5.5: FFFF Vertex Shader Berechnung
Man erkennt in den beiden Bildern deutlich die veränderte Farbgebung gegenüber der
originalen Ausgabe. Dies lässt sich wie bereits oben erwähnt nicht erklären.
Die Berechnung der Mandelbrotmenge lässt sich insofern gut auf der GPU durchführen,
da dort die Stärken der parallenen Berechnung ausgeschöpft werden können. Die Berechnung würde sich gut parallelsieren lassen. Besispielsweise können verschiedene Quadranten parrallel berechnet werden.
Marc Blunck
44
RST-Labor WS06/07
GPGPU
5.3 GPULab
GPULab ist eine Zusatzbibliothek für GPGPU, um auf der GPU linearen Algebra schnell
lösen zu können. Um den Gebrauch der GPU zu erleichtern, weist GPULab eine matlabähnliche Sprache auf, weshalb es interessant gewesen wäre hiermit die Mandelbrotmenge
zu implementieren. Leider stehen notwenige Funktionen nicht zur Verfügung, da das
Projekt bisher nicht abgeschlossen wurde.
Hier sieht man die Bedienoberäche des Programms. [11]
Abbildung 5.6: GPULab [11]
GPULab verlangt beim Programmstart als Hardwarevoraussetzung eine ShaderModel
3.0 fähige Grakkarte, die bei mir leider nicht vorhanden war. Trotzdem lieÿ sich das
Programm starten, nach einer sehr einfachen Berechnung stürzte das Programm aber
ab.
Es wurde trotzdem der Versuch unternommen, einen Bubblesort-Algorithmus zu implementieren. Allerdings fällt der Einstieg aufgrund der nicht vorhandenen Dokumentation
recht schwer. Da das Programm eine Erweiterung von Matlab ist, wurde versucht, ein
Matlab Tutorial durchzuarbeiten, dies scheiterte allerdings schon an den Grundlagen, z.b.
beim Erstellen eines Arrays brach das Programm mit einer Fehlermeldung (Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt. bei Microsoft.DirectX.Direct3D.
Texture..ctor(Device device, Int32 width, Int32 height, Int32 numLevels, Usage usage,
Format format, Pool pool)) ab, nach mehreren erfolglosen Versuchen schloÿ sich das Programm aufgrund einer anderen Fehlermeldung (There is no pending code and the variable
on top of the stack is not a matrix neither a oat -> SystemNullReference Exception).
Unter diesen Vorraussetzungen erschien es nicht sinnvoll, dieses Projekt mangels unterstützender Hardware weiter zu verfolgen.
Marc Blunck
45
RST-Labor WS06/07
GPGPU
5.4 ATI Avivo Xcode
Neuer Video-Encoder von ATI, der die Rechenpower der Grakkarte nutzt und so das
Umrechnen von Filmen in andere Formate drastisch beschleunigt. Herkömmliche EncoderProgramme nutzen für die aufwändigen Berechnungen bei der Umkodierung eines Films
nur den Hauptprozessor des PCs. Der ATI-Encoder Avivo Xcode spannt zusätzlich Grakkarten aus der ATI-Reihe Radeon X1 für diese Berechnungen ein. Zur Zeit der Entstehung dieses Berichts werden die Berechnungen noch auf der CPU durchgeführt, die
Implementierung in die GPU soll im Januar 2007 folgen. Trotz dieser Einschränkung soll
die Geschwindigkeit beim enkodieren in das WMV9-Format etwa um das fünache höher
sein als beim Windows Media Encoder Leider konnten wir dies Programm nicht testen,
da Avivo Xcode nur auf Rechnern läuft, die eine Grakkarte mit Avivo-Architektur aus
der neuen Radeon-X1-Serie von ATI eingebaut haben. Dazu gehören die Radeon-Familien
X1300, X1600, X1800 in ihren aktuellen Varianten Pro, XT und All-in-Wonder. Hier sieht
man die Bedienoberäche des Programms und den Verlauf einer Komprimierung. [9]
Abbildung 5.7: ATI Avivo Xcode [9]
Marc Blunck
46
RST-Labor WS06/07
GPGPU
5.5 GPUSort
GPUSort ist ein für NVIDIA Grakkarten geschriebenes Programm, welches verschiedene Sortieralgorithmen anbietet, die alle in der Grakkarte ausgeführt werden. Auf der
Website des Projektes wird dafür geworben, dass die Implementierung schneller sein soll,
als qsort() auf einem 3,4 GHz Pentium mit einer NVIDIA 6800 Ultra GPU. Der Algorithmus soll in der Lage sein, Flieÿkommazahlen von 16 und 32 Bit Genauigkeit zu
berechnen. Allerdings ist die Gröÿe, abhängig vom Speicher der Grakkarte, limitiert.
Dieses Limit berechnet sich laut den Entwicklern von GPUSort folgendermaÿen: Maximale Arraylänge in Millionen = Video RAM in MB/16. Eine NVIDIA Grakkarte mit
256 MB Video RAM kann also ein Array mit 256/16 => 16 Millionen Werten sortieren. Aufgrund der Programmierung wird auch hier eine NVIDIA Karte vorausgesetzt,
da ATI Karten die Unterstützung für das sogenannte Framebuer Objekt fehlt. Weiter-
n komplett in der Grakkarte sortiert. Die
hin werden nur Arrays mit einer Gröÿe von 2
gröÿte Zweierpotenz, welche kleiner ist als der Array Index wird in der GPU sortiert,
der Rest wird in der CPU berechnet, das Ergebnis wird dann zusammengefügt. Daher
ist die Arraygröÿe sehr stark einuÿgebend auf die Geschwindigkeit des Sortierens. Mit
GPUSort soll sich allerdings eine erhebliche Geschwindigkeitssteigerung im Vergleich zur
CPU einstellen. [12]
5.6 BrookGPU
Brook GPU ist ein Projekt, welches einen Compiler und eine Laufzeitumgebung bereitstellt, um moderne Grakkarten zu programmieren, ähnlich wie Cg von NVIDIA. Es wird
allerdings nur der Quellcode bereitgestellt, fertig kompilierte Programme gibt es nicht.
Um Brook kompilieren zu können, muss man vorher die Cygwin Umgebung installiert
haben, welche Linux Anwendungen auf Windows portiert hat. Auÿerdem braucht man
einen C Compiler von Microsoft. Leider lieÿ sich das Brook-Projekt nicht kompilieren,
da erforderliche Programme und Libraries nicht gefunden wurden, es konnte nach langer
Suche keine andere Lösung gefunden werden. [14]
Marc Blunck
47
RST-Labor WS06/07
GPGPU
6 Fazit
Ein Grund Grakkarten für die Ausführung von general
purpose Programmen einzu-
setzen ist die deutlich höhere Rechenleistung und Speicherbrandbreite gegenüber aktuellen CPUs. Um diese Leistungssteigerung zu erreichen wurde die GPU als sogenannter
Stream-Prozessor
konzipiert. Stream-Prozessoren arbeiten, im Gegensatz zu seriellen
Prozessoren, immer auf kompletten Datenströmen. Dieses Stream-Modell selber wirkt
sich aber erheblich auf die Programmierung von Grakkarten aus und hat zur Folge,
dass die Implementierung von Datenstrukturen und Algorithmen in der Regel nicht so
einfach erfolgen kann wie man es von der CPU her gewöhnt ist. So weisen viele der auf
der CPU verwendeten Algorithmen typischerweise nicht den nötigen Parallelitätsgrad
auf um ezient auf der GPU ausgeführt werden zu können und aufgrund des StreamModells müssen alle Datenstrukturen in Streams (typischerweise Textur-Streams) abgebildet werden, was gerade für dünn-besetzte Datenstrukturen die möglicherweise auch
dynamisch wachsen und schrumpfen sollen recht kompliziert ist. Die tatsächliche Implementierung solcher dünnbesetzen Datenstrukturen ist dann in der Regel erheblich von
den wirklich vorhandenen Daten abhängig. Zum praktischen Teil bleibt zu sagen, dass
die Shader Model 3.0 Karten für den Versuch nicht vorhanden waren, was die praktische
Arbeit deutlich erschwerte. Auÿerdem waren die meisten Programme für Grakkarten
des Herstellers NVIDIA optimiert, darum konnten diese nicht mit den vorhandenen ATI
Karten getestet werden. Zusätzlich zu den Hardwareproblemen kamen auch einige Softwareprobleme, so lieÿ sich z.B: Brook nicht kompilieren und somit auch nicht testen,
einige Programme, wie z.B. FFFF oder GPULab stürzten auf manchen Rechnern schon
beim Start ab. Es bleibt zu sagen, dass die Nutzung der Rechenleistung von GPUs ein
sehr interessantes Gebiet ist, allerdings braucht man für eine Vielzahl an Programmen
aktuelle Hardware.
[15]
Marc Blunck
48
RST-Labor WS06/07
GPGPU
Literaturverzeichnis
[1]
Proseminar: Multimedia-Hardwareerweiterungen - GPGPU
http://marcdawir.ma.funpic.de/wiki/index.php?title=
Proseminar:_Multimedia-Hardwareerweiterungen_-_GPGPU
[2]
GPGPU
http://de.wikipedia.org/wiki/GPGPU
[3]
GPGPU Programming
http://www.gpgpu.org/vis2005/
[4]
C for graphics
http://en.wikipedia.org/wiki/Cg_programming_language
http://de.wikipedia.org/wiki/C_for_graphics
[5]
The GeForce 6 Series GPU Architecture
http://download.nvidia.com/developer/GPU_Gems_2/GPU_Gems2_
ch30.pdf
[6]
Streaming Architectures and Technology Trends
http://download.nvidia.com/developer/GPU_Gems_2/GPU_Gems2_
ch29.pdf
[7]
Matt Pharr, Randima Fernando: GPU Gems 2,
A Primer. Addison Wesley Publishing Company, 2005
http://developer.nvidia.com/object/gpu_gems_2_home.html
[8]
Shader Seminar
http://wwwisg.cs.uni-magdeburg.de/~spindler/wiki/
ShaderSeminar2005/index.php?n=Main.HomePage
[9]
ATI AVIVO Xcode
http://archiv.chip.de/news/c1_archiv_news_18001300.html
http://www.chip.de/downloads/c1_downloads_18000750.html
http://www.pspfreak.de/2006/11/02/
avivo-xcode-filme-deutlich-schneller-codieren/
[10]
FFFF - Fast Floating Fractal Fun
http://sourceforge.net/projects/ffff
[11]
GPUlab
http://gpulab.sourceforge.net/
Marc Blunck
49
RST-Labor WS06/07
[12]
GPGPU
GPUSort
http://defectivecompass.wordpress.com/2006/06/25/
learning-from-gpusort/
http://gamma.cs.unc.edu/GPUSORT/
[13]
Cg Toolkit 1.5
http://developer.nvidia.com/object/cg_toolkit.html
[14]
BrookGPU
http://graphics.stanford.edu/projects/brookgpu
[15]
Dominik Göddeke, GPGPU Basic Math Tutorial
http://www.mathematik.uni-dortmund.de/~goeddeke/gpgpu/
tutorial.html
[16]
Glow
http://glow.sourceforge.net/download.html
[17]
The OpenGL Extension Wrangler Library
http://glew.sourceforge.net/
[18]
OpenGL
http://opengl.org
[19]
NVIDIA SDK
http://developer.nvidia.com/object/sdk_home.html
[20]
ATI SDK
http://ati.amd.com/developer/radeonSDK.html
[21]
Programmieren mit OpenGL und GLUT, Manfred Brill
http://www.vislab.de/cgbuch/intros/glut.pdf
[22]
OpenGL Shading Language, Second Edition
Addison Wesley Professional, January 25, 2006, ISBN 0-321-33489-2
[23]
Die Mandelbrotmenge
http://de.wikipedia.org/wiki/Mandelbrot-Menge
[24]
Die Juliamenge
http://de.wikipedia.org/wiki/Julia-Menge
Marc Blunck
50
RST-Labor WS06/07
GPGPU
Abbildungsverzeichnis
1.1
Vergleich GFlops [2]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
2.1
Architektur eines PC, [5] . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.2
Aufbau GPU [5]
8
2.3
Vertex-Prozessor einer GeForce 6 Serie [5]
2.4
Rasterisierung und früher Z-Test
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
9
. . . . . . . . . . . . . . . . . . . . . . .
10
2.5
Fragment-Prozessor und Texel-Pipeline [5] . . . . . . . . . . . . . . . . . .
11
2.6
Eine Textur- und Fragmenteinheit hat 4 Pixel-Shader [5] . . . . . . . . . .
12
2.7
Speicheraufteilung bei der GPU der GeForce 6 Serie [5]
. . . . . . . . . .
12
2.8
GPU Streams [7]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
3.1
Stream-Modell [6] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
4.1
Komplexe Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
22
4.2
Mandelbrotmenge [23]
22
4.3
OpenGL Datentypen und Suxes [21]
. . . . . . . . . . . . . . . . . . . .
23
4.4
Visual Studio Projekteigenschaften . . . . . . . . . . . . . . . . . . . . . .
27
4.5
Vektoren für Lichtberechnung [22] . . . . . . . . . . . . . . . . . . . . . . .
32
4.6
Mandelbrotmenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
36
4.7
Mandelbrotmenge mit verändertem Zoomfaktor . . . . . . . . . . . . . . .
36
4.8
Juliamenge
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
4.9
Juliamenge
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
39
4.10 Juliamenge mit verändertem Zoomfaktor . . . . . . . . . . . . . . . . . . .
40
5.1
ATI SDK Mandelbrotmenge . . . . . . . . . . . . . . . . . . . . . . . . . .
41
5.2
ATI SDK Mandelbrotmenge . . . . . . . . . . . . . . . . . . . . . . . . . .
42
5.3
FFFF Hauptbildschirm . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
43
5.4
FFFF Fragment Shader Berechnung
. . . . . . . . . . . . . . . . . . . . .
44
5.5
FFFF Vertex Shader Berechnung . . . . . . . . . . . . . . . . . . . . . . .
44
5.6
GPULab [11]
45
5.7
ATI Avivo Xcode [9]
Marc Blunck
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
51
RST-Labor WS06/07
GPGPU
Tabellenverzeichnis
2.1
Übersicht Bandbreite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1
Cg Datentypen
Marc Blunck
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
20
52
RST-Labor WS06/07
GPGPU
A Quellcode
A.1 main.cpp
#include
#include
#include
#include
#include
<stdio.h>
<stdlib.h>
<GL/glew.h>
<GL/glut.h>
"textfile.h" // zum Lesen u. Schreiben von Textdateien.
GLint loc1,loc2,loc3,loc4;
GLuint v,f,p;
float Zoom = 1.0;
float Xcenter = 0.0;
float Ycenter = 0.0;
float MaxI = 10;
void changeSize(int w, int h) {
//
Division durch Null verhindern, wenn Fenster zu klein ist.
if(h == 0)
h = 1;
float ratio = 1.0* w / h;
// Koordinaten vorm Modifizieren zurücksetzten.
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glViewport(0, 0, w, h);
// Die korrekte Perspektive einstellen.
gluPerspective(45,ratio,1,100);
glMatrixMode(GL_MODELVIEW);
}
float a = 0;
Marc Blunck
53
RST-Labor WS06/07
GPGPU
void renderScene(void) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt(0.0,0.0,3.0,
0.0,0.0,0.0,
0.0f,1.0f,0.0f);
glUniform1f(loc1, Zoom);
glUniform1f(loc2, Xcenter);
glUniform1f(loc3, Ycenter);
glUniform1f(loc4, MaxI);
glPushMatrix();
glBegin(GL_QUADS);
glTexCoord2f(0.0f,
glTexCoord2f(1.0f,
glTexCoord2f(1.0f,
glTexCoord2f(0.0f,
glEnd();
glPopMatrix();
a+=0.05;
glutSwapBuffers();
}
0.0f);
0.0f);
1.0f);
1.0f);
glVertex3f(-1.0f, -1.0f, 1.0f); //
glVertex3f( 1.0f, -1.0f, 1.0f); //
glVertex3f( 1.0f, 1.0f, 1.0f); //
glVertex3f(-1.0f, 1.0f, 1.0f); //
Unten Links
Unten Rechts
Oben Rechts
Oben Links
void processNormalKeys(unsigned char key, int x, int y) {
if (key == 27)
exit(0);
}
#define printOpenGLError() printOglError(__FILE__, __LINE__)
int printOglError(char *file, int line)
{
//
// Gibt 1 bei OpenGL-Fehler zurück, sonst 0.
//
GLenum glErr;
int
retCode = 0;
glErr = glGetError();
while (glErr != GL_NO_ERROR)
{
printf("glError in file %s @ line %d: %s\n", file, line, gluErrorString(glErr));
retCode = 1;
glErr = glGetError();
}
Marc Blunck
54
RST-Labor WS06/07
}
GPGPU
return retCode;
void printShaderInfoLog(GLuint obj)
{
int infologLength = 0;
int charsWritten = 0;
char *infoLog;
glGetShaderiv(obj, GL_INFO_LOG_LENGTH,&infologLength);
if (infologLength > 0)
{
infoLog = (char *)malloc(infologLength);
glGetShaderInfoLog(obj, infologLength, &charsWritten, infoLog);
printf("%s\n",infoLog);
free(infoLog);
}
}
void printProgramInfoLog(GLuint obj)
{
int infologLength = 0;
int charsWritten = 0;
char *infoLog;
glGetProgramiv(obj, GL_INFO_LOG_LENGTH,&infologLength);
if (infologLength > 0)
{
infoLog = (char *)malloc(infologLength);
glGetProgramInfoLog(obj, infologLength, &charsWritten, infoLog);
printf("%s\n",infoLog);
free(infoLog);
}
}
void setShaders() {
char *vs = NULL,*fs = NULL;
v = glCreateShader(GL_VERTEX_SHADER);
f = glCreateShader(GL_FRAGMENT_SHADER);
vs = textFileRead("mandel.vert");
fs = textFileRead("mandel.frag");
const char * vv = vs;
const char * ff = fs;
Marc Blunck
55
RST-Labor WS06/07
GPGPU
glShaderSource(v, 1, &vv,NULL);
glShaderSource(f, 1, &ff,NULL);
free(vs);free(fs);
glCompileShader(v);
glCompileShader(f);
printShaderInfoLog(v);
printShaderInfoLog(f);
p = glCreateProgram();
glAttachShader(p,v);
glAttachShader(p,f);
glLinkProgram(p);
printProgramInfoLog(p);
glUseProgram(p);
loc1
loc2
loc3
loc4
}
=
=
=
=
glGetUniformLocation(p,
glGetUniformLocation(p,
glGetUniformLocation(p,
glGetUniformLocation(p,
"Zoom");
"Xcenter");
"Ycenter");
"MaxIterations");
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100,100);
glutInitWindowSize(800,600);
glutCreateWindow("RST-L");
glutDisplayFunc(renderScene);
glutIdleFunc(renderScene);
glutReshapeFunc(changeSize);
glutKeyboardFunc(processNormalKeys);
glClearColor(0.0,0.0,0.0,1.0);
glewInit();
if (glewIsSupported("GL_VERSION_2_0"))
printf("Ready for OpenGL 2.0\n");
else {
printf("OpenGL 2.0 not supported\n");
exit(1);
}
Marc Blunck
56
RST-Labor WS06/07
GPGPU
setShaders();
glutMainLoop();
return 0;
}
Marc Blunck
57