Statusmaschine OpenGL

Die GPU1) aus Sicht des Entwicklers entspricht der Sicht auf die CPU2). Die meisten Entwickler haben heutzutage allerdings eine CPU noch nicht aus der Nähe gesehen - und das gilt leider auch für Informatik-Studiengänge.

Ob C eine Hochsprache ist, darüber streitet man heute gerne. C ist sehr maschinennah, aber der Streit liegt vermutlich daran, dass viele nicht mehr wissen, wieviel Platz zwischen 'maschinennah' und 'direkt an der Maschine' ist. Da liegen noch einige Stockwerke mehr zwischen, als zwischen z.B. C und C++.

Was ist eine Statusmaschine?

In meiner Kindheit gab es Rubiks Würfel: ein Würfel, der je Seite 9 Farbflächen hatte, die man durchmengen konnte und anschließend vor der Aufgabe stand, das Chaos wieder in Ordnung zu bringen, so dass auf jeder Fläche des Würfels alle 9 Farbflächen die gleiche Farbe aufwiesen:

Diese Vermischung der Farbflächen ist ein Zustand. Genau diese Vermischung. Jede andere Durchmischung der Farben ist ein entsprechend anderer Zustand. Das Ziel des Spiels ist es, den Zustand zu erreichen, wo alle Farbflächen gleicher Farbe der gleichen Seite des Würfels liegen.

Wenn wir nun den Würfel in irgendeiner Form verdrehen, erhalten wir einen neuen Zustand.

Was hat das ganze nun mit Programmieren zu tun?
Nun, ein Prozessor besitzt einige wenige Register, die sind vergleichbar mit globalen Variablen in einer höheren Programmiersprache. Unsere Würfel-Statusmaschine besitzt beispielsweise 6 Register (vorne, hinten, links, rechts, oben und unten) mit je neun Farbinformationen. Üben wir nun einen Befehl aus, wie zum Beispiel „Rotiere linkes Element an der X-Achse“, so werden in den Registern vorn, hinten, oben und unten Farben ausgetauscht, im Register Links werden die Farben gedreht und das Register rechts bleibt unverändert. Wir haben einen neuen Status erreicht.

Wichtig zu verstehen ist nun, dass hier keine Zuweisung stattgefunden hat, sondern es wurde lediglich der Status des Würfels verändert. Man kann den Würfel nun fragen, welche Farbflächen auf der Vorderseite zu sehen sind, aber eine Zuweisung wie

Register variable = Prozessor.rotiereLinks( x ).vorne;

lässt sich nicht formulieren. Das ist ein Verknüpfen von Anweisungen (rotiereLinks, Zugriff auf Register Vorne und Zuweisung auf die Variable variable.) Das kann nur eine Hochsprache.

Man muss diese Dinge Schritt für Schritt durchführen. Zum einen gibt es kein Prozessor-Objekt, es gibt nur einen globalen Prozessor. Zum anderen lassen sich Anweisungen nicht miteinander verbinden. Immer eins nach dem Anderen.

rotiereLinks(x);
kopiere vorne, variable;

So verändert man den Würfel erst und wenn der Schritt abgeschlossen ist, dann schaut man, wie die Vorderseite aussieht. Man könnte nun gucken, ob der Status vorne gleichfarbig ist. Der Vorteil bei einer CPU ist, dass man einen Status direkt in den Prozessor laden kann - statt zu knobeln, kann man auch einfach den Status wie man ihn braucht in die Statusmaschine laden. Das entspricht etwa dem Auseinanderbauen und wieder zusammenstecken des Würfels.

So funktionieren Prozessoren - unabhängig davon, ob es ein Hauptprozessor (CPU) oder ein Grafikprozessor (GPU) ist. Der Zustand des Prozessors ändert sich nur dann, wenn man explizit den Befehl dafür gibt. Es gibt keine temporären Zwischenergebnisse, die man mit Operatoren verknüpfen könnte.

Nehmen wir die Rechung wert = 1*2+3*4:

lade 1 in Register
multipliziere Register mit 2  - der Status ändert sich, in Register steht jetzt 2
addiere Register mit 3        - der Status ändert sich, in Register steht jetzt 5
multipliziere Register mit 4  - der Status ändert sich, in Regsiter steht jetzt 20.
kopiere Register nach Variable wert

