Wie kann Rendering vom Aktualisieren des Modells sicher entkoppelt werden?

8

Im Gespräch mit einigen Spieleentwicklern haben sie vorgeschlagen, dass eine performante OpenGL ES-basierte Engine nicht alles im Hauptthread behandelt. Dadurch kann die Game Engine auf Geräten mit mehreren CPU-Kernen besser arbeiten.

Sie sagten, dass ich Updates vom Rendering entkoppeln könnte. Also, wenn ich das richtig verstanden habe, kann eine Spielmaschinenlaufschleife wie folgt funktionieren:

  1. Richten Sie einen CADisplayLink ein, der eine render Methode aufruft.

  2. render Methode rendert das aktuelle Weltmodell im Hintergrund.

  3. Die Methode
  4. render ruft dann die Methode update für den Hauptthread auf.

Während es im Hintergrund rendert, kann es gleichzeitig das Weltmodell für die nächste Iteration gleichzeitig aktualisieren.

Für mich fühlt sich das alles sehr wackelig an. Kann jemand erklären oder verlinken, wie dieses gleichzeitige Rendering + Aktualisieren des Modells in der Realität durchgeführt wird? Es ist mir schleierhaft, wie dies nicht zu Problemen führen würde, denn was ist, wenn das Modellupdate länger dauert als das Rendern oder anders herum? Wer wartet auf was und wann?

Was ich versuche zu verstehen, ist, wie dies theoretisch sowohl von einem hohen Standpunkt als auch im Detail umgesetzt wird.

    
Proud Member 02.12.2013, 11:06
quelle

2 Antworten

9

In "Realität" gibt es viele verschiedene Ansätze. Es gibt keinen "einen wahren Weg". Was für dich richtig ist, hängt wirklich von einem Los ab, auf Faktoren, die du in deiner Frage nicht besprochen hast, aber ich werde trotzdem eine Aufnahme machen. Ich bin mir auch nicht sicher, wie CADisplayLink hier ist. Ich denke normalerweise, dass das nützlich für Dinge ist, die Rahmensynchronisation erfordern (z. B. Lippensynchronisation von Audio und Video), was nicht so klingt, wie Sie es brauchen, aber lassen Sie uns ein paar verschiedene Möglichkeiten betrachten, wie Sie dies tun könnten. Ich denke, der Kern Ihrer Frage ist, ob es eine zweite "Schicht" zwischen dem Modell und der Ansicht gibt.

Hintergrund: Einzel-Thread (d. h. nur Haupt-Thread) Beispiel

Betrachten wir zunächst, wie eine normale Singlethread-App funktionieren könnte:

  1. Benutzerereignisse kommen im Hauptthread
  2. vor
  3. Event-Handler lösen Aufrufe von Controller-Methoden aus.
  4. Controller-Methoden aktualisieren den Modellstatus.
  5. Änderungen am Modellstatus machen den Ansichtszustand ungültig. (d. h. -setNeedsDisplay )
  6. Wenn der nächste Frame angezeigt wird, löst der Fensterserver ein erneutes Rendern des Ansichtszustands aus dem aktuellen Modellstatus aus und zeigt die Ergebnisse an

Beachten Sie, dass die Schritte 1-4 zwischen den Vorkommen von Schritt 5 oft vorkommen können, da dies eine Singlethread-App ist, während Schritt 5 stattfindet, die Schritte 1-4 nicht stattfinden und Benutzerereignisse in die Warteschlange gestellt werden bis zum Abschluss von Schritt 5. Dies wird in der Regel Frames in einer erwarteten Art und Weise fallenlassen, unter der Annahme, dass die Schritte 1-4 "sehr schnell" sind.

Entkopplung vom Haupt-Thread

Betrachten wir nun den Fall, in dem Sie das Rendering auf einen Hintergrundthread übertragen möchten. In diesem Fall sollte die Sequenz etwa so aussehen:

  1. Benutzerereignisse kommen im Hauptthread
  2. vor
  3. Event-Handler lösen Aufrufe von Controller-Methoden aus.
  4. Controller-Methoden aktualisieren den Modellstatus.
  5. Bei Änderungen am Modellstatus wird eine asynchrone Rendering-Aufgabe für die Ausführung im Hintergrund in die Warteschlange gestellt.
  6. Wenn der asynchrone Rendering-Task abgeschlossen ist, wird die resultierende Bitmap in der Ansicht an anderer Stelle bekannt und ruft -setNeedsDisplay in der Ansicht auf.
  7. Wenn der nächste Frame angezeigt wird, löst der Fensterserver einen Aufruf von -drawRect in der Ansicht aus, die nun so implementiert wird, dass die zuletzt fertiggestellte Bitmap vom "bekannten freigegebenen Ort" genommen und in die Ansicht kopiert wird.

