Optimisation du rendu pour mobile

Bonjour chers lecteurs, amateurs et professionnels de la programmation graphique! Nous attirons votre attention sur une série d'articles consacrés à l'optimisation du rendu pour les appareils mobiles: téléphones et tablettes basés sur iOS et Android. Le cycle comprendra trois parties. Dans la première partie, nous examinerons les fonctionnalités de l' architecture populaire de tuiles GPU sur mobile . Dans le second, nous passerons en revue les principales familles de GPU présentés dans les appareils modernes, et examinerons leurs forces et leurs faiblesses. Dans la troisième partie, nous nous familiariserons avec les fonctionnalités d'optimisation du shader.

Passons donc à la première partie.

Le développement de cartes vidéo sur les ordinateurs de bureau et les consoles a eu lieu en l'absence de restrictions importantes sur la consommation d'énergie. Avec l'avènement des cartes vidéo pour appareils mobiles, les ingénieurs ont été confrontés à la tâche d'assurer des performances acceptables à des résolutions de bureau comparables, tandis que la consommation d'énergie de ces cartes vidéo devrait être de 2 ordres de grandeur inférieure. 



La solution a été trouvée dans une architecture spéciale appelée Tile Based Rendering (TBR) . Pour un programmeur graphique ayant une expérience dans le développement de PC, lorsqu'il se familiarise avec le développement mobile, tout semble familier: une API OpenGL ES similaire est utilisée, la même structure du pipeline graphique. Cependant, l'architecture des tuiles des GPU mobiles est considérablement différente de celle utilisée sur les consoles PC / Mode immédiat . Connaître les forces et les faiblesses de TBR vous aidera à prendre les bonnes décisions et à obtenir d'excellentes performances avec Mobile.

Vous trouverez ci-dessous un schéma simplifié d'un pipeline graphique classique utilisé sur les PC et les consoles pour la troisième décennie.


Au stade du traitement de la géométrie, les attributs de sommet sont lus dans la mémoire vidéo du GPU. Après diverses transformations (Vertex Shader), les primitives prêtes à être rendues dans l'ordre d'origine (FIFO) sont transmises au rasterizer, qui divise les primitives en pixels. Après cela, l'étape de traitement des fragments de chaque pixel (Fragment Shader) est effectuée , et les valeurs de couleur obtenues sont écrites dans le tampon d'écran, qui est également situé dans la mémoire vidéo. Une caractéristique de l'architecture traditionnelle du «Mode Immédiat» est l'enregistrement du résultat du Fragment Shader dans des sections arbitraires du tampon d'écran lors du traitement d'un seul appel de tirage. Ainsi, pour chaque appel de tirage, l'accès à la totalité de la mémoire tampon d'écran peut être requis. Travailler avec un large éventail de mémoire nécessite une bande passante de bus appropriée ( bande passante ) et est associé à une consommation d'énergie élevée. Par conséquent, les GPU mobiles ont commencé à adopter une approche différente. Sur l'architecture de tuile typique des cartes vidéo mobiles, le rendu se fait dans un petit morceau de mémoire correspondant à la partie de l'écran - la tuile. La petite taille de la tuile (par exemple 16x16 pixels pour les cartes vidéo Mali, 32x32 pour PowerVR) vous permet de la placer directement sur la puce de la carte vidéo, ce qui rend la vitesse d'accès comparable à la vitesse d'accès aux registres de base du shader, c'est-à-dire très vite.


Cependant, comme les primitives peuvent tomber dans des sections arbitraires du tampon d'écran et qu'une tuile n'en recouvre qu'une petite partie, une étape supplémentaire dans le pipeline graphique était nécessaire. Voici un diagramme simplifié du fonctionnement du pipeline avec l'architecture de tuiles.


