Optimisation à l'aide de traitement par lots

Introduction

Les moteurs de jeu doivent envoyer un ensemble d'instructions au GPU afin de lui dire quoi et où dessiner. Ces instructions sont envoyées à l'aide d'instructions communes, appelées APIs. Des exemples d'API graphiques sont OpenGL, OpenGL ES et Vulkan.

Différentes API entraînent des coûts différents lors du dessin des objets. OpenGL gère beaucoup de travail pour l'utilisateur dans le pilote GPU, au prix de draw calls plus coûteux. Par conséquent, il est souvent possible d'accélérer les applications en réduisant le nombre dedraw calls.

Draw calls

En 2D, nous devons indiquer au GPU de rendre une série de primitives (rectangles, lignes, polygones, etc.). La technique la plus évidente consiste à dire au GPU de rendre une primitive à la fois, en lui donnant certaines informations telles que la texture utilisée, le matériau, la position, la taille, etc. puis en disant "Dessine !" (c'est ce qu'on appelle un draw call).

Bien que cela soit conceptuellement simple du côté du moteur, les GPU fonctionnent très lentement lorsqu'ils sont utilisés de cette manière. Les GPU fonctionnent beaucoup plus efficacement si vous leur dites de dessiner un certain nombre de primitives similaires en un seul appel de dessin, que nous appellerons "lot".

Et il s'avère qu'ils ne travaillent pas seulement un peu plus vite lorsqu'ils sont utilisés de cette manière, ils travaillent beaucoup plus vite.

Comme Godot est conçu pour être un moteur polyvalent, les primitives entrant dans le moteur de rendu de Godot peuvent être dans n'importe quel ordre, parfois similaires, parfois dissemblables. Afin de faire correspondre la nature généraliste de Godot avec les préférences de traitement par lots des GPU, Godot comporte une couche intermédiaire qui peut automatiquement regrouper les primitives lorsque cela est possible et envoyer ces lots sur le GPU. Cela peut augmenter les performances de rendu tout en n'exigeant que peu (ou pas) de modifications de votre projet Godot.

Comment ça marche

Les instructions arrivent dans le moteur de rendu de votre jeu sous la forme d'une série d'objets, chacun pouvant contenir une ou plusieurs commandes. Les objets correspondent à des Nodes dans l'arbre des scènes, et les commandes correspondent à des primitives telles que des rectangles ou des polygones. Certains objets, comme les TileMaps et le texte, peuvent contenir un grand nombre de commandes (respectivement des tuiles et des glyphes). D'autres, tels que les sprites, peuvent ne contenir qu'une seule commande (un rectangle).

Le traitement par lots utilise deux techniques principales pour regrouper les primitives :

  • Les éléments consécutifs peuvent être réunis ensemble.

  • Les commandes consécutives dans un élément peuvent être jointes pour former un lot.

Rupture de traitement par lots

Les lots ne peuvent avoir lieu que si les éléments ou les commandes sont suffisamment similaires pour être rendus en un draw call. Certaines modifications (ou techniques) empêchent par nécessité la formation d'un lot contigu, c'est ce qu'on appelle "breaking batching".

Le traitement par lots sera cassé par (entre autres) :

  • Changement de texture.

  • Changement de matériel.

  • Changement de type de primitive (par exemple, passer de rectangles à des lignes).

Note

Si, par exemple, vous dessinez une série de sprites ayant chacun une texture différente, il n'y a aucune possibilité de les regrouper.

Détermination de l’ordre de rendu

La question se pose : si seuls des objets similaires peuvent être rassemblés dans un lot, pourquoi ne pas examiner tous les objets d'une scène, regrouper tous les objets similaires et les dessiner ensemble ?

En 3D, c'est souvent exactement comme cela que les moteurs fonctionnent. Cependant, dans Godot 2D, les objets sont dessinés dans "l'ordre du peintre", de l'arrière vers l'avant. Cela permet de s'assurer que les éléments à l'avant sont dessinés par-dessus les éléments précédents lorsqu'ils se chevauchent.

Cela signifie également que si nous essayons de dessiner des objets en fonction de la texture, cette ordre du peintre risque de se rompre et les objets seront dessinés dans le mauvais ordre.