Hier gibt es ein paar Nuancen. Betrachten wir zunächst den Fall, in dem Sie lediglich versuchen, das Rendering vom Hauptthread zu entkoppeln (und im Moment die Verwendung mehrerer Kerne zu ignorieren - mehr später):

Sie wollen mit Sicherheit nie mehr als eine Renderaufgabe gleichzeitig ausführen. Sobald Sie mit dem Rendern eines Frames beginnen, möchten Sie ihn wahrscheinlich nicht abbrechen / stoppen. Wahrscheinlich möchten Sie zukünftige, nicht gestartete Rendering-Vorgänge in einer einzelnen Slot-Queue in Warteschlange einreihen, die immer die letzte eingereihte nicht gestartete Render-Operation enthält. Dies sollte Ihnen ein vernünftiges Frame-Drop-Verhalten geben, damit Sie nicht "hinter" Render-Frames geraten, die Sie stattdessen einfach fallen lassen sollten.

Wenn es einen vollständig gerenderten, aber noch nicht angezeigten Rahmen gibt, denke ich, dass Sie immer diesen Rahmen anzeigen möchten. In diesem Sinne möchten Sie -setNeedsDisplay nicht in der Ansicht aufrufen, bis die Bitmap vollständig und an der bekannten Stelle ist.

Sie müssen Ihren Zugriff über die Threads synchronisieren. Wenn Sie beispielsweise die Rendering-Operation in die Warteschlange stellen, besteht der einfachste Ansatz darin, einen schreibgeschützten Snapshot des Modellstatus zu erstellen und ihn an die Renderoperation zu übergeben, die nur aus dem Snapshot liest. Dadurch müssen Sie sich nicht mehr mit dem "Live" -Modell synchronisieren (das durch Ihre Controller-Methoden als Reaktion auf zukünftige Benutzerereignisse im Hauptthread mutiert werden könnte.) Die andere Synchronisierungsaufgabe besteht darin, die fertigen Bitmaps an die Ansicht weiterzuleiten und der Aufruf von -setNeedsDisplay . Der einfachste Ansatz besteht wahrscheinlich darin, dass das Bild eine Eigenschaft in der Ansicht ist und die Einstellung dieser Eigenschaft (mit dem vollständigen Bild) und der Aufruf von -setNeedsDisplay an den Hauptthread gesendet wird.

Hier gibt es ein kleines Problem: Wenn Benutzerereignisse mit einer hohen Rate eingehen und Sie mehrere Frames in der Dauer eines einzelnen Display-Frames (1 / 60s) wiedergeben können, könnten Sie Rendern Bitmaps, die auf den Boden fallen. Dieser Ansatz hat den Vorteil, der Ansicht immer den aktuellsten Frame zur Anzeigezeit zur Verfügung zu stellen (verringerte wahrgenommene Latenz), aber er hat den Vorteil, dass er alle Rechenkosten zum Rendern der Frames verursacht, die niemals erhalten werden gezeigt (dh Macht). Der richtige Kompromiss hier wird für jede Situation anders sein und kann feinere Anpassungen beinhalten.

Verwendung mehrerer Kerne - inhärent paralleles Rendering

Wenn Sie davon ausgehen, dass Sie das Rendern vom Hauptthread wie oben beschrieben entkoppelt haben und Ihre Rendering-Operation selbst in sich parallelisierbar ist, dann parallelisieren Sie einfach Ihre eine Rendering-Operation, während Sie auf dieselbe Weise mit der Ansicht interagieren, und Sie sollten multi erhalten -Core-Parallelismus kostenlos. Vielleicht könnten Sie jeden Frame in N Kacheln unterteilen, wobei N die Anzahl der Kerne ist. Wenn alle N Kacheln fertig gerendert sind, können Sie sie zusammensetzen und an die Ansicht übergeben, als ob die Rendering-Operation monolithisch gewesen wäre. Wenn Sie mit einem schreibgeschützten Snapshot des Modells arbeiten, sollten die Setup-Kosten der N-Kachel-Tasks minimal sein (da sie alle dasselbe Quellmodell verwenden können).

Mehrfache Kerne verwenden - Inhärent serielles Rendern

Für den Fall, dass Ihre Rendering-Operation inhärent seriell ist (die meisten Fälle in meinen Erfahrungen), besteht Ihre Option, mehrere Cores zu verwenden, darin, so viele Rendering-Operationen wie Cores im Flug zu haben. Wenn ein Frame abgeschlossen ist, würde es alle eingereihten oder noch im Flug befindlichen Rendering-Operationen anzeigen, die sie aufgeben und abbrechen können, und dann würde sie sich selbst wie in dem Beispiel nur zur Entkopplung durch die Ansicht anzeigen lassen / p>

Wie im Fall der Entkopplung erwähnt, liefert dies immer den aktuellsten Frame für die Ansicht zur Anzeigezeit, aber es entstehen alle Rechen- (d. h. Energie) -Kosten des Renderns der Frames, die niemals angezeigt werden.

