Eine mit Hilfe einer Reihe besonderer Graphik-Routinen beschriebene Szene (incl. Blickpunkt, Lichtern etc.) soll als ein 2-dim. (Pixel-)Bild dargestellt ("gerendert") werden.
pl-rend.c | Hauptprogramm |
stage-g.c | Generierungs-Stufe |
stage-t.c | Transformations-Stufe |
stage-c.c | Clipping-Stufe |
stage-e.c | Edge-Stufe |
stage-p.c | Pixel-Stufe |
graphics.h | Graphik-Bibliothek (Header) |
graphics.c | Graphik-Bibliothek (Implementierung) |
graphic-utils.h | Hilfsfunktionen zur Graphik (Header) |
graphic-utils.c | Hilfsfunktionen zur Graphik (Implementierung) |
boolean.h | Wahrheitswerte |
queue.h | Queue mit Synchronisation (Header) |
queue.c | Queue mit Synchronisation (Implementierung) |
pipeline.h | allgemeine Pipeline (Header) |
pipeline.c | allgemeine Pipeline (Implementierung) |
render-pipe.h | besondere Graphik-Render-Pipeline (Header) |
render-pipe.c | besondere Graphik-Render-Pipeline (Implementierung) |
Graphik-Rendern ist ein Prozeß mit mehreren Stufen:
Fließband-artiger Kontrollfluß:
Die erste Stufe erzeugt Daten und gibt sie weiter, die anderen lesen
"vom Band", arbeiten und geben ggf. wieder Daten zur
Weiterverarbeitung an die folgende Stufe:
Die Objekte auf dem Band ("Token") beinhalten eine Aufgabe und die damit
verbundenen Daten. Beispiele:
Programm nach "Programming with Threads" und Beispielsourcen dazu (s.o.).
Jeder Thread entspricht einer Stufe.
Synchronisation der Threads durch eine Pipeline = Kette von Zwischenspeichern (Queues):
Interface:
queue_t queue_init(int count);
void queue_destroy(queue_t q);
void queue_put(queue_t q, void *ptr);
void *queue_get(queue_t q);
Implementierung:
queue_t enthält:
queue_put:
lock Queue voll -> warte Queue war leer -> wecke einen möglichen Abholer stelle Objekt in die Queue unlock
queue_get:
lock Queue leer -> warte Queue war voll -> wecke einen möglichen Lieferanten hole Objekt aus der Queue unlock
Interface:
pipe_t *pipe_init(int size, int q_size, pipe_stage_t stage[],
data_new_t data_new, data_free_t data_free);
void pipe_destroy(pipe_t *pipe);
void pipe_put(pipe_t *pipe, void *data);
Das Hauptprogramm macht nichts weiter als die Pipeline zu starten und dann (via pipe_destroy) zu warten. Die eigentliche Initiative geht von der 1. Stufe aus.
Implementierung:
Die von pipe_init gestarteten Threads führen alle die zentrale Funktion pipe_dispatch aus:
get my stage if (stage == 0) führe die entsprechende 1. Stufen-Funktion aus (die kümmert sich dann darum, die Pipeline zu füttern) else hole ein neues Token übergib dieses der Stufen-Funktion zur Bearbeitung entscheide anhand des Return-Codes, ob das (evtl. veränderte) Token weitergegeben oder aufgeräumt wird.
Die Pipe wird beendet, indem die 1. Stufe ein shutdown-Token auf die Reise schickt. Dieses wird von allen gelesen und zum Anlaß genommen, die Arbeit einzustellen, nicht ohne die frohe Botschaft seinem Nachfolger zuzurufen.
Die Render-Pipeline ist einfach eine spezialisierte Pipeline mit 5 Stufen (je eine für GEN, TRAN, CLIP, EDGE, PIXEL) und einem besonderem Token-Datentyp nebst zugehörigen Speicher-Management-Funktionen.
Token beinhalten jeweils einen Typ (Aufgabe) und zugehörige Daten. Es gibt folgende Typen:
BEGIN | Initialisierungs-Infos für alle |
END | Aufräumen und Beenden, PIXEL schreibt das Ergebnis ins File |
POLY | Geometriedaten eines Polygons, für alle |
TRANS | Transformationsmatrizen für TRAN |
LIGHT | Lichtquelle für EDGE |
FOV | spezielle Transformation für EDGE |
Die Graphik-Bibliothek enthält neben GiBegin und GiEnd einige
Routinen zur
Beschreibung von Flächen im Raum (Geometrie und Oberfläche) und
Lichtquellen sowie für Transformationen (Verschiebung, Drehung,
Skalierung, Projektion). Sie ist stark an das RenderMan-Interface
angelehnt.
Diese Routinen enthalten auch die pipe_put-Calls, die die Render-Pipe
füttern.
Die GEN-Stufe ist das eigentliche "Anwendungsprogramm", es enthält die Beschreibung der darzustellenden Szene. Dazu baut es aus den Funktionen der Graphik-Bibliothek höhere Funktionen zur Beschreibung von Kugeln, Würfeln, einem Trinkbecher usw. auf und stellt so eine Demo-Szene zusammen:
Die Thread-Struktur bzw. Parallelisierung ist vollständig in den Datenstrukturen Pipeline (Thread-Verwaltung) und Queue (Synchronisation) versteckt. Nur der Programmierer der Graphik-Bibliothek muß überhaupt etwas von der Pipeline wissen, sie ist für ihn ein Ausgabedevice (wie ein Framebuffer oder ein File).
Einen gewissen Einfluß auf die Performance hat die gewählte Queue-Größe: Ist sie zu klein, warten Threads vielleicht unnötig auf Arbeit bzw. auf Abnehmer.
Angesichts der ungleichen und sich dynamisch ändernden Arbeitsbelastung der einzelnen Stufen ist das größte Performance-Problem, ein ordentliches Load-Balancing zu erzielen.
Da normalerweise die PIXEL-Stufe einen Großteil der CPU-Zeit schluckt, sollte man sie selbst auch wieder parallelisieren. Dazu eignen sich "normale" Schleifen-Aufteilungs-Verfahren. Diese Mischung mehrerer Parallelisierungs-Methoden ist typisch für "richtige" Anwendungen