Framework de synchronisation

Le framework de synchronisation décrit explicitement les dépendances entre différentes opérations asynchrones dans le système graphique Android. Il fournit une API qui permet aux composants d'indiquer quand les tampons sont libérés. Il permet également de transmettre des primitives de synchronisation entre les pilotes du noyau et l'espace utilisateur, ainsi qu'entre les processus de l'espace utilisateur eux-mêmes.

Par exemple, une application peut mettre en file d'attente le travail à effectuer dans le GPU. Le GPU commence à dessiner cette image. Bien que l'image n'ait pas encore été dessinée dans la mémoire, le pointeur de tampon est transmis au compositeur de fenêtres avec une barrière qui indique quand le travail du GPU sera terminé. Le compositeur de fenêtres commence le traitement à l'avance et transmet le travail au contrôleur d'affichage. De même, le travail du processeur est effectué à l'avance. Une fois le GPU terminé, le contrôleur d'affichage affiche immédiatement l'image.

Le framework de synchronisation permet également aux implémenteurs d'utiliser des ressources de synchronisation dans leurs propres composants matériels. Enfin, il offre une visibilité sur le pipeline graphique pour faciliter le débogage.

Synchronisation explicite

La synchronisation explicite permet aux producteurs et aux consommateurs de tampons graphiques de signaler quand ils ont fini d'utiliser un tampon. Elle est implémentée dans l'espace du noyau.

La synchronisation explicite offre, entre autres, les avantages suivants :

  • Moins de variations de comportement entre les appareils
  • Meilleure prise en charge du débogage
  • Amélioration des métriques de test

Le framework de synchronisation comporte trois types d'objets :

  • sync_timeline
  • sync_pt
  • sync_fence

sync_timeline

sync_timeline est une chronologie croissante de manière monotone que les fournisseurs doivent implémenter pour chaque instance de pilote, comme un contexte GL, un contrôleur d'affichage ou un blitter 2D. sync_timeline compte les tâches envoyées au noyau pour un matériel particulier. sync_timeline garantit l'ordre des opérations et permet des implémentations spécifiques au matériel.

Suivez ces consignes lors de l'implémentation de sync_timeline :

  • Fournissez des noms utiles pour tous les pilotes, chronologies et barrières afin de simplifier le débogage.
  • Implémentez les opérateurs timeline_value_str et pt_value_str dans les chronologies pour rendre la sortie de débogage plus lisible.
  • Implémentez le remplissage driver_data pour permettre aux bibliothèques de l'espace utilisateur, telles que la bibliothèque GL, d'accéder aux données de chronologie privées, si nécessaire. data_driver permet aux fournisseurs de transmettre des informations sur les éléments immuables sync_fence et sync_pts pour créer des lignes de commande basées sur eux.
  • N'autorisez pas l'espace utilisateur à créer ou à signaler explicitement une barrière. La création explicite de signaux/barrières entraîne une attaque par déni de service qui interrompt la fonctionnalité du pipeline.
  • N'accédez pas explicitement aux éléments sync_timeline, sync_pt, ou sync_fence. L'API fournit toutes les fonctions requises.

sync_pt

sync_pt est une valeur ou un point unique sur une sync_timeline. Un point comporte trois états : actif, signalé et erreur. Les points commencent à l'état actif et passent à l'état signalé ou erreur. Par exemple, lorsqu'un consommateur d'image n'a plus besoin d'un tampon, un sync_pt est signalé afin qu'un producteur d'image sache qu'il peut à nouveau écrire dans le tampon.

sync_fence