Wenn das Modell langsam ist ...

Ich habe keine Fälle angesprochen, in denen es tatsächlich die Aktualisierung des Modells aufgrund von Benutzerereignissen ist, die zu langsam ist, weil in gewisser Hinsicht, wenn das der Fall ist, Sie sich nicht mehr für das Rendern interessieren. Wie kann das Rendering mithalten, wenn das Modell nicht mithalten kann? Unter der Annahme, dass Sie einen Weg finden, das Rendering und die Modellberechnungen zu verzahnen, beraubt das Rendering immer Zyklen von den Modellberechnungen, die definitionsgemäß immer dahinter stehen. Mit anderen Worten, Sie können nicht hoffen, etwas N Mal pro Sekunde zu rendern, wenn das Etwas selbst nicht N mal pro Sekunde aktualisiert werden kann.

Ich kann mir Fälle vorstellen, in denen Sie etwas wie eine fortlaufende Physiksimulation in einen Hintergrundthread entladen könnten. Ein solches System müsste seine Echtzeit-Performance selbst verwalten, und wenn Sie dies tun, müssen Sie die Ergebnisse des Systems mit dem eingehenden Benutzer-Ereignis-Stream synchronisieren. Es ist ein Chaos.

Im allgemeinen Fall möchten Sie wirklich , dass die Ereignisbehandlung und Modellmutation -weise schneller als Echtzeit ist und das Rendering der "harte Teil" sein muss. Ich habe Mühe, mir einen aussagekräftigen Fall vorzustellen, in dem die Aktualisierung des Modells der limitierende Faktor ist, aber dennoch ist es wichtig, das Rendern für die Leistung zu entkoppeln.

Anders ausgedrückt: Wenn Ihr Modell nur mit 10 Hz aktualisiert werden kann, macht es keinen Sinn, Ihre Ansicht schneller als 10 Hz zu aktualisieren. Die Hauptherausforderung dieser Situation tritt auf, wenn Benutzerereignisse viel schneller als 10 Hz kommen. Diese Herausforderung besteht darin, die eingehenden Ereignisse sinnvoll zu verwerfen, abzutasten oder zu vereinigen, um sinnvoll zu bleiben und eine gute Benutzererfahrung zu bieten.

Irgendein Code

Hier ist ein einfaches Beispiel dafür, wie das entkoppelte Hintergrund-Rendering aussehen könnte, basierend auf der Cocoa Application-Vorlage in Xcode. (Ich erkannte nach Codierung dieses OS X-basierten Beispiel, dass die Frage mit ios getaggt wurde, also denke ich, das ist "für was auch immer es wert ist")

%Vor%

Fazit

In der Zusammenfassung kann nur das Entkoppeln des Renderings von dem Haupt-Thread, aber nicht notwendigerweise das Parallelisieren (d. h. der erste Fall) ausreichend sein. Um von dort weiter zu gehen, möchten Sie wahrscheinlich Möglichkeiten untersuchen, wie Sie Ihre Rendering-Operation pro Frame parallelisieren können. Die Parallelisierung der Zeichnung von mehreren Frames bringt einige Vorteile mit sich, aber in einer batteriebetriebenen Umgebung wie iOS wird es wahrscheinlich deine App / Spiel zu einem Batteriehog machen.

Für jede Situation, in der Modellaktualisierungen und nicht Rendering das limitierende Reagens sind, hängt der richtige Ansatz stark von den spezifischen Details der Situation ab und ist im Vergleich zum Rendering viel schwieriger zu verallgemeinern.

>     
ipmcc 05.12.2013, 15:45
quelle
1

Meine 2 Cent wert.

GL Spiel in meinem begrenzten Verständnis immer gehen update dann rendern.

update cycle aktualisiert grundsätzlich alle sich visuell im Spiel ändernden Teile (zB: Ort / Farbe / etc) auf ihren nächsten zeitlichen Wert. Dies kann außerhalb eines Worker-Threads geschehen, vielleicht in Ihrem Fall, vor der Zeit, in eine Menge von zukünftigen t, t + 1, t + 2, t + n zukünftigen Werten eingereiht.

render cycle macht das eigentliche Rendering innerhalb des Haupt-Threads mit dem oben berechneten Wert (t, t + 1, t + 2, t + n). Alle Rendering-Vorgänge müssen innerhalb des Haupt-Threads durchgeführt werden, sonst werden Sie bemerkenswerte Artefakte sehen. Im Rendering-Zyklus können Sie je nach Zeitunterschied Frames / Schnellvorlauf überspringen (zB: rendern t + 1, t + 4 Werte) oder in Zeitlupe spielen (t + 0.1, t + 0.2).

Viel Glück mit deinen Studien!

    
dklt 11.12.2013 08:17
quelle