Wir haben kein Zwischenergebnis und die Befehle müssen so angeordnet werden, dass sie die Prioritäten der Rechnung entsprechen. Wir wollten (1*2) + (3*4) rechnen, aber wenn man das so runterbetet, dann entsteht (((1*2)+3)*4). Wenn wir kein weiteres Register haben, dann müssen wir Zwischenergebnisse im Arbeitsspeicher ablegen. In der Regel hat man aber zumindest eine Handvoll Register:

lade 1 in Register1
multipliziere Register1 mit 2  - der Status ändert sich, in Register1 steht jetzt 2
Lade 3 in Register2            - der Status ändert sich, in Register2 steht jetzt 3
multipliziere Register2 mit 4  - der Status ändert sich, in Regsiter2 steht jetzt 12 
addiere auf Register1 den Wert von Register2 - der Status von R1 wechselt auf 14, R2 bleibt 12.
kopiere Register1 nach Variable wert

Nun haben wir in Register1 das Ergebnis der Rechnung sehen.

Die CPU weiß dabei weder, ob sie gerade ein C Programm ausführt oder was genau sie da ausrechnet. Es wird lediglich Befehl für Befehl abgearbeitet und entsprechend der Befehle ändert sich der Zustand des Prozessors. Dass ich ein für mich interessantes Ergebnis erhalte, wenn ich den Zustand von Register1 dann ausgebe oder in eine Variable kopiere, das spielt für den Prozessor keine Rolle.

Die Aufgabe einer Hochsprache ist es, komplizierte Ausdrücke wie 1*2+3*4 für eine Statusmaschine aufzuarbeiten, so dass diese die richtigen Ergebnisse liefert. Was in C also nur eine Zeile ist, kann in Maschinensprache für den Prozessor sehr viele Befehle bedeuten.

Wichtig ist, dass es immer nur einen Prozessor, bzw. einen Rubiks Würfel gibt, den man sich so zurecht legen muss, dass er die gewünschten Ergebnisse liefert. Wir laden hier eine Zahl in ein Register, mit der wir die Rechnung beginnen wollen. Möchten wir den Rubiks Würfel in einen bestimmten Zustand „drehen“, so ist es von Vorteil, wenn man einen bekannten Zustand lädt, z.B. alle Flächen links rot, oben weiß, unten schwarz, rechts blau, vorne grün und hinten gelb darstellt. Dreht man nun die linken Seite nach oben um die X Achse, so sind links alle Flächen weiterhin rot, rechts weiterhin alle blau, aber oben sind nur noch die 6 rechten Flächen weiß, während die 3 linken Flächen grün wurden. Entsprechend verändern sich vorne, hinten und unten.

Wenn wir diesen Zustand wiederholen wollen, können wir erst den Grundzustand laden und wieder die linke Seite an der X Achse nach oben drehen. Genauso rechnet ein Prozessor auch aus, dass 1+2 immer drei ist: erst den bekannten Grundzustand laden - die 1 - und dann diesen Zustand so verändern, dass wir auf das gewünschte Ergebnis kommen - in diesem Fall also 2 addieren.

Diese Arbeitsweise den Zustand des Grafikprozessors in einen gewünschten Zustand zu bringen ist in OpenGL weiterhin „State Of The Art“. Möchte man eine Szene darstellen, löscht man zunächst die vorhandene Szene - man sorgt also für einen bekannten Grundzustand. Anschließend kopiert die gewünschte Farbe in das Farbregister (auch hier wird ein bekannter Grundzustand festgelegt) und zeichnet zum Beispiel ein Dreieck. Solange der Grafikprozessor grün im Farbregister stehen hat, wird er alles grün malen, was er zeichnen soll. Dieser Zustand ändert sich erst, wenn man eine neue Farbe ins Farbregister kopiert. Legt man sich vorher nicht fest, dass man grün zeichnen möchte, dann werden die Dreiecke in der Farbe gezeichnet, die zuletzt ins Farbregister kopiert wurde. Löscht man vor dem Zeichnen die vorhandene Szene übermalt man die vorhandene Szene. Das Ergebnis wäre in beiden Fällen zufällig. Entsprechend heißt die Devise: Erst den Zustand des Grafikprozessors so einstellen, dass das, was man zeichnen möchte auch so aussieht, wie man es sehen möchte.

1)
Graphics Processing Unit
2)
Central Processing Unit