Dans Godot, cet ordre arrière-vers-avant est déterminé par :

  • L'ordre des objets dans l'arbre des scènes.

  • L'index Z des objets.

  • Couches de canevas.

  • YSort nœuds.

Note

Vous pouvez regrouper des objets similaires pour faciliter le traitement par lot. Bien que cela ne soit pas une obligation de votre part, considérez que c'est une approche optionnelle qui peut améliorer les performances dans certains cas. Consultez la section Diagnostiques pour vous aider à prendre cette décision.

Une astuce

Et maintenant, un tour de passe-passe. Bien que l'idée de l'ordre du peintre soit que les objets sont rendus de l'arrière vers l'avant, considérez 3 objets A, B et C, qui contiennent 2 textures différentes : de l'herbe et du bois.

../../_images/overlap1.png

Ils sont ordonnés, dans l'ordre du peintre :

A - wood
B - grass
C - wood

Comme la texture change, ils ne peuvent pas être mis en lots et seront rendus en 3 draw calls.

Cependant, l'ordre du peintre n'est nécessaire que dans l'hypothèse où ils seront dessinés sur le dessus les uns des autres. Si nous assouplissons cette hypothèse, c'est-à-dire si aucun de ces trois objets ne se chevauchent, il n'est pas nécessaire de préserver l'ordre du peintre. Le résultat rendu sera le même. Et si nous pouvions en tirer profit ?

Réorganisation des éléments

../../_images/overlap2.png

Il s'avère que nous pouvons réorganiser les éléments. Toutefois, nous ne pouvons le faire que si les éléments remplissent les conditions d'un test de chevauchement, afin de garantir que le résultat final sera le même que s'ils n'étaient pas réorganisés. Le test de chevauchement est très bon marché en termes de performance, mais il n'est pas absolument gratuit, de sorte qu'il y a un léger coût à tester pour décider si les éléments peuvent être réorganisés. Le nombre d'éléments à tester pour réorganisation peut être défini dans les paramètres du projet (voir ci-dessous), afin d'équilibrer les coûts et les bénéfices dans votre projet.

A - wood
C - wood
B - grass

Comme la texture ne change qu'une seule fois, nous pouvons rendre ce qui précède en seulement 2 draw calls.

Lumières

Bien que le travail du système de traitement par lots soit normalement assez simple, il devient considérablement plus complexe lorsque des lumières 2D sont utilisées. Ceci, car les lumières sont dessinées à l'aide de passes supplémentaires, une pour chaque lumière affectant la primitive. Considérez 2 sprites A et B, avec une texture et un matériau identiques. Sans lumières, ils seraient regroupés et dessiné en un seul appel. Mais avec 3 lumières, ils seraient dessinés comme suit, chaque ligne un draw call :

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

Cela fait beaucoup draw calls : 8 pour seulement 2 sprites. Si l'on considère que nous dessinons 1000 sprites. Le nombre de draw calls devient rapidement astronomique et les performances en souffrent. C'est en partie pour cette raison que les lumières ont le potentiel de ralentir considérablement le rendu 2D.

Cependant, si vous vous souvenez de notre astuce de magicien lors de la réorganisation des objets, il s'avère que nous pouvons utiliser la même astuce pour contourner l'ordre du peintre pour les lumières !

Si A et B ne se chevauchent pas, nous pouvons les rendre ensemble dans un lot, le processus de dessin est donc le suivant :

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

Cela fait 4 draw calls. Pas mal, c'est une reduction de 2×. Cependant, considérez que dans un jeu réel, vous pourriez dessiner prés de 1000 sprites.

  • Avant : 1000 * 4 = 4000 draw calls.

  • Après : 1 * 4 = 4 draw calls.

C'est une diminution de 1000 fois des draw calls, et cela devrait donner une énorme augmentation des performances.

Test de chevauchement

Cependant, comme pour la réorganisation des objets, les choses ne sont pas si simples. Nous devons d'abord effectuer le test de chevauchement pour déterminer si nous pouvons joindre ces primitives. Ce test de chevauchement a un faible coût. Encore une fois, vous pouvez choisir le nombre de primitives à rechercher dans le test de chevauchement pour équilibrer les avantages par rapport au coût. Habituellement, avec des lumières, les avantages l'emportent largement sur les coûts.

