Optimierungen durch Stapelverarbeitung

Einführung

Spiele-Engines müssen eine Reihe von Anweisungen an die GPU senden um der GPU mitzuteilen, was und wo sie zeichnen sollen. Diese Anweisungen werden mithilfe allgemeiner Anweisungen gesendet, die als APIs (Application Programming Interfaces) bezeichnet werden. Beispiele hierfür sind OpenGL, OpenGL ES und Vulkan.

Unterschiedliche APIs verursachen beim Zeichnen von Objekten unterschiedliche Kosten. OpenGL erledigt eine Menge Arbeit für den Benutzer im GPU-Treiber auf Kosten teurerer Zeichnungsaufrufe. Infolgedessen können Anwendungen häufig beschleunigt werden, indem die Anzahl der Zeichnungsaufrufe verringert wird.

Zeichnungsaufrufe

In 2D müssen wir die GPU anweisen eine Reihe von Grundelementen (Rechtecke, Linien, Polygone usw.) zu rendern. Die naheliegendste Technik besteht darin die GPU anzuweisen, jeweils ein Grundelement zu rendern, indem sie einige Informationen wie die verwendete Textur, das Material, die Position, die Größe usw. mitteilt und dann "zeichnen!" sagt. (Dies wird als Zeichnungsaufruf bezeichnet).

Es zeigt sich, dass dies auf der Engineseite zwar konzeptionell einfach ist, GPUs jedoch auf diese Weise sehr langsam arbeiten. GPUs arbeiten viel effizienter wenn man ihnen sagen, dass sie nicht einzelne Grundelemente, sondern eine ganze Reihe ähnlicher Grundelemente in einem einzigen Zeichnungsaufruf zeichnen sollen, den wir als "Stapel" bezeichnen.

Und es zeigt sich, dass sie auf diese Weise nicht nur ein bisschen schneller arbeiten, sondern viel schneller.

Da Godot als Allzweck-Engine konzipiert ist, können die in den Godot-Renderer eintretenden Grundelemente in beliebiger Reihenfolge vorliegen, manchmal ähnlich und manchmal unähnlich. Um die Möglichkeiten von Godot mit den Stapelpräferenzen von GPUs abzugleichen, verfügt Godot über eine Zwischenschicht, die nach Möglichkeit automatisch Grundelemente gruppieren und diese Stapel an die GPU weiterleiten kann. Dies kann zu einer Steigerung der Renderleistung führen, während nur wenige Änderungen an Ihrem Godot-Projekt erforderlich sind, wenn überhaupt.

Wie es funktioniert

Anweisungen werden von Ihrem Spiel in Form einer Reihe von Elementen in den Renderer eingegeben, von denen jedes einen oder mehrere Befehle enthalten kann. Die Elemente entsprechen Nodes im Szenenbaum, und die Befehle entsprechen Grundelementen wie Rechtecken oder Polygonen. Einige Elemente, wie z.B. TileMaps und Text, können eine große Anzahl von Befehlen enthalten (Kacheln bzw. Buchstaben). Andere, wie z.B. Sprites, enthalten möglicherweise nur einen einzigen Befehl (Rechteck).

Der Stapler verwendet zwei Haupttechniken, um Grundelemente zu gruppieren:

  • Aufeinanderfolgende Elemente können miteinander verbunden werden.

  • Aufeinanderfolgende Befehle innerhalb eines Elements können zu einem Stapel zusammengefügt werden.

Stapel unterbrechen

Das Stapeln kann nur stattfinden, wenn die Elemente oder Befehle ähnlich genug sind, um in einem Zeichnungsaufruf gerendert zu werden. Bestimmte Änderungen (oder Techniken) verhindern notwendigerweise die Bildung eines zusammenhängenden Stapels. Dies wird als "unterbrechen des Stapels" bezeichnet.

Stapeln wird (unter anderem) unterbrochen durch:

  • Ändern der Textur.

  • Ändern des Materials.

  • Ändern des primitiven Typs (z.B. von Rechtecken zu Linien).