sync_fence est une collection de valeurs sync_pt qui ont souvent des parents sync_timeline différents (par exemple, pour le contrôleur d'affichage et le GPU). sync_fence, sync_pt et sync_timeline sont les principales primitives que les pilotes et l'espace utilisateur utilisent pour communiquer leurs dépendances. Lorsqu'une barrière est signalée, toutes les commandes émises avant la barrière sont terminées, car le pilote du noyau ou le bloc matériel exécute les commandes dans l'ordre.

Le framework de synchronisation permet à plusieurs consommateurs ou producteurs de signaler quand ils ont fini d'utiliser un tampon, en communiquant les informations de dépendance avec un paramètre de fonction. Les barrières sont soutenues par un descripteur de fichier et sont transmises de l'espace du noyau à l'espace utilisateur. Par exemple, une barrière peut contenir deux valeurs sync_pt qui indiquent quand deux consommateurs d'images distincts ont terminé de lire un tampon. Lorsque la barrière est signalée, les producteurs d'images savent que les deux consommateurs ont terminé leur consommation.

Les barrières, comme les valeurs sync_pt, commencent à l'état actif et changent d'état en fonction de l'état de leurs points. Si toutes les valeurs sync_pt sont signalées, le sync_fence est signalé. Si un sync_pt passe à l'état d'erreur, l'ensemble du sync_fence est en état d'erreur.

L'appartenance à un sync_fence est immuable une fois la barrière créée. Pour obtenir plusieurs points dans une barrière, une fusion est effectuée, où les points de deux barrières distinctes sont ajoutés à une troisième barrière. Si l'un de ces points a été signalé dans la barrière d'origine et l'autre non, la troisième barrière ne sera pas non plus à l'état signalé.

Pour implémenter la synchronisation explicite, fournissez les éléments suivants :

  • Un sous-système d'espace du noyau qui implémente le framework de synchronisation pour un pilote matériel particulier. Les pilotes qui doivent être compatibles avec les barrières sont généralement tout ce qui accède au compositeur matériel (HWC) ou communique avec lui. Les fichiers clés incluent les éléments suivants :
    • Implémentation principale :
      • kernel/common/include/linux/sync.h
      • kernel/common/drivers/base/sync.c
    • Documentation à l'adresse kernel/common/Documentation/sync.txt
    • Bibliothèque pour communiquer avec l'espace du noyau dans platform/system/core/libsync
  • Le fournisseur doit fournir les barrières de synchronisation appropriées en tant que paramètres aux fonctions validateDisplay() et presentDisplay() dans la couche d'abstraction matérielle (HAL).
  • Deux extensions GL liées aux barrières (EGL_ANDROID_native_fence_sync et EGL_ANDROID_wait_sync) et prise en charge des barrières dans le pilote graphique.

Étude de cas : implémenter un pilote d'affichage

Pour utiliser l'API compatible avec la fonction de synchronisation, développez un pilote d'affichage qui comporte une fonction de tampon d'affichage. Avant l'existence du framework de synchronisation, cette fonction recevait des objets dma-buf, plaçait ces tampons sur l'écran et bloquait l'affichage pendant que le tampon était visible. Exemple :

/*
 * assumes buffer is ready to be displayed.  returns when buffer is no longer on
 * screen.
 */
void display_buffer(struct dma_buf *buffer);

Avec le framework de synchronisation, la fonction display_buffer est plus complexe. Lorsqu'un tampon est affiché, il est associé à une barrière qui indique quand il sera prêt. Vous pouvez mettre en file d'attente et lancer le travail une fois la barrière effacée.

La mise en file d'attente et le lancement du travail une fois la barrière effacée ne bloquent rien. Vous renvoyez immédiatement votre propre barrière, qui signale quand le tampon sera retiré de l'écran. Lorsque vous mettez en file d'attente des tampons, le noyau répertorie les dépendances avec le framework de synchronisation :

/*
 * displays buffer when fence is signaled.  returns immediately with a fence
 * that signals when buffer is no longer displayed.
 */
struct sync_fence* display_buffer(struct dma_buf *buffer, struct sync_fence
*fence);

Intégration de la synchronisation

Cette section explique comment intégrer le framework de synchronisation de l'espace du noyau aux parties de l'espace utilisateur du framework Android et aux pilotes qui doivent communiquer entre eux. Les objets de l'espace du noyau sont représentés sous forme de descripteurs de fichiers dans l'espace utilisateur.

Conventions d'intégration

Suivez les conventions d'interface HAL Android :

  • Si l'API fournit un descripteur de fichier qui fait référence à un sync_pt, le pilote du fournisseur ou la HAL utilisant l'API doit fermer le descripteur de fichier.
  • Si le pilote du fournisseur ou la HAL transmet un descripteur de fichier contenant un sync_pt à une fonction d'API, le pilote du fournisseur ou la HAL ne doit pas fermer le descripteur de fichier.
  • Pour continuer à utiliser le descripteur de fichier de barrière, le pilote du fournisseur ou la HAL doit dupliquer le descripteur.

Un objet de barrière est renommé chaque fois qu'il passe par BufferQueue. La prise en charge des barrières du noyau permet aux barrières d'avoir des chaînes pour les noms. Le framework de synchronisation utilise donc le nom de la fenêtre et l'index du tampon mis en file d'attente pour nommer la barrière, par exemple SurfaceView:0. Cela est utile pour le débogage afin d'identifier la source d'un interblocage, car les noms apparaissent dans le résultat de /d/sync et les rapports de bug.

Intégration d'ANativeWindow

ANativeWindow est compatible avec les barrières. dequeueBuffer, queueBuffer et cancelBuffer comportent des paramètres de barrière.

Intégration d'OpenGL ES

L'intégration de la synchronisation d'OpenGL ES repose sur deux extensions EGL :

  • EGL_ANDROID_native_fence_sync permet d'encapsuler ou de créer des descripteurs de fichiers de barrière Android natifs dans EGLSyncKHR objets.
  • EGL_ANDROID_wait_sync autorise les blocages côté GPU plutôt que côté processeur, ce qui permet au GPU d'attendre EGLSyncKHR. L' EGL_ANDROID_wait_sync extension est identique à l' EGL_KHR_wait_sync extension.

Pour utiliser ces extensions indépendamment, implémentez l'extension EGL_ANDROID_native_fence_sync avec la prise en charge du noyau associée. Ensuite, activez l'extension EGL_ANDROID_wait_sync dans votre pilote. L'extension EGL_ANDROID_native_fence_sync se compose d'un type d'objet de barrière natif distinct EGLSyncKHR. Par conséquent, les extensions qui s'appliquent aux types d'objets EGLSyncKHR existants ne s'appliquent pas nécessairement aux objets EGL_ANDROID_native_fence, ce qui évite les interactions indésirables.

L'extension EGL_ANDROID_native_fence_sync utilise un attribut de descripteur de fichier de barrière natif correspondant qui ne peut être défini qu'au moment de la création et ne peut pas être interrogé directement à partir d'un objet de synchronisation existant. Cet attribut peut être défini sur l'un des deux modes suivants :

  • Un descripteur de fichier de barrière valide encapsule un descripteur de fichier de barrière Android natif existant dans un objet EGLSyncKHR.
  • -1 crée un descripteur de fichier de barrière Android natif à partir d'un EGLSyncKHR objet.

Utilisez l'appel de fonction DupNativeFenceFD() pour extraire l'objet EGLSyncKHR du descripteur de fichier de barrière Android natif. Cela a le même résultat que l'interrogation de l'attribut défini, mais respecte la convention selon laquelle le destinataire ferme la barrière (d'où l'opération de duplication). Enfin, la destruction de l'objet EGLSyncKHR ferme l'attribut de barrière interne.

Intégration du compositeur matériel

Le HWC gère trois types de barrières de synchronisation :

  • Les barrières d'acquisition sont transmises avec les tampons d'entrée aux appels setLayerBuffer et setClientTarget. Elles représentent une écriture en attente dans le tampon et doivent signaler avant que le SurfaceFlinger ou le HWC tente de lire à partir du tampon associé pour effectuer la composition.
  • Les barrières de libération sont récupérées après l'appel à presentDisplay à l'aide de l'appel getReleaseFences. Elles représentent une lecture en attente à partir du tampon précédent sur la même couche. Une barrière de libération signale quand le HWC n'utilise plus le tampon précédent car le tampon actuel a remplacé le tampon précédent sur l'écran. Les barrières de libération sont renvoyées à l'application avec les tampons précédents qui seront remplacés lors de la composition actuelle. L'application doit attendre qu'une barrière de libération signale avant d'écrire de nouveaux contenus dans le tampon qui lui a été renvoyé.
  • Les barrières de présentation sont renvoyées, une par frame, dans le cadre de l'appel à presentDisplay. Les barrières de présentation représentent le moment où la composition de cette frame est terminée ou, à l'inverse, le moment où le résultat de la composition de la frame précédente n'est plus nécessaire. Pour les écrans physiques, presentDisplay renvoie des barrières de présentation lorsque la frame actuelle s'affiche à l'écran. Une fois les barrières de présentation renvoyées, il est possible d'écrire à nouveau dans le tampon cible SurfaceFlinger, le cas échéant. Pour les écrans virtuels, les barrières de présentation sont renvoyées lorsqu'il est possible de lire à partir du tampon de sortie.