Render Thread: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Schwache Bindung)
 
(8 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
=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 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 weiterer Vorteil tritt auf, wenn man eine Anwendung mit mehreren Fenstern entwickelt, da man einfach für jedes Fenster eine weitere Instanz erzeugen kann und die Logik für was auf welchen Fenster gezeichnet werden soll in der Programm Logik und nicht Render Logik liegt.
  
 
Ein Render Thread bringt ein Problem mit sich, man muss ein Thread erzeugen und eine Kommunikation mit den zu rendernen Daten herstellen.
 
Ein Render Thread bringt ein Problem mit sich, man muss ein Thread erzeugen und eine Kommunikation mit den zu rendernen Daten herstellen.
Zeile 6: Zeile 6:
  
 
==Starke Bindung==
 
==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.
+
Die 1. Möglichkeit ist, dass der Render Thread direkt auf die auf Thread Safe Daten zu greift und anhand dieser die OpenGL Befehle erzeugt.
Der Render Thread greift auf Thread Safe Daten zu und kann anhand dieser die OpenGL Befehle erzeugen.
+
Dies macht den Render Thread sehr mächtig jedoch auch unflexibel, weil er stark an die Programmlogik gekoppelt wird. Damit wird er nicht mehr in anderen Programmen, d.h. mit anderer Logik, funktionieren. Jedoch ist es einfach, schnelle Ergebnisse zu erzielen und einfacher zu optimieren.
  
 
==Schwache Bindung==
 
==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.
Der Render Thread nimmt ein Intermediate Befehlsliste entgegen und kann diese dann in OpenGL Befehle umsetzen.
+
Diese 2. Möglichkeit macht den Render Thread wiederverwendbar aber benötigt mehr Zeit für das Design, Entwickeln und Optimieren.
 
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.
 
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(B1,B2) und ein Front-Buffer(FB) erzeugt, der Front-Buffer wird von der Programmlogik befüllt und am Ende wird ein Swap angefordert.
+
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.
Nun wird der Front-Buffer mit dem 1. Back-Buffer(B1) geswapped, sollte nun der Renderer durch sein, wird er B1 mit B2 austauschen und beginnt mit der verarbeitung von B1, während die Programmlogik FB und B2 swappen kann.
+
 
Der Sinn ist, dass nun Render Thread und Logik mit unterschiedlichen Frequenzen arbeiten können und immer ein aktueller vollständiger Stand verfügbar ist.
+
[[Datei:renderthread_communication.png]]
Mit dieser Variante muss man sich nur um Multithreading sichere Programmierung bei den 3 Queues kümmern.
+
 
Dies kann man auch Lösen, in dem man Lock-Free Queues benutzt.
+
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.
 
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 schneller ist die Kommunikation.
+
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.
 
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.
 
Zu spezielle Befehle erkennt man in der Regel, dass diese keine Parameter haben und sehr selten verwendet werden.
<code=cpp>RenderThread::Enqueue(RedTintedCatMessage());</code>
+
<source lang="cpp">Window1.Context.Enqueue(RedTintedCatMessage());</source>
 
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.
 
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.
<code=cpp>RenderThread::Enqueue(BeginMessage(FaceType::Triangle));
+
<source lang="cpp">Window1.Context.Enqueue(BeginMessage(FaceType::Triangle));
RenderThread::Enqueue(ColorMessage(FaceColor::Red));
+
Window1.Context.Enqueue(ColorMessage(FaceColor::Red));
RenderThread::Enqueue(VertexMessage(-1,-1,0);
+
Window1.Context.Enqueue(VertexMessage(-1,-1,0);
RenderThread::Enqueue(VertexMessage(1,-1,0);
+
Window1.Context.Enqueue(VertexMessage(1,-1,0);
RenderThread::Enqueue(VertexMessage(0,1,0);
+
Window1.Context.Enqueue(VertexMessage(0,1,0);
RenderThread::Enqueue(EndMessage());</code>
+
Window1.Context.Enqueue(EndMessage());</source>
 
Eine einfache Möglichkeit ist Renderdaten als Objekte zu zerlegen und auf diesen auf zu bauen.
 
Eine einfache Möglichkeit ist Renderdaten als Objekte zu zerlegen und auf diesen auf zu bauen.
<code=cpp>Mesh catMesh("cat.mesh");
+
<source lang="cpp">Mesh catMesh("cat.mesh");
 
Shader redTinting("tinting.shader");
 
Shader redTinting("tinting.shader");
RenderThread::Enqueue(BindMeshMessage(catMesh));
+
Window1.Context.Enqueue(BindMeshMessage(catMesh));
RenderThread::Enqueue(BindShaderMessage(redTinting));
+
Window1.Context.Enqueue(BindShaderMessage(redTinting));
RenderThread::Enqueue(SetShaderVariable("tintingColor", FaceColor::Red));
+
Window1.Context.Enqueue(SetShaderVariable("tintingColor", FaceColor::Red));
RenderThread::Enqueue(Draw(catMesh.StartIndex(), catMesh.LastIndex()));</code>
+
Window1.Context.Enqueue(Draw(catMesh.StartIndex(), catMesh.LastIndex()));</source>

Aktuelle Version vom 11. Juni 2014, 21:04 Uhr

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 weiterer Vorteil tritt auf, wenn man eine Anwendung mit mehreren Fenstern entwickelt, da man einfach für jedes Fenster eine weitere Instanz erzeugen kann und die Logik für was auf welchen Fenster gezeichnet werden soll in der Programm Logik und nicht Render Logik liegt.

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 ist, dass der Render Thread direkt auf die auf Thread Safe Daten zu greift und anhand dieser die OpenGL Befehle erzeugt. Dies macht den Render Thread sehr mächtig jedoch auch unflexibel, weil er stark an die Programmlogik gekoppelt wird. Damit wird er nicht mehr in anderen Programmen, d.h. mit anderer Logik, funktionieren. Jedoch ist es einfach, schnelle Ergebnisse zu erzielen und einfacher zu optimieren.

Schwache Bindung

Der Render Thread nimmt ein Intermediate Befehlsliste entgegen und kann diese dann in OpenGL Befehle umsetzen. Diese 2. Möglichkeit macht den Render Thread wiederverwendbar aber benötigt mehr Zeit für das Design, Entwickeln und Optimieren. 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.

renderthread communication.png

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.

Window1.Context.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.

Window1.Context.Enqueue(BeginMessage(FaceType::Triangle));
Window1.Context.Enqueue(ColorMessage(FaceColor::Red));
Window1.Context.Enqueue(VertexMessage(-1,-1,0);
Window1.Context.Enqueue(VertexMessage(1,-1,0);
Window1.Context.Enqueue(VertexMessage(0,1,0);
Window1.Context.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");
Window1.Context.Enqueue(BindMeshMessage(catMesh));
Window1.Context.Enqueue(BindShaderMessage(redTinting));
Window1.Context.Enqueue(SetShaderVariable("tintingColor", FaceColor::Red));
Window1.Context.Enqueue(Draw(catMesh.StartIndex(), catMesh.LastIndex()));