Synchronisation zwischen Game Logic Thread und Rendering Thread

16

Wie trennt man Spielelogik und Rendering? Ich weiß, dass es hier anscheinend bereits Fragen gibt, die genau das fragen, aber die Antworten sind für mich nicht zufriedenstellend.

Soweit ich weiß, besteht der Sinn der Trennung in verschiedene Threads darin, dass die Spielelogik sofort mit dem nächsten Tick beginnen kann, anstatt auf den nächsten vsync zu warten, bei dem das Rendern schließlich vom Swapbuffer-Aufruf zurückkehrt, auf dem es blockiert wurde.

Aber speziell welche Datenstrukturen werden verwendet, um Race-Bedingungen zwischen dem Game-Logic-Thread und dem Rendering-Thread zu verhindern. Vermutlich benötigt der Rendering-Thread Zugriff auf verschiedene Variablen, um herauszufinden, was zu zeichnen ist, aber die Spielelogik könnte dieselben Variablen aktualisieren.

Gibt es eine De-facto-Standardtechnik zur Behandlung dieses Problems? Vielleicht möchten Sie die vom Rendering-Thread benötigten Daten nach jeder Ausführung der Spielelogik kopieren. Was auch immer die Lösung ist, wird der Overhead der Synchronisation oder was auch immer geringer sein, als nur alles mit einem einzigen Thread auszuführen?

user782220
quelle
1
Ich hasse es, nur einen Link als Spam zu versenden
Roy T.
Ein weiterer Link: software.intel.com/en-us/articles/…
Chewy Gumball
1
Diese Links geben das typische Endergebnis wieder, das man gerne hätte, erläutern aber nicht, wie es geht. Würden Sie den gesamten Szenengraphen für jedes Bild oder etwas anderes kopieren? Die Diskussionen sind zu hoch und vage.
User782220
Ich dachte, die Links waren ziemlich eindeutig darüber, wie viel Status jeweils kopiert wird. z.B. (vom 1. Link) "Ein Stapel enthält alle Informationen, die zum Zeichnen eines Rahmens erforderlich sind, enthält jedoch keinen anderen Spielstatus." oder (vom 2. Link) "Daten müssen jedoch noch geteilt werden, aber jetzt, anstatt dass jedes System auf einen gemeinsamen Datenort zugreift, um Positions- oder Orientierungsdaten abzurufen, hat jedes System eine eigene Kopie" (siehe insbesondere 3.2.2 - Status) Manager)
DMGregory
Wer auch immer diesen Intel-Artikel geschrieben hat, scheint nicht zu wissen, dass Threading auf höchster Ebene eine sehr schlechte Idee ist. Niemand macht etwas so Dummes. Plötzlich muss die gesamte Anwendung über spezialisierte Kanäle kommunizieren, und überall gibt es Sperren und / oder große koordinierte staatliche Börsen. Ganz zu schweigen davon, dass nicht gesagt werden kann, wann die gesendeten Daten verarbeitet werden, sodass es äußerst schwierig ist, über die Funktionsweise des Codes nachzudenken. Es ist viel einfacher, die relevanten Szenendaten (unveränderlich als Referenzzähler, veränderlich nach Wert) an einer Stelle zu kopieren und vom Subsystem sortieren zu lassen, wie es will.
snake5

Antworten:

1

Ich habe an der gleichen Sache gearbeitet. Das zusätzliche Problem ist, dass OpenGL (und meines Wissens OpenAL) und eine Reihe anderer Hardwareschnittstellen im Grunde genommen Zustandsmaschinen sind, die nicht gut damit auskommen, von mehreren Threads aufgerufen zu werden. Ich glaube nicht, dass ihr Verhalten überhaupt definiert ist, und für LWJGL (möglicherweise auch JOGL) gibt es oft eine Ausnahme.

Am Ende erstellte ich eine Folge von Threads, die eine bestimmte Schnittstelle implementierten, und lud sie auf den Stapel eines Steuerobjekts. Wenn dieses Objekt ein Signal zum Beenden des Spiels erhält, durchläuft es jeden Thread, ruft eine implementierte ceaseOperations () -Methode auf und wartet, bis sie geschlossen werden, bevor es sich selbst schließt. Universelle Daten, die für das Rendern von Ton, Grafik oder anderen Daten relevant sein können, werden in einer Folge von Objekten gespeichert, die flüchtig sind oder allen Threads universell zur Verfügung stehen, jedoch niemals im Thread-Speicher gespeichert werden. Da gibt es eine leichte Leistungsbeeinträchtigung, aber richtig eingesetzt, hat es mir ermöglicht, Audio flexibel einem Thread, Grafik einem anderen, Physik einem anderen und so weiter zuzuweisen, ohne sie in die traditionelle (und gefürchtete) "Spieleschleife" einzubinden.