Bemerkung

Wenn Sie beispielsweise eine Reihe von Sprites mit jeweils unterschiedlicher Textur zeichnen, können diese auf keinen Fall gestapelt werden.

Festlegen der Renderreihenfolge

Es stellt sich die Frage, ob nur ähnliche Elemente in einem Stapel zusammengezogen werden können. Warum gehen wir nicht alle Elemente in einer Szene durch, gruppieren alle ähnlichen Elemente und zeichnen sie zusammen?

In 3D funktionieren Engines oft genau so. In Godots 2D Renderer werden die Elemente jedoch in der "Reihenfolge eines Malers" von hinten nach vorne gezeichnet. Dadurch wird sichergestellt, dass vordere Elemente über dahinter liegenden (also vorher erstellte) Elementen gezeichnet werden, wenn sie sich überlappen.

Dies bedeutet auch, dass, wenn wir versuchen Objekte in der Reihenfolge ihrer Textur zu zeichnen, diese Malerreihenfolge möglicherweise unterbrochen wird und Objekte in der falschen Reihenfolge gezeichnet werden.

In Godot wird diese Reihenfolge von hinten nach vorne bestimmt durch:

  • Die Reihenfolge der Objekte im Szenenbaum.

  • Der Z-Index von Objekten.

  • Die Canvas-Ebene.

  • YSort Nodes.

Bemerkung

Sie können ähnliche Objekte zusammenfassen, um das Stapeln zu vereinfachen. Dies ist zwar keine Voraussetzung, aber stellen Sie sich dies als optionalen Ansatz vor, der in einigen Fällen die Leistung verbessern kann. Weitere Informationen finden Sie im Abschnitt Diagnose: Diagnose.

Ein Trick

Und jetzt ein Kunststück: Obwohl die Idee der Malerreihenfolge ist, dass Objekte von hinten nach vorne gerendert werden, betrachten wir nun 3 Objekte A, B und C, die 2 verschiedene Texturen enthalten, Gras und Holz.

../../_images/overlap1.png

In der Malerreihenfolge werden sie so sortiert:

A - wood
B - grass
C - wood

Da sich die Textur ändert, können sie nicht gestapelt werden und werden in 3 Zeichnungsaufrufen gerendert.

Die Bestellung des Malers wird jedoch nur unter der Annahme benötigt, dass sie übereinander gezeichnet werden. Wenn wir diese Annahme lockern, d.h. wenn sich keines dieser 3 Objekte überlappt, besteht keine Notwendigkeit die Malerreihenfolge beizubehalten. Das gerenderte Ergebnis ist das gleiche. Was wäre, wenn wir dies nutzen könnten?

Objekte neu ordnen

../../_images/overlap2.png

Elemente können auch neu geordnet werden. Dies ist jedoch nur möglich, wenn die Elemente die Bedingungen eines Überlappungstests erfüllen. Hiermit wird sichergestellt, dass das Endergebnis das gleiche ist, als ob sie nicht neu angeordnet worden wären. Der Überlappungstest ist in Bezug auf die Leistung sehr günstig, aber nicht absolut kostenlos. Daher ist es mit geringen Kosten verbunden vorausschauend zu entscheiden, ob Elemente neu geordnet werden können. Die Anzahl der Elemente, die neu geordnet werden müssen, kann in den Projekteinstellungen (siehe unten) festgelegt werden, um Kosten und Nutzen in Ihrem Projekt auszugleichen.

A - wood
C - wood
B - grass

Da sich die Textur nur einmal ändert, können wir das Obige in nur 2 Zeichnungsaufrufen rendern.

Lichter