Après avoir traité les sommets et construit les primitives, ces dernières, au lieu d'être envoyées au pipeline de fragments, tombent dans le soi-disant Tiler . Ici, les primitives sont distribuées par des tuiles, dans les pixels dont elles tombent. Après une telle distribution, qui, en règle générale, couvre tous les appels de tirage dirigés vers un objet tampon de trame (aka cible de rendu ), les tuiles sont rendues séquentiellement. Pour chaque tuile, la séquence d'actions suivante est effectuée:

  1. Chargement d'anciens contenus FBO à partir de la mémoire système ( Charger
  2. Rendu des primitifs tombant dans cette tuile
  3. Téléchargement de nouveau contenu FBO dans la mémoire système ( Store )


Il convient de noter que l' opération de chargement peut être considérée comme une superposition supplémentaire de la «texture plein écran» sans compression. Si possible, évitez cette opération, c'est-à-dire Ne laissez pas FBO basculer d' avant en arrière. Si tout son contenu est effacé avant le rendu en FBO , l' opération de chargement n'est pas effectuée. Cependant, afin d'envoyer le bon signal au conducteur, les paramètres d'un tel nettoyage doivent répondre à certains critères:

  1. Doit être désactivé Scissor Rect
  2. L'enregistrement dans tous les canaux de couleur et alpha doit être autorisé.

Pour empêcher l' opération de chargement du tampon de profondeur et du gabarit, ils doivent également être nettoyés avant le début du rendu.

Il est également possible d'éviter le magasin opération pour le tampon de profondeur / pochoir. Après tout, le contenu de ces tampons ne s'affiche pas à l'écran. Avant l'opération glSwapBuffers , vous pouvez appeler glDiscardFramebufferEXT ou glInvalidateFramebuffer

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glDiscardFramebufferEXT (GL_FRAMEBUFFER, 2, attachments);

const GLenum attachments[] = {GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT};
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, attachments);

Il existe des scénarios de rendu dans lesquels le placement des tampons de profondeur / gabarit, ainsi que des tampons MSAA dans la mémoire système n'est pas requis. Par exemple, si le rendu dans la FBO avec le tampon de profondeur est continu et que les informations de profondeur de la trame précédente ne sont pas utilisées, le tampon de profondeur n'a pas besoin d'être chargé dans la mémoire de tuiles avant le début du rendu, ni déchargé après la fin du rendu. Par conséquent, la mémoire système ne peut pas être allouée sous le tampon de profondeur. Les API graphiques modernes telles que Vulkan et Metal vous permettent de définir explicitement le mode de mémoire pour vos homologues FBO  ( MTLStorageModeMemoryless in Metal, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT + VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT dans Vulkan ).

Il convient de noter en particulier l'implémentation de MSAA sur les architectures de tuiles. Le tampon haute résolution pour le MSAA ne laisse pas la mémoire des tuiles en divisant le FBO en plusieurs tuiles. Par exemple, pour MSAA 2x2, 16x16 tuiles seront résolus en 8x8 pendant la magasin opération, à savoir Au total, il faudra traiter 4 fois plus de tuiles. Mais la mémoire supplémentaire pour MSAA n'est pas requise, et en raison du rendu dans la mémoire rapide des tuiles, il n'y aura pas de restrictions de bande passante importantes . Cependant, utilisezMSAA sur l'architecture de tuiles augmente la charge sur Tiler, ce qui peut affecter négativement les performances de rendu des scènes avec beaucoup de géométrie.

En résumant ce qui précède, nous présentons le schéma souhaité de travail avec FBO sur l'architecture de tuile:

// 1.   ,    auxFBO
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
glDisable(GL_SCISSOR);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glDepthMask(GL_TRUE);
// glClear,     
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | 
           GL_STENCIL_BUFFER_BIT);

renderAuxFBO();         

//   /      
glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);
// 2.   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
glDisable(GL_SCISSOR);

glClear(...);
//   mainFBO    auxFBO
renderMainFBO(auxFBO);

glInvalidateFramebuffer(GL_FRAMEBUFFER, 2, depth_and_stencil);

Si vous passez au rendu auxFBO au milieu de la formation mainFBO , vous pouvez obtenir des opérations de chargement et de stockage inutiles , ce qui peut augmenter considérablement le temps de formation de la trame. Dans notre pratique, nous avons rencontré un ralentissement du rendu même dans le cas de paramètres FBO inactifs, c'est-à-dire sans le rendu réel en eux. En raison de l'architecture du moteur, notre ancien circuit ressemblait à ceci:

