Render Thread
Ein Render Thread seperiert die Programm Logik von der Darstellungs Logik und erlaubt so eine Lockere Bindung, wiederverwendung bzw. Änderung von der jeweiligen Logik und unterschiedliche Frequenzen bei der Ausführung.
Ein Render Thread bringt ein Problem mit sich, man muss ein Thread erzeugen und eine Kommunikation mit den zu rendernen Daten herstellen. Hier gibt es 2 Prinzipien, 1. der Thread greift direkt auf die Logikdaten zu und interpretiert die Renderbefehle oder 2. die Logik gibt dem Thread die Renderbefehle.
Starke Bindung
Die 1. Möglichkeit macht den Render Thread sehr mächtig und unflexibel, weil er stark an die Programmlogik gekoppelt wird und damit nicht mehr in anderen Programmen, mit anderer Logik, funktionieren kann aber dafür ist es einfach schnelle Ergebnisse zu erzielen und einfacher zu optimieren. Der Render Thread greift auf Thread Safe Daten zu und kann anhand dieser die OpenGL Befehle erzeugen.
Schwache Bindung
Die 2. Möglichkeit macht den Render Thread wiederverwendbar aber benötigt mehr Zeit für das Design, Entwickeln und Optimieren. Der Render Thread nimmt ein Intermediate Befehlsliste entgegen und kann diese dann in OpenGL Befehle umsetzen. Dabei kann man verschiedene Implementierungen machen, wie z.B. 2 Thread safe Queues für Front- und Back-Buffer, die geswapped werden oder Tripple Buffer. Bei Tripple Buffer werden 2 Back-Buffer(BB1,BB2) und ein Front-Buffer(FB1) erzeugt, der Front-Buffer wird von der Programmlogik befüllt und der Render Thread kann ein Back-Buffer verarbeiten.
Die Programm Logik kann nie auf die Back-Buffer zugreifen aber indirekt mit dem Front-Buffer Swap Befehl den Front-Buffer und den gerade nicht verwendetend Back-Buffer austauschen. Der Render Thread verwendet immer nur ein Back-Buffer und wenn er fertig ist, macht er ein Swap auf den Back-Buffern und bekommt so eine neue Queue aus den 2. Back-Buffer. In BB1 Slot wird also immer die Queue verarbeitet, in BB2 liegt immer ein vollständige Befehlsliste, für die Verarbeitung, bereit und im Front-Buffer wird eine neue Befehlsliste erstellt.
Wenn die Program Logik mit 2000 Frames arbeitet, dann füllt sie 2000 mal den Front-Buffer und führt den Swap Befehle aus. Sollte der Render Thread mit einer niederen Frequenz laufen, dann wird die Programm Logik nichts anderes machen als den noch nicht gerenderten Stand auf ein aktuelleren Stand zu bringen, damit der Render Thread den letzten vollständigen Stand wieder spiegelt. Wenn die Game Logik mit einer niederen Frequenz läuft, dann kann man 2 Möglichkeiten implementieren. Entweder wechselt man einfach die beiden Back-Buffer und rendert mal den letzen und mal den Vorletzen oder der Render Thread räumt die Queue auf, bevor man den Swap aufruft und wenn man eine leere Queue hat wird garnicht SwapBuffer von der Render API aufgerufen und somit bleibt der letzte gerenderte Frame im Grafikkarten Speicher erhalten.
Der Render Thread sollte mit einer anderen Frequenz laufen als die Programm Logik, z.B. der aktuellen Monitor Frequenz(VSync) oder niederer. Dies ermöglicht es z.B. präzise Physik Simulationen mit hunderten bis tausenden durchläufen pro Sekunde.
Die Kunst bei diesem Prinzip ist, dass man ein möglichst kleines Kommunikations Protokoll entwickelt. Je kleiner das Protokoll ist, des so mehr Code wird wieder verwendet, weniger Fehler können gemacht werden und so schneller ist die Kommunikation.
Hier ein paar Beispiele um die Unterschiede besser wieder zu geben. Zu spezielle Befehle erkennt man in der Regel, dass diese keine Parameter haben und sehr selten verwendet werden.
RenderThread::Enqueue(RedTintedCatMessage());
Zu Atomare Befehle erkennt man in der Regel, dass man sehr große Codeblöcke auf der Logikseite schreiben muss, um einzelne Daten zu visualisieren.
RenderThread::Enqueue(BeginMessage(FaceType::Triangle));
RenderThread::Enqueue(ColorMessage(FaceColor::Red));
RenderThread::Enqueue(VertexMessage(-1,-1,0);
RenderThread::Enqueue(VertexMessage(1,-1,0);
RenderThread::Enqueue(VertexMessage(0,1,0);
RenderThread::Enqueue(EndMessage());
Eine einfache Möglichkeit ist Renderdaten als Objekte zu zerlegen und auf diesen auf zu bauen.
Mesh catMesh("cat.mesh");
Shader redTinting("tinting.shader");
RenderThread::Enqueue(BindMeshMessage(catMesh));
RenderThread::Enqueue(BindShaderMessage(redTinting));
RenderThread::Enqueue(SetShaderVariable("tintingColor", FaceColor::Red));
RenderThread::Enqueue(Draw(catMesh.StartIndex(), catMesh.LastIndex()));