Obwohl die Arbeit für das Stapelsystem normalerweise recht einfach ist, wird sie bei Verwendung von 2D-Lichtern erheblich komplexer, da Lichter in mehreren Durchgängen gezeichnet werden, einer für jede Lichtquelle, die das Grundelement beeinflusst. Betrachten wir 2 Sprites A und B mit identischer Textur und identischem Material. Ohne Beleuchtung würden sie in einem Zeichnungsaufruf zusammengefasst und gezeichnet. Mit 3 Lichtquellen jedoch würden sie wie folgt gezeichnet, wobei jede Linie ein Zeichnungsaufruf ist:

../../_images/lights_overlap.png
A
A - light 1
A - light 2
A - light 3
B
B - light 1
B - light 2
B - light 3

Das sind viele Zeichnungsaufrufe, 8 für nur 2 Sprites. Stellen Sie sich nun vor wir zeichnen 1000 Sprites, die Anzahl der Zeichnungsaufrufe wird schnell astronomisch und die Leistung wird leiden. Dies ist teilweise der Grund, warum Beleuchtung das Potenzial hat das 2D-Rendering drastisch zu verlangsamen.

Wenn Sie sich jedoch an den Trick bei der Neuordnung von Gegenständen erinnern, können wir mit demselben Trick die Maler-Reihenfolge für Beleuchtung umgehen!

Wenn sich A und B nicht überlappen, können wir sie in einem Stapel zusammen rendern, sodass der Zeichnungsvorgang wie folgt abläuft:

../../_images/lights_separate.png
AB
AB - light 1
AB - light 2
AB - light 3

Das sind nur 4 Zeichnungsaufrufe. Nicht schlecht, das ist eine 50%-ige Verbesserung. Bedenken Sie jedoch, dass Sie in einem echten Spiel möglicherweise an die 1000 Sprites herankommen.

  • Vorher: 1000 * 4 = 4000 Zeichnungsaufrufe.

  • Nachher: 1 * 4 = 4 Zeichnungsaufrufe.

Dies ist eine 1000-fache Verringerung der Zeichnungsaufrufe und sollte zu einer enormen Leistungssteigerung führen.

Überlappungstest

However, as with the item reordering, things are not that simple. We must first perform the overlap test to determine whether we can join these primitives. This overlap test has a small cost. Again, you can choose the number of primitives to lookahead in the overlap test to balance the benefits against the cost. With lights, the benefits usually far outweigh the costs.

