Nutzen Sie das Multithreading zwischen Game Loop und OpenGL

10

Sprechen im Kontext eines Spiels, das auf openGL-Renderer basiert:

Nehmen wir an, es gibt zwei Themen:

  1. Aktualisiert die Spiellogik und -physik usw. für die Objekte im Spiel

  2. Lässt openGL für jedes Spielobjekt Aufrufe basierend auf Daten in den Spielobjekten zeichnen (dieser Thread 1 wird ständig aktualisiert).

Sofern Sie nicht zwei Kopien jedes Spielobjekts im aktuellen Status des Spiels haben, müssen Sie Thread 1 anhalten, während Thread 2 die Draw-Aufrufe ausführt. Andernfalls werden die Spielobjekte während eines Draw-Aufrufs für dieses Objekt aktualisiert ist unerwünscht!

Das Stoppen von Thread 1, um sicher Draw-Aufrufe von Thread 2 zu tätigen, macht jedoch den gesamten Zweck von Multithreading / Parallelität zunichte

Gibt es dafür einen besseren Ansatz als die Verwendung von Hunderten oder Tausenden oder die Synchronisierung von Objekten / Zäunen, damit die Multicore-Architektur für die Leistung genutzt werden kann?

Ich weiß, dass ich weiterhin Multithreading zum Laden von Texturen und zum Kompilieren von Shadern für Objekte verwenden kann, die noch nicht Teil des aktuellen Spielstatus sind. Wie kann ich dies jedoch für die aktiven / sichtbaren Objekte tun, ohne Konflikte beim Zeichnen und Aktualisieren zu verursachen?

Was ist, wenn ich in jedem Spielobjekt eine separate Synchronisierungssperre verwende? Auf diese Weise blockiert jeder Thread nur ein Objekt (Idealfall) und nicht den gesamten Aktualisierungs- / Zeichnungszyklus! Aber wie teuer ist es, jedes Objekt zu sperren (das Spiel kann tausend Objekte haben)?

Allahjane
quelle
1. Angenommen, es gibt nur zwei Threads, die nicht gut skaliert werden. 4 Kerne sind sehr häufig und nehmen nur zu. 2. Antwort auf die Best-Practice-Strategie: Wenden Sie die Segregation der Verantwortlichkeit für Befehlsabfragen und das Akteurmodell / Komponentenentitätssystem an. 3. Realistische Antwort: Hacken Sie es einfach auf die traditionelle C ++ - Art mit Sperren und so.
Den
Ein gemeinsamer Datenspeicher, eine Sperre.
Justin
@justin genau wie gesagt mit einer Synchronisierungssperre Der einzige Vorteil des Multithreading wird bei Tageslicht brutal ermordet. Der Update-Thread müsste warten, bis der Draw-Thread alle Objekte aufruft. Dann wartet der Draw-Thread, bis die Update-Schleife die Aktualisierung abgeschlossen hat ! Das ist schlimmer als Single-Threaded-Ansatz
Allahjane
1
Es scheint, als ob Multithread-Ansätze in Spielen mit asynchroner Grafik-API (z. B. openGL) immer noch ein aktives Thema sind und es keine Standard- oder nahezu perfekte Lösung dafür gibt
Allahjane,
1
Der Datenspeicher, der Ihrer Engine und Ihrem Renderer gemeinsam ist, sollte nicht Ihr Spielobjekt sein. Es sollte eine Darstellung sein, die der Renderer so schnell wie möglich verarbeiten kann.
Justin

Antworten:

13

Der von Ihnen beschriebene Ansatz mit Sperren wäre sehr ineffizient und höchstwahrscheinlich langsamer als die Verwendung eines einzelnen Threads. Der andere Ansatz, Kopien von Daten in jedem Thread zu behalten, würde wahrscheinlich "geschwindigkeitsmäßig" gut funktionieren, jedoch mit unerschwinglichen Speicherkosten und Codekomplexität, um die Kopien synchron zu halten.

Hierfür gibt es mehrere alternative Ansätze. Eine beliebte Lösung für das Rendern mit mehreren Threads ist die Verwendung eines doppelten Befehlspuffers. Dies besteht darin, das Renderer-Backend in einem separaten Thread auszuführen, in dem alle Zeichenaufrufe und die Kommunikation mit der Rendering-API ausgeführt werden. Der Front-End-Thread, der die Spielelogik ausführt, kommuniziert mit dem Back-End-Renderer über einen Befehlspuffer (doppelt gepuffert).. Mit diesem Setup haben Sie nach Abschluss eines Frames nur einen Synchronisationspunkt. Während das Front-End einen Puffer mit Renderbefehlen füllt, verbraucht das Back-End den anderen. Wenn beide Fäden gut ausbalanciert sind, sollte keiner verhungern. Dieser Ansatz ist jedoch nicht optimal, da er zu einer Latenz in den gerenderten Frames führt. Außerdem macht der OpenGL-Treiber dies wahrscheinlich bereits in seinem eigenen Prozess, sodass Leistungssteigerungen sorgfältig gemessen werden müssten. Es werden auch bestenfalls nur zwei Kerne verwendet. Dieser Ansatz wurde jedoch in mehreren erfolgreichen Spielen wie Doom 3 und Quake 3 verwendet

Skalierbarere Ansätze, die Multi-Core-CPUs besser nutzen, basieren auf unabhängigen Aufgaben , bei denen Sie eine asynchrone Anforderung auslösen, die in einem sekundären Thread bearbeitet wird, während der Thread, der die Anforderung ausgelöst hat, mit einer anderen Arbeit fortfährt. Die Aufgabe sollte idealerweise keine Abhängigkeiten zu den anderen Threads aufweisen, um Sperren zu vermeiden (vermeiden Sie auch gemeinsame / globale Daten wie die Pest!). Eine aufgabenbasierte Architektur kann in lokalisierten Teilen eines Spiels besser verwendet werden, z. B. beim Berechnen von Animationen, beim Finden von KI-Pfaden, beim Generieren von Prozeduren, beim dynamischen Laden von Szenenrequisiten usw. Spiele sind natürlich ereignisreich, die meisten Arten von Ereignissen sind asynchron Es ist einfach, sie in separaten Threads auszuführen.

Zum Schluss empfehle ich zu lesen:

glampert
quelle
nur neugierig! Woher weißt du, dass Doom 3 diesen Ansatz verwendet hat? Ich dachte, Entwickler lassen dort nie Techniken raus!
Allahjane
4
@Allahjane, Doom 3 ist Open Source . In dem von mir bereitgestellten Link finden Sie einen Überblick über die Gesamtarchitektur des Spiels. Und Sie irren sich, ja, es ist selten, vollständig Open Source-Spiele zu finden, aber Entwickler stellen ihre Techniken und Tricks normalerweise in Blogs, Zeitungen und Veranstaltungen wie GDC vor .
Glampert
1
Hmm. Dies war eine großartige Lektüre! Danke, dass du mich darauf aufmerksam gemacht hast! Sie bekommen die Zecke :)
Allahjane