Considérez également que selon la disposition des primitives dans le viewport, le test de chevauchement échouera parfois (parce que les primitives se chevauchent et ne doivent donc pas être jointes). En pratique, la diminution des draw calls peut être moins dramatique que la situation parfaite sans chevauchement. Cependant, les performances sont généralement bien plus élevées que sans cette optimisation de l'éclairage.

Light Scissoring

Le traitement par lots peut rendre plus difficile l'occlusion des objets qui ne sont pas ou partiellement affectés par une lumière. Cela peut augmenter considérablement le taux de remplissage requis et ralentir le rendu. Le taux de remplissage est la vitesse à laquelle les pixels sont colorés. C'est un autre goulot d'étranglement potentiel sans rapport avec les draw calls.

Pour contrer ce problème (et accélérer l'éclairage en général), le traitement par lot introduit le light scissoring. Cela permet l'utilisation de la commande OpenGL glScissor(), qui identifie une zone en dehors de laquelle le GPU ne rendra aucun pixel. Nous pouvons ainsi optimiser considérablement le taux de remplissage en identifiant la zone d'intersection entre une lumière et une primitive, et limiter le rendu de la lumière à cette zone seulement.

Le light scissoring est contrôlé avec le paramètre de projet scissor_area_threshold. Cette valeur est comprise entre 1.0 et 0.0, avec 1.0 étant off (pas de scissoring), et 0.0 étant le scissoring dans toutes circonstances. La raison de ce réglage est que le scissoring peut avoir un petit coût sur certains hardware. Cela dit, le scissoring devrait généralement entraîner un gain de performance lorsque vous utilisez un éclairage 2D.

La relation entre le seuil et le fait qu'une opération de scissoring ait lieu n'est pas tout à fait simple. Généralement il représente la zone de pixels potentiellement "sauvegardée" par une opération de scissoring (c'est-à-dire la taux de remplissage sauvegardée). À 1.0, il faudrait sauvegarder tous les pixels de l'écran, ce qui n'arrive que rarement (voire jamais) c'est pourquoi il est désactivé. En pratique, les valeurs utiles sont regroupées vers zéro, car seul un petit pourcentage de pixels doit être sauvegardé pour que l'opération soit utile.

Il n'est probablement pas nécessaire aux les utilisateurs de s'inquiéter de la relation exacte , mais pour la curiosité, elle est inclus dans l'annexe : Calcul du seuil de light scissoring

Diagramme d'exemple du light scissoring

En bas à droite se trouve une lumière, la zone rouge correspond aux pixels sauvegardés par l'opération de scissoring. Seule l'intersection doit être rendue.

Pré-calcul de sommet

Le shader GPU reçoit des instructions sur ce qu'il faut dessiner de 2 manières principales :

  • Shader uniforms (par exemple, modulation de la couleur, transformation d'un objet).

  • Attributs de sommet (couleur de sommet, transformation locale).

Cependant, dans un seul draw call (batch), nous ne pouvons pas changer les uniforms. Cela signifie que naïvement, nous ne serions pas en mesure de regrouper des éléments ou des commandes qui changent final_modulate ou la transformation d'un objet. Malheureusement, c'est un très grand nombre de cas. Par exemple, les sprites sont généralement des nœuds individuels avec leur propre transformation, et ils peuvent avoir leur propre modulation de couleur.

Pour contourner ce problème, le traitement par lots peut "pré-calculer" certaines des uniforms dans les attributs de vertex.

  • La transformation d'élément peut être combinée avec la transformation locale et envoyée dans un attribut de sommet.

  • La couleur modulée finale peut être combinée avec les couleurs des vertex, et envoyée dans un attribut de vertex.

Dans la plupart des cas, cela fonctionne bien, mais ce raccourci casse si un shader s'attend à ce que ces valeurs soient disponibles individuellement plutôt que combinées. Cela peut se produire dans les shaders personnalisés.

Shaders personnalisés

En raison de la limitation décrite ci-dessus, certaines opérations dans les shaders personnalisés empêcheront le pré-calcul des sommets et réduiront donc le potentiel de traitement par lot. Bien que nous nous efforcions de réduire ces cas, les mises en garde suivantes s'appliquent actuellement :

  • La lecture ou l'écriture de COLOR ou MODULATE désactive le pré-cacul des couleurs de sommet.

  • La lecture de VERTEX désactive le pré-calcul de la position du sommet.

Paramètres du projet

Afin d'affiner le traitement par lots, un certain nombre de paramètres de projet sont disponibles. Vous pouvez généralement les laisser par défaut pendant le développement, mais il est bon d'expérimenter pour vous assurer que vous obtenez les performances maximales. Passer un peu de temps à régler les paramètres peut souvent donner un gain de performance considérable pour très peu d'efforts. Pour plus d'informations, consultez les infobulles dans les paramètres du projet.

rendu/traitement par lots/options

  • use_batching - Activation et désactivation du traitement par lots.

  • use_batching_in_editor Activation et désactivation du traitement par lots dans l'éditeur Godot. Ce paramètre n'affecte en rien le projet en cours d’exécution.

  • single_rect_fallback - C'est un moyen plus rapide de dessiner des rectangles ne pouvant être regroupés en lots, mais il peut entraîner un scintillement sur certains hardware, il n'est donc pas recommandé.

rendu/traitement par lots/paramètres

  • max_join_item_commands - L'une des façons les plus importantes de réaliser le traitement par lots est de joindre ensemble des éléments adjacents appropriés (nœuds), cependant ils ne peuvent être joints que si les commandes qu'ils contiennent sont compatibles. Le système doit donc faire une recherche parmi les commandes d'un élément pour déterminer s'il peut être joint. Le coût par commande est faible, et les éléments comportant un grand nombre de commandes ne valent pas la peine d'être joints, de sorte que la meilleure valeur peut dépendre du projet.

  • colored_vertex_format_threshold - Pré-calculer les couleurs dans les sommets donne un format de sommet plus grand. Cela ne vaut pas nécessairement la peine d'être fait, à moins qu'il y ait beaucoup de changements de couleur dans un élément joint. Ce paramètre représente la proportion de commandes contenant des changements de couleur / le total des commandes, au-dessus duquel il passe aux couleurs pré-calculées.

  • batch_buffer_size - Cela détermine la taille maximale d'un lot, cela n'a pas d'effet énorme sur les performances mais peut valoir la peine d'être diminuer pour les mobiles si la RAM une préoccupation.

  • item_reordering_lookahead - La réorganisation des éléments peut aider, en particulier pour les sprites entrelacés utilisant des textures différentes. Le lookahead pour le test de chevauchement a un faible coût, donc la meilleure valeur peut changer selon le projet.

rendu/traitement par lots/lumières

  • scissor_area_threshold - Voir light scissoring.

  • max_join_items - Joindre des éléments avant le calcule d'éclairage peut augmenter considérablement les performances. Cela nécessite un test de chevauchement, dont le coût est faible, de sorte que les coûts et les avantages peuvent dépendre du projet, et donc de la meilleure valeur à utiliser ici.

rendu/traitement par lots/débogage

  • flash_batching - Il s'agit d'une fonction de débogage qui permet d'identifier les régressions entre le rendu par lots et l'ancien rendu. Lorsqu'elle est activée, le rendus par lots et l'ancien rendu sont utilisés alternativement sur chaque trame. Cela diminue les performances et ne doit pas être utilisé pour l'exportation finale, mais uniquement pour les tests.

  • diagnose_frame - Cela permettra d'imprimer périodiquement un journal de diagnostic des lots sur l'IDE / la console de Godot.

rendu/traitement par lots/précision

  • uv_contract - Sur certains matériels (notamment certains appareils Android), il a été signalé que des tuiles de tilemap dessinaient légèrement en dehors de leur plage d'UV, entraînant des artefacts de bord tels que des lignes autour des tuiles. Si vous constatez ce problème, essayez d'activer contrat uv. Cela provoque une petite contraction des coordonnées UV pour compenser les erreurs de précision des appareils.

  • uv_contract_amount - Nous espérerons que la quantité par défaut devrait traiter les artefacts sur la plupart des appareils, mais juste au cas où, cette valeur est modifiable.

Diagnostiques

Bien que vous puissiez modifier les paramètres et examiner l'effet sur la fréquence d'image, vous pouvez avoir l'impression de travailler à l'aveugle, sans avoir la moindre idée de ce qui se passe sous le capot. Pour vous aider, le traitement par lots offre un mode diagnostique, qui imprimera périodiquement (dans l'IDE ou dans la console) une liste des lots en cours de traitement. Cela peut aider à repérer les situations où le traitement par lots ne se déroule pas comme prévu et à les corriger, afin d'obtenir les meilleures performances possibles.

Lecture d'un diagnostique

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

Ceci est un diagnostique typique.

  • joined_item : - Un élément joint peut contenir une ou plusieurs références à des éléments (nœuds). En général, les joined_items contenant de nombreuses références sont préférables à de nombreux joined_items contenant une seule référence. La possibilité de joindre des éléments sera déterminée par leur contenu et leur compatibilité avec l'élément précédent.

  • **batch R : ** Un lot contenant des rectangles. Le deuxième chiffre est le nombre de rectangles. Le deuxième nombre entre crochets est l'ID de texture Godot, et les nombres entre accolades est la couleur. Si le lot contient plus d'un rectangle, MULTI est ajouté à la ligne pour faciliter l'identification. Il est bon de voir MULTI, car cela indique que la mise en lot a réussi.

  • lot D : Un lot par défaut, contenant tout le reste qui n’est pas actuellement par lots.

Lots par défaut

Le deuxième chiffre qui suit les lots par défaut est le nombre de commandes dans le lot, et il est suivi d'un bref résumé du contenu :

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

Vous pouvez voir des lots par défaut "dummy" ne contenant aucune commande ; vous pouvez les ignorer.

Questions fréquentes

Je n'obtiens pas une grande augmentation de performance en utilisant le traitement par lots.

  • Essayez les diagnostiques, voyez dans quelle mesure le traitement par lots se fait, et si il peut être amélioré

  • Essayez de modifier les paramètres de traitement par lots dans les paramètres du projet.

  • Considérez que le traitement par lots n'est peut-être pas votre goulot d'étranglement (voir goulots d'étranglement).

J'obtiens une diminution de performance avec le traitement par lots.

  • Essayez les étapes décrites ci-dessus pour augmenter le nombre de possibilités de traitement par lots.

  • Essayez d'activer single_rect_fallback.

  • La méthode de repli à rectangle unique est la méthode par défaut utilisée sans traitement par lots, et elle est environ deux fois plus rapide, mais elle peut entraîner un scintillement sur certains hardware, son utilisation est donc déconseillée.

  • Après avoir essayé ce qui précède, si votre scène se déroule toujours moins bien, envisagez d'arrêter le traitement par lots.

J'utilise des shaders personnalisés et les éléments ne sont pas traiter par lots.

  • Les shaders personnalisés peuvent poser des problèmes pour la mise en lots, voir la section sur les shaders personnalisés

Je vois des artefacts de lignes apparaître sur certains hardware.

  • Voir le paramétrage du projet uv_contract qui peut être utilisé pour résoudre ce problème.

J'utilise un grand nombre de textures, donc peu d'éléments sont traité par lots.

  • Envisagez l'utilisation d'atlas de textures. En plus de permettre le traitement par lots, ceux-ci réduisent le besoin de changements d'état associés à un changement de texture.

Annexe

Primitives par lot

Toutes les primitives ne peuvent pas être mises en lots. La mise en lot n'est pas non plus garantie, en particulier avec les primitives utilisant une bordure avec antialiasing. Les types de primitives suivants sont actuellement disponibles :

  • RECT

  • NINEPATCH (selon le mode de wrapping)

  • POLY

  • LINE

Avec les primitives non mises en lot, vous pouvez obtenir de meilleures performances en les dessinant manuellement avec des polys dans une fonction _draw(). Voir Dessin personnalisé en 2D pour plus d'informations.

Calcul du seuil de light scissoring

La proportion réelle de la zone de pixels de l'écran utilisée comme seuil est la valeur scissor_area_threshold à la puissance 4.

Par exemple, sur un écran de 1920 x 1080 il y a 2,073,600 pixels.

A un seuil de 1,000 pixels, la proportion serait de :

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

Ainsi, un scissor_area_threshold de 0,15 serait une valeur raisonnable à essayer.

En allant dans l'autre sens, par exemple avec un scissor_area_threshold de 0.5 :

0.5 ^ 4 = 0.0625
0.0625 * 2073600 = 129600 pixels

Si le nombre de pixels sauvegardés est supérieur à ce seuil, le scissoring est activé.