Also consider that depending on the arrangement of primitives in the viewport, the overlap test will sometimes fail (because the primitives overlap and therefore shouldn't be joined). In practice, the decrease in draw calls may be less dramatic than in a perfect situation with no overlapping at all. However, performance is usually far higher than without this lighting optimization.

Beleuchtung beschneiden

Batching can make it more difficult to cull out objects that are not affected or partially affected by a light. This can increase the fill rate requirements quite a bit and slow down rendering. Fill rate is the rate at which pixels are colored. It is another potential bottleneck unrelated to draw calls.

In order to counter this problem (and speed up lighting in general), batching introduces light scissoring. This enables the use of the OpenGL command glScissor(), which identifies an area outside of which the GPU won't render any pixels. We can greatly optimize fill rate by identifying the intersection area between a light and a primitive, and limit rendering the light to that area only.

Light scissoring is controlled with the scissor_area_threshold project setting. This value is between 1.0 and 0.0, with 1.0 being off (no scissoring), and 0.0 being scissoring in every circumstance. The reason for the setting is that there may be some small cost to scissoring on some hardware. That said, scissoring should usually result in performance gains when you're using 2D lighting.

Die Beziehung zwischen dem Schwellenwert und der Frage, ob eine Beschneidungsoperation stattfindet, ist nicht ganz einfach, sondern repräsentiert im Allgemeinen den Pixelbereich, der möglicherweise durch eine Beschneidungsoperation "gespeichert" wird (d.h. die gespeicherte Füllrate). Bei 1,0 müssten die gesamten Bildschirmpixel gespeichert werden, was selten oder nie der Fall ist, sodass es ausgeschaltet ist. In der Praxis werden die nützlichen Werte gegen Null gehen, da nur ein kleiner Prozentsatz der Pixel gespeichert werden muss, damit die Operation nützlich ist.

Die genaue Beziehung ist wahrscheinlich nicht erforderlich, damit sich Benutzer Sorgen machen müssen, aber aus Interesse ist es im Anhang enthalten: Schwelle der Licht-Beschneidung berechnen

Light scissoring example diagram

Unten rechts ist ein Lichtquelle, der rote Bereich sind die Pixel, die durch die Beschneidungsoperation gespeichert wurden. Es muss nur der überschneidende Bereich gerendert werden.

Vertex brennen (englisch: Baking)

Der GPU-Shader erhält auf zwei Arten Anweisungen zum Zeichnen:

  • Shader-Uniforms (z.B. Farbe modulieren, Objekttransformation).

  • Vertex-Attribute (Vertex-Farbe, lokale Transformation).

However, within a single draw call (batch), we cannot change uniforms. This means that naively, we would not be able to batch together items or commands that change final_modulate or an item's transform. Unfortunately, that happens in an awful lot of cases. For instance, sprites are typically individual nodes with their own item transform, and they may have their own color modulate as well.

Um dieses Problem zu umgehen, kann die Stapelverarbeitung einige der Uniforms in die Vertex-Attribute "brennen".

  • Die Elementtransformation kann mit der lokalen Transformation kombiniert und in einem Vertex-Attribut gesendet werden.

  • Die endgültige Modulationsfarbe kann mit den Vertex-Farben kombiniert und in einem Vertex-Attribut gesendet werden.

In den meisten Fällen funktioniert dies einwandfrei, aber diese Verknüpfung funktioniert nicht, wenn ein Shader einzelne und nicht kombiniert verfügbare Werte erwartet. Dies kann in benutzerdefinierten Shadern passieren.

Benutzerdefinierte Shader

Infolgedessen verhindern bestimmte Vorgänge in benutzerdefinierten Shadern das brennen und verringern somit die Möglichkeit zu Stapeln. Während wir daran arbeiten diese Fälle zu verringern, gelten derzeit die folgenden Bedingungen:

  • Lesen oder Schreiben von COLOR oder MODULATE deaktiviert das brennen von Vertex-Farben.

  • Lesen von VERTEX deaktiviert das brennen der Vertex-Position.

Projekteinstellungen

To fine-tune batching, a number of project settings are available. You can usually leave these at default during development, but it's a good idea to experiment to ensure you are getting maximum performance. Spending a little time tweaking parameters can often give considerable performance gains for very little effort. See the on-hover tooltips in the Project Settings for more information.

Rendern / Stapelverarbeitung / Optionen

  • use_batching - schaltet die Stapelverarbeitung ein oder aus.

  • use_batching_in_editor Schaltet die Stapelverarbeitung im Godot-Editor ein oder aus. Diese Einstellung wirkt sich in keiner Weise auf das laufende Projekt aus.

  • single_rect_fallback - Dies ist eine schnellere Methode zum Zeichnen nicht stapelbarer Rechtecke, kann jedoch auf einiger Hardware zu Flimmern führen und wird daher nicht empfohlen.

Rendern / Stapelverarbeitung / Parameter

  • max_join_item_commands - Eine der wichtigsten Möglichkeiten zum Stapeln besteht darin, geeignete benachbarte Elemente (Nodes) miteinander zu verbinden. Sie können jedoch nur verbunden werden, wenn die darin enthaltenen Befehle kompatibel sind. Das System muss daher einen Blick auf die Befehle in einem Element werfen um festzustellen, ob diese verbunden werden können. Dies hat geringe Kosten pro Befehl und Elemente mit einer großen Anzahl von Befehlen sind es nicht wert, verbunden zu werden. Daher kann der beste Wert projektabhängig sein.

  • coloured_vertex_format_threshold - Das brennen von Farben in Eckpunkte führt zu einem größeren Vertex-Format. Dies ist nicht unbedingt sinnvoll, es sei denn, innerhalb eines verbundenen Elements werden viele Farbänderungen vorgenommen. Dieser Parameter gibt den Anteil der Befehle an, die Farbänderungen enthalten, bzw. die Gesamtzahl der Befehle, ab denen zu gebrannten Farben gewechselt wird.

  • batch_buffer_size - Dies bestimmt die maximale Größe eines Stapels. Dies hat keinen großen Einfluss auf die Leistung, kann sich jedoch für Mobilgeräte verringern, wenn der RAM knapp ist.

  • item_reordering_lookahead - Die Neuordnung von Elementen kann insbesondere bei verschachtelten Sprites mit unterschiedlichen Texturen hilfreich sein. Die Vorschau auf den Überlappungstest ist mit geringen Kosten verbunden, sodass sich der beste Wert je Projekt ändern kann.

Rendern / Stapelverarbeitung / Beleuchtung

  • scissor_area_threshold - siehe Beleuchtung beschneiden.

  • max_join_items - Joining items before lighting can significantly increase performance. This requires an overlap test, which has a small cost, so the costs and benefits may be project dependent, and hence the best value to use here.

Rendern / Stapelverarbeitung / Fehlersuche

  • flash_batching - This is purely a debugging feature to identify regressions between the batching and legacy renderer. When it is switched on, the batching and legacy renderer are used alternately on each frame. This will decrease performance, and should not be used for your final export, only for testing.

  • diagnose_frame - Hiermit wird in regelmäßigen Abständen ein Diagnose-Stapelprotokoll an die Godot-IDE / Konsole ausgegeben.

Rendern / Stapelverarbeitung / Präzision

  • uv_contract - Auf einigen Hardware-Geräten (insbesondere einigen Android-Geräten) wurde berichtet, dass TileMap-Kacheln leicht außerhalb ihres UV-Bereichs gezeichnet wurden, was zu Kantenartefakten wie Linien um Kacheln führte. Wenn dieses Problem auftritt, aktivieren Sie den UV-Verenger. Dies führt zu einer kleinen Kontraktion der UV-Koordinaten, um Präzisionsfehler an Geräten auszugleichen.

  • uv_contract_amount - Normalerweise sollte der Standardwert Artefakte auf den meisten Geräten beheben, aber falls nicht kann dieser Wert bearbeitet werden.

Diagnose

Although you can change parameters and examine the effect on frame rate, this can feel like working blindly, with no idea of what is going on under the hood. To help with this, batching offers a diagnostic mode, which will periodically print out (to the IDE or console) a list of the batches that are being processed. This can help pinpoint situations where batching isn't occurring as intended, and help you fix these situations to get the best possible performance.

Diagnose lesen und verstehen

canvas_begin FRAME 2604
items
    joined_item 1 refs
            batch D 0-0
            batch D 0-2 n n
            batch R 0-1 [0 - 0] {255 255 255 255 }
    joined_item 1 refs
            batch D 0-0
            batch R 0-1 [0 - 146] {255 255 255 255 }
            batch D 0-0
            batch R 0-1 [0 - 146] {255 255 255 255 }
    joined_item 1 refs
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
            batch D 0-0
            batch R 0-2560 [0 - 144] {158 193 0 104 } MULTI
canvas_end

Dies ist eine typische Diagnose.

  • joined_item: A joined item can contain 1 or more references to items (nodes). Generally, joined_items containing many references is preferable to many joined_items containing a single reference. Whether items can be joined will be determined by their contents and compatibility with the previous item.

  • batch R: A batch containing rectangles. The second number is the number of rects. The second number in square brackets is the Godot texture ID, and the numbers in curly braces is the color. If the batch contains more than one rect, MULTI is added to the line to make it easy to identify. Seeing MULTI is good as it indicates successful batching.

  • Stapel D - Ein Standard-Stapel, der alles andere enthält, was derzeit nicht gestapelt ist.

Standard Stapeldateien

Die zweite Zahl nach Standardstapeln ist die Anzahl der Befehle im Stapel, gefolgt von einer kurzen Zusammenfassung des Inhalts:

l - line
PL - polyline
r - rect
n - ninepatch
PR - primitive
p - polygon
m - mesh
MM - multimesh
PA - particles
c - circle
t - transform
CI - clip_ignore

Möglicherweise werden "Platzhalter"-Standardstapel angezeigt, die keine Befehle enthalten. Sie können diese ignorieren.

Häufig gestellte Fragen

Man bekommt keine große Leistungssteigerung durch das Einschalten der Stapelverarbeitung.

  • Probieren Sie die Diagnose aus um festzustellen, wie viel Stapelverarbeitung stattfindet und ob sie verbessert werden kann

  • Versuchen Sie, die Stapelparameter in den Projekteinstellungen zu ändern.

  • Bedenken Sie, dass das die Stapelverarbeitung möglicherweise nicht Ihr Engpass ist (siehe Engpässe).

Es entsteht ein Leistungsabfall beim Stapelverarbeiten.

  • Versuchen Sie die oben beschriebenen Schritte, um die Anzahl der Stapelmöglichkeiten zu erhöhen.

  • Versuchen Sie single_rect_fallback einzuschalten.

  • Die Einzelrechteck-Rückfall-Methode wird standardmäßig ohne Stapelverarbeitung verwendet und ist ungefähr doppelt so schnell. Bei einigen Hardwarekomponenten kann sie jedoch zu Flimmern führen. Daher wird von ihrer Verwendung abgeraten.

  • Wenn Ihre Szene nach dem Ausführen der obigen Schritte immer noch leistungsmäßig schlecht abschneidet, sollten Sie das Stapeln deaktivieren.

Bei Verwendung benutzerdefinierter Shader werden die Elemente nicht gestapelt.

  • Benutzerdefinierte Shader können beim Stapeln problematisch sein, siehe Abschnitt über benutzerdefinierte Shader

Linienartefakte sind sichtbar auf bestimmter Hardware.

  • Siehe die Projekteinstellung uv_contract mit der dieses Problem gelöst werden kann.

Bei Verwendung einer großen Anzahl von Texturen, werden nur wenige Elemente gestapelt.

  • Betrachten Sie die Verwendung von Texturatlanten. Diese ermöglichen nicht nur das Stapeln, sondern reduzieren auch die Notwendigkeit von Statusänderungen, die mit dem Ändern der Textur verbunden sind.

Anhang

Batched primitives

Not all primitives can be batched. Batching is not guaranteed either, especially with primitives using an antialiased border. The following primitive types are currently available:

  • RECT

  • NINEPATCH (abhängig vom Wrapping-Modus)

  • POLY

  • LINE

With non-batched primitives, you may be able to get better performance by drawing them manually with polys in a _draw() function. See 2D benutzerdefiniertes zeichnen for more information.

Schwelle der Licht-Beschneidung berechnen

Der tatsächliche Anteil der Bildschirmpixelfläche der als Schwellenwert verwendet wird, ist der Wert scissor_area_threshold hoch 4.

Zum Beispiel gibt es auf einer Bildschirmgröße von 1920 x 1080 2.073.600 Pixel.

Bei einer Schwelle von 1000 Pixeln wäre der Anteil:

1000 / 2073600 = 0.00048225
0.00048225 ^ (1/4) = 0.14819

Ein scissor_area_threshold von 0.15 wäre also ein vernünftiger Wert um es zu versuchen.

Gehen Sie in die andere Richtung, zum Beispiel mit einem scissor_area_threshold von 0.5:

0.5 ^ 4 = 0.0625
0.0625 * 2073600 = 129600 pixels

Wenn die Anzahl der gespeicherten Pixel diesen Schwellenwert überschreitet, wird die Beschneidung aktiviert.