In der Regel durchlaufen alle OpenGL-Aufrufe den Grafikthread, alle OpenAL-Aufrufe den Audiothread, alle Eingaben über den Eingabethread und alles, worüber sich der organisierende Steuerthread Gedanken machen muss, ist die Threadverwaltung. Der Spielstatus wird in der GameState-Klasse abgehalten, die alle nach Bedarf einsehen können. Wenn ich mich jemals dazu entscheide, beispielsweise, dass JOAL veraltet ist und ich stattdessen die neue Edition von JavaSound verwenden möchte, implementiere ich nur einen anderen Thread für Audio.

Hoffentlich sehen Sie, was ich sage, ich habe bereits ein paar tausend Zeilen zu diesem Projekt. Wenn Sie möchten, dass ich versuche, eine Probe zusammen zu kratzen, werde ich sehen, was ich tun kann.

Michael Oberlin
quelle
Das Problem, dem Sie möglicherweise gegenüberstehen, ist, dass dieses Setup auf einem Multi-Core-Computer nicht besonders gut skalierbar ist. Ja, es gibt Aspekte eines Spiels, die im Allgemeinen am besten in einem eigenen Thread bereitgestellt werden, z. B. Audio, aber ein Großteil der verbleibenden Spielschleife kann tatsächlich seriell in Verbindung mit Thread-Pool-Aufgaben verwaltet werden. Wenn Ihr Thread-Pool Affinitätsmasken unterstützt, können Sie sich einfach in eine Warteschlange einreihen, um beispielsweise Rendering-Aufgaben auszuführen, die auf demselben Thread ausgeführt werden sollen, und Ihren Thread-Scheduler dazu bringen, Thread-Arbeitswarteschlangen zu verwalten und nach Bedarf zu stehlen.
Naros
1

Normalerweise wird die Logik, die sich mit dem Rendern von Grafiken befasst, (und ihr Zeitplan und wann sie ausgeführt werden usw.) von einem separaten Thread behandelt. Dieser Thread ist jedoch bereits von der Plattform implementiert (in Betrieb), die Sie zum Entwickeln Ihrer Spieleschleife (und des Spiels) verwenden.

Um eine Spieleschleife zu erhalten, in der die Spielelogik unabhängig vom Grafikaktualisierungszeitplan aktualisiert wird, müssen Sie keine zusätzlichen Threads erstellen. Tippen Sie einfach auf den bereits vorhandenen Thread für die Grafikaktualisierungen.

Dies hängt davon ab, welche Plattform Sie verwenden. Beispielsweise:

  • Wenn Sie dies auf den meisten Open GL-bezogenen Plattformen tun ( GLUT für C / C ++ , JOLG für Java , OpenGL ES-bezogene Aktion für Android ), erhalten Sie normalerweise eine Methode / Funktion, die regelmäßig vom Rendering-Thread aufgerufen wird und die Sie verwenden kann in Ihre Spieleschleife integriert werden (ohne dass die Iterationen der Gameloops davon abhängen, wann diese Methode aufgerufen wird). Für GLUT mit C machen Sie so etwas:

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • In JavaScript können Sie Web Worker verwenden, da es kein Multithreading gibt (das Sie programmatisch erreichen können) . Sie können auch den "requestAnimationFrame" -Mechanismus verwenden, um benachrichtigt zu werden, wenn ein neues Grafik-Rendering geplant wird, und Ihre Spielstatusaktualisierungen entsprechend vornehmen .

Grundsätzlich möchten Sie eine Mixed-Step-Spieleschleife: Sie haben einen Code, der den Spielstatus aktualisiert und der im Haupt-Thread Ihres Spiels aufgerufen wird, und Sie möchten auch regelmäßig auf den bereits vorhandenen Code zugreifen (oder von diesem zurückgerufen werden) Bestehender Grafik-Rendering-Thread für Heads-Ups, um festzustellen, wann es Zeit ist, die Grafiken zu aktualisieren.

Shivan Drache
quelle
0

In Java gibt es das Schlüsselwort "synchronized", das Variablen sperrt, die Sie übergeben, um sie threadsicher zu machen. In C ++ können Sie dasselbe mit Mutex erreichen. Z.B:

Java:

synchronized(a){
    //code using a
}

C ++:

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

Durch das Sperren von Variablen wird sichergestellt, dass sie sich nicht ändern, während der darauf folgende Code ausgeführt wird. Daher werden Variablen von Ihrem Aktualisierungsthread nicht geändert, während Sie sie rendern. t). Sie müssen jedoch mit dem synchronisierten Schlüsselwort in Java aufpassen, da es nur sicherstellt, dass sich der Zeiger auf die Variable / das Objekt nicht ändert. Die Attribute können sich weiterhin ändern, ohne den Zeiger zu ändern. Um dies zu erwägen, können Sie das Objekt selbst kopieren oder synchronisiert für alle Attribute des Objekts aufrufen, die Sie nicht ändern möchten.