//   mainFBO
glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   
glBindFramebuffer(GL_FRAMEBUFFER, auxFBO);
//  auxFBO
renderAuxFBO();

glBindFramebuffer(GL_FRAMEBUFFER, mainFBO);
//   mainFBO
renderMainFBO(auxFBO);

Malgré le manque d'appels gl après la première installation de mainFBO , sur certains appareils, nous avons obtenu des opérations de chargement et de stockage supplémentaires et de mauvaises performances.

Pour améliorer notre compréhension des frais généraux liés à l'utilisation des FBO intermédiaires , nous avons mesuré la perte de temps pour la commutation des FBO en plein écran à l' aide d'un test synthétique. Le tableau indique le temps passé sur le magasin fonctionnement lors de la commutation FBO plusieurs fois dans une trame (le temps d'une telle opération est donnée). Fonctionnement de la charge absent en raison de glClear, c'est à dire. un scénario plus favorable a été mesuré. L'autorisation utilisée sur l'appareil a contribué. Cela pourrait plus ou moins correspondre à la puissance du GPU installé. Par conséquent, ces chiffres ne donnent qu'une idée générale du coût de la commutation des cibles sur les cartes vidéo mobiles de différentes générations.
GPUmillisecondesGPUmillisecondes
Adreno 3205.2
Adreno 5120,74
PowerVR G62003.3Adreno 6150,7
Mali-4003.2Adreno 5300,4
Mali-t7201,9Mali-g510,32
PowerVR SXG 5441.4Mali-t830
0,15

Sur la base des données obtenues, nous pouvons recommander de ne pas utiliser plus d'un ou deux commutateurs FBO par trame, au moins pour les anciennes cartes vidéo. Si le jeu a une passe de code distincte pour les appareils bas de gamme, il est conseillé de ne pas y utiliser la modification FBO. Cependant, sur le bas de gamme, la question de l'abaissement de la résolution devient souvent pertinente. Sur Android, vous pouvez réduire la résolution de rendu sans recourir à un FBO intermédiaire en appelant SurfaceHolder.setFixedSize ():

surfaceView.getHolder().setFixedSize(...)

Cette méthode ne fonctionnera pas si le jeu est rendu via l' application Surface principale (un schéma typique pour travailler avec NativeActivity ). Si vous utilisez la surface principale , une résolution inférieure peut être définie en appelant la fonction native ANativeWindow_setBuffersGeometry.

JNIEXPORT void JNICALL Java_com_organization_app_AppNativeActivity_setBufferGeometry(JNIEnv *env, jobject thiz, jobject surface, jint width, jint height)
{
ANativeWindow* window = ANativeWindow_fromSurface(env, surface); 
ANativeWindow_setBuffersGeometry(window, width, height, AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM); 
}

En Java:

private static native void setBufferGeometry(Surface surface, int width , int height )
...
//   SurfaceHolder.Callback
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
     setBufferGeometry(holder.getSurface(), 768, 1366); /* ... */
...

Enfin, nous mentionnons la commande ADB pratique pour contrôler les tampons de surface sélectionnés sur Android:

adb shell dumpsys surfaceflinger

Vous pouvez obtenir une conclusion similaire qui vous permet d'estimer la consommation de mémoire pour les tampons de surface:


La capture d'écran montre le système mettant en évidence 3 tampons pour une triple mise en mémoire tampon du jeu GLSurfaceView (surligné en jaune), ainsi que 2 tampons pour la surface principale (surlignée en rouge). Dans le cas du rendu via la surface principale, qui est le schéma par défaut lors de l'utilisation de NativeActivity , l'allocation de tampons supplémentaires peut être évitée. 

C'est tout pour le moment. Dans les articles suivants, nous classerons les GPU mobiles et analyserons les méthodes d'optimisation pour eux.

All Articles