zedutchgandalf
quelle
1
Mutexe sind hier nicht unbedingt die Antwort, da das OP nicht nur die Spielelogik und das Rendering entkoppeln muss, sondern auch verhindern möchte, dass die Fähigkeit eines Threads blockiert wird, sich in seiner Verarbeitung vorwärts zu bewegen, unabhängig davon, wo sich der andere Thread gerade in seiner Verarbeitung befindet Schleife.
Naros
0

Was ich allgemein gesehen habe, um Logik / Render-Thread-Kommunikation zu handhaben, ist, Ihre Daten dreifach zu puffern. Auf diese Weise sagt der Render-Thread Bucket 0, aus dem er liest. Der Logik-Thread verwendet Bucket 1 als Eingabequelle für den nächsten Frame und schreibt die Frame-Daten in Bucket 2.

An den Synchronisationspunkten werden die Indizes der Bedeutungen der drei Buckets vertauscht, sodass die Daten des nächsten Frames an den Render-Thread übergeben werden und der Logik-Thread vorwärts fortgesetzt werden kann.

Es gibt jedoch nicht unbedingt einen Grund, Rendering und Logik in ihre jeweiligen Threads zu unterteilen. Tatsächlich können Sie die serielle Spielschleife beibehalten und Ihre Render-Framerate durch Interpolation vom Logikschritt entkoppeln. Wenn Sie die Vorteile von Multi-Core-Prozessoren mit dieser Art von Setup nutzen möchten, verfügen Sie über einen Thread-Pool, der Gruppen von Aufgaben bearbeitet. Bei diesen Aufgaben kann es sich einfach um Dinge handeln, anstatt eine Liste von Objekten von 0 bis 100 zu iterieren. Sie iterieren die Liste in 5 Buckets von 20 über 5 Threads, wodurch Sie effektiv Ihre Leistung steigern, aber die Hauptschleife nicht übermäßig verkomplizieren.

Naros
quelle
0

Dies ist ein alter Beitrag, der aber immer noch auftaucht. Ich wollte meine 2 Cent hier hinzufügen.

Erste Liste Daten, die in UI / Display-Thread vs Logikthread gespeichert werden sollen. Im UI-Thread können Sie 3D-Mesh, Texturen, Lichtinformationen und eine Kopie der Positions- / Rotations- / Richtungsdaten einfügen.

Im Spielelogik-Thread benötigen Sie möglicherweise die Größe des Spielobjekts in 3D, Begrenzungsprimitive (Kugel, Würfel), vereinfachte 3D-Mesh-Daten (z. B. für detaillierte Kollisionen), alle Attribute, die Bewegung / Verhalten beeinflussen, wie Objektgeschwindigkeit, Drehungsverhältnis usw. und auch Positions- / Rotations- / Richtungsdaten.

Wenn Sie zwei Listen vergleichen, sehen Sie, dass nur eine Kopie der Positions- / Rotations- / Richtungsdaten von der Logik an den UI-Thread übergeben werden muss. Möglicherweise benötigen Sie auch eine Art Korrelations-ID, um festzustellen, zu welchem ​​Spielobjekt diese Daten gehören.

Wie Sie dies tun, hängt davon ab, mit welcher Sprache Sie arbeiten. In Scala können Sie Software Transactional Memory verwenden, in Java / C ++ eine Art Sperren / Synchronisieren. Ich mag unveränderliche Daten, daher neige ich dazu, bei jedem Update ein neues unveränderliches Objekt zurückzugeben. Dies ist ein bisschen Speicherverschwendung, aber mit modernen Computern ist es keine so große Sache. Wenn Sie freigegebene Datenstrukturen sperren möchten, können Sie dies dennoch tun. Schauen Sie sich die Exchanger-Klasse in Java an. Die Verwendung von zwei oder mehr Puffern kann die Dinge beschleunigen.

Bevor Sie mit dem Teilen von Daten zwischen Threads beginnen, müssen Sie herausfinden, wie viele Daten Sie tatsächlich übergeben müssen. Wenn Sie einen Octree haben, der Ihren 3D-Raum unterteilt, und Sie können 5 von 10 Objekten sehen, auch wenn Ihre Logik alle 10 aktualisieren muss, müssen Sie nur die 5 neu zeichnen, die Sie sehen. Weitere Informationen finden Sie in diesem Blog: http://gameprogrammingpatterns.com/game-loop.html Hier geht es nicht um die Synchronisierung, sondern darum, wie die Spielelogik von der Anzeige getrennt ist und welche Herausforderungen Sie bewältigen müssen (FPS). Hoffe das hilft,

Kennzeichen

Mark Citizen
quelle