Pipeline graphique HAR

Cette page décrit le pipeline graphique complet du moteur de rendu à haute disponibilité (HAR, High Availability Renderer), en traçant le flux de données d'un document de conception Figma jusqu'aux pixels finaux affichés à l'écran.

Présentation

Le pipeline convertit les définitions d'UI de haut niveau en commandes graphiques de bas niveau et les présente efficacement sur les écrans matériels. Il est conçu pour les applications automobiles critiques pour la sécurité, en mettant l'accent sur le rendu déterministe, la gestion efficace des états et l'interaction robuste avec les sous-systèmes graphiques de la plate-forme, tels que Direct Rendering Manager (DRM) et Generic Buffer Management (GBM).

Le pipeline peut être divisé en quatre phases principales :

  1. Prerender : traitement du graphique de scène, application des personnalisations et résolution de la mise en page.
  2. Génération de commandes : conversion du graphique de scène résolu en une liste d'affichage indépendante du backend.
  3. Rendu : exécution des commandes de dessin à l'aide du moteur graphique Impeller.
  4. Présentation : gestion des tampons de trame et synchronisation avec le matériel d'affichage.

Flux des graphiques HAR

Figure 1. Flux graphique HAR.

Phase 1: Prerender

Cette phase transforme la conception Figma statique et l'état dynamique de l'application en une arborescence d'UI entièrement résolue en mémoire, prête pour le rendu. Elle s'exécute sur un thread de réduction dédié, distinct de la boucle d'affichage principale.

1.1 Base de DesignCompose

Le pipeline HAR est basé sur l'écosystème DesignCompose.

  • Source : l'UI est conçue dans Figma et exportée à l'aide du plug-in DesignCompose.
  • Définition : la sortie est une instance de DesignComposeDefinition, une représentation sérialisée de la conception (nœuds, styles, variantes).
  • Liaison de données : le modèle d'UI de l'application utilise des macros procédurales (par exemple, #[Design(node = "#speed")]) pour lier explicitement les champs de structure Rust à des nœuds nommés spécifiques dans le document Figma. Cela permet à l'état de l'application de contrôler automatiquement les propriétés des éléments visuels.

Les principaux composants de cette base sont les suivants :

  • Réducteur : agit comme la boucle d'événements centrale, en traitant les actions et en mettant à jour l'état actuel. Le framework fournit DefaultReducer, mais une implémentation de réducteur personnalisée peut être fournie si nécessaire.
  • Présentateur : relie l'état actuel au modèle d'UI. Le Presenter trait est spécifié par le harry crate de framework, et une implémentation de référence (UIModelPresenter) est fournie dans le harry-app-core crate.
  • Modèle d'UI : génère des personnalisations en fonction de l'état actuel. Le code du modèle d'UI est généré à l'aide de la macro DesignDocument fournie par le crate derive_customizations. La structure UIModel du crate harry-app-core en fournit un exemple.
  • Squoosh : fournit la structure de données SquooshView et le dépôt de variantes, utilisés pour afficher l'UI en fonction de la conception. Un document de conception sérialisé est chargé par le crate dc_bundle à partir de la bibliothèque DesignCompose et converti en une arborescence de structures SquooshView pour des performances d'exécution efficaces.

1.2 Boucle de réduction

Le pipeline est piloté par des actions. Le framework spécifie le type énuméré Actions, qui définit les actions internes utilisées par le framework lui-même, mais inclut également une variante CustomAction qui permet aux utilisateurs de définir des actions supplémentaires spécifiques à l'application (par exemple, UpdateVehicleSpeed ou ButtonPress).

Le framework fournit également le trait StateAction, qui simplifie l'implémentation des actions qui affectent l'état de l'application et génèrent éventuellement des effets secondaires qui sont ensuite renvoyés à l'application à partir du réducteur pour traitement. L'énumération CustomActions du crate harry-app-core en fournit un exemple détaillé.

Voici un aperçu de base de la boucle de réduction :

  • Traitement des actions : Reducer reçoit une action et met à jour l'état actuel. Il s'agit des données brutes telles que la vitesse actuelle ou les témoins (voyants d'avertissement) actifs. Cela peut également générer des effets secondaires (par exemple, un signal sonore lorsque le voyant de la ceinture de sécurité clignote).
  • Présentation : Presenter mappe le nouvel état dans UIModel. UIModel est un modèle de vue qui contient des données spécifiquement mises en forme pour l'UI (par exemple, la mise en forme de la vitesse "120" en une chaîne "65 mph").
  • Génération de personnalisations : la méthode apply du modèle d'UI est appelée pour générer un ensemble d'instances RenderCustomization. Il s'agit d'instructions explicites pour modifier la conception Figma (par exemple, "Définir le texte du nœud #speed sur '65 mph'").
  • UpdatePolicy pour l'optimisation : après chaque passe de prerender, une valeur UpdatePolicy est renvoyée, indiquant quand la prochaine mise à jour du rendu est requise. Si aucune modification d'état n'est en attente et qu'aucune animation n'est en cours d'exécution, UpdatePolicy signale qu'aucune autre mise à jour n'est nécessaire immédiatement. Dans ce cas, le réducteur cesse de générer de nouvelles listes d'affichage, ce qui évite les cycles de rendu inutiles et préserve les ressources jusqu'à ce qu'une nouvelle action ou un nouvel événement déclenche une modification.

1.3 Ingestion de vues et initialisation du dépôt

Le pipeline commence par une instance DesignComposeDefinition. Il s'agit du document de conception Figma sérialisé par DesignCompose dans une structure de tampon de protocole.

  • Chargement initial : au démarrage, la conception principale (spécifiée par son nœud racine) est convertie de DesignComposeDefinition en une arborescence SquooshView initiale. Il s'agit d'un processus unique.

  • Dépôt : SquooshVariantRepository gère les variantes de composants réutilisables et les vues chargées initialement.

  • Chargement différé : pour minimiser le temps de démarrage et l'utilisation de la mémoire, les vues supplémentaires (celles qui ne font pas partie de l'arborescence du nœud racine initial) ne sont chargées de manière différée à partir du document que lorsqu'elles sont explicitement référencées et nécessaires à la logique de rendu (par exemple, lors d'une personnalisation de liste).

1.4 Passe de personnalisation

L'arborescence SquooshView est parcourue pour appliquer l'état dynamique de l'application :

  • Échanges de variantes : les instances de composants sont échangées avec des variantes spécifiques (par exemple, en passant d'une icône représentant le mode de conduite actuel de sport à éco) en fonction de la logique d'exécution.

  • Expansion de la liste : un seul élément de modèle dans Figma est remplacé par une liste dynamique d'enfants. De nouveaux ID uniques sont générés pour ces enfants afin de vérifier une identité stable pour les animations.

  • Remplacements de texte et de style : le contenu textuel (par exemple, la valeur de la vitesse) et les styles (par exemple, l'opacité, la couleur) sont mis à jour à partir de l'état actuel.

1.5 Résolution variable

Les jetons de conception et les variables définis dans Figma ou localement dans l'application sont résolus.

  • Liaison : les propriétés SquooshView qui font référence à des variables (comme les couleurs ou les dimensions) sont remplacées par leurs valeurs concrètes pour la trame actuelle.

1.6 Calcul de la mise en page

  • Mise en page dynamique : DynamicLayout calcule la position et la taille finales (limites) de chaque nœud de l'arborescence SquooshView.

  • Mise en page du texte : TextHelper utilise une implémentation du trait LayoutHelper pour calculer les métriques de texte, l'habillage et la mise en forme. Cela permet de vérifier que le texte s'affiche correctement dans ses contraintes avant le rendu.

1.7 Cadrans et jauges

Il s'agit d'une étape spécialisée pour les UI automobiles.

  • MeterData: si un nœud comporte des données de compteur (définies dans Figma), sa géométrie est modifiée dynamiquement en fonction de meter_value (par exemple, la vitesse du véhicule).
    • Arcs : l'angle de balayage est ajusté.
    • Rotations : la transformation de rotation est calculée en fonction des angles de début et de fin.
    • Barres de progression : la largeur ou la hauteur d'un rectangle est mise à l'échelle.
    • Vecteurs de progression : la longueur d'un chemin vectoriel est ajustée.

1.8 Animation

  • Diffing : le SquooshView actuel est comparé à previous_squoosh_view de PreRenderCache.

  • Interpolation : si des propriétés ont changé, Squoosh crée des interpolateurs pour effectuer une transition fluide des valeurs (par exemple, l'opacité ou la transformation) au fil du temps.

Phase 2 : Génération de commandes

Une fois l'arborescence SquooshView entièrement résolue et animée, elle est convertie en une séquence linéaire de commandes de dessin.

Le composant clé de cette phase est le crate DisplayList :

  • generate_dl : cette fonction parcourt de manière récursive l'arborescence SquooshView.

  • Traduction :

    • Formes et chemins : convertis en DisplayListEntry avec la variante DisplayListAppearance appropriée (par exemple, Rect ou Path)
    • Texte : converti avec TextHelper en entrées de dessin de texte.
    • Transformations et clips : convertis en paires PushTransform3D et PopTransform3D ou PushClipRegion et PopClipRegion pour gérer la pile d'état de dessin.
    • Masquage : converti en paires PushMaskLayer et PopMaskLayer pour créer et fusionner correctement les calques.

Le résultat final est une instance de Vec<DisplayListEntry> qui décrit ce qu'il faut dessiner, indépendamment de la manière de le dessiner.

2.1 Transfert au looper

Une fois la DisplayList générée, le réducteur l'encapsule dans une instance de ViewDescriptor et l'envoie via un canal MPSC Rust (LooperMessage) au thread du looper. Le Looper est responsable des phases de rendu et d'affichage, ce qui empêche le thread du réducteur de bloquer le pipeline graphique.

Phase 3 : Rendu

La DisplayList indépendante de la plate-forme est transmise au backend de rendu, où les commandes abstraites sont traduites en instructions GPU.

HAR utilise Impeller, un moteur de rendu initialement conçu pour Flutter. Impeller est conçu pour résoudre le problème des problèmes de fréquence d'images dus à la compilation de shaders en précompilant un petit ensemble efficace de shaders au moment de la compilation. Cette approche, combinée à un traitement par lot efficace et à un backend hautement optimisé, offre les avantages suivants :

  • Performances déterministes : élimine pratiquement les problèmes de compilation de nuanceurs au moment de l'exécution.
  • Démarrage rapide : réduit la surcharge d'initialisation.
  • Faible encombrement : produit une taille binaire compacte.

Pour une présentation complète de l'architecture d'Impeller, regardez [Présentation d'Impeller, le nouveau moteur de rendu de Flutter][impeller-video]. Bien que la vidéo traite de Flutter, ces avantages fondamentaux renforcent directement la pile automobile HAR.

Les principaux composants de la phase de rendu sont les suivants :

  • ImpellerRenderer: convertit la liste d'affichage de la phase de prerender en commandes de rendu Impeller.

  • API Impeller Rust : encapsule la bibliothèque Impeller pour une utilisation dans Rust (crates impeller et impeller-rs-bindgen).

  • TypographyContext : gère l'enregistrement des polices et la mise en forme du texte.

impeller-video

3.1 Initialisation et gestion des surfaces

  • Création de contexte : le moteur de rendu initialise une instance de impeller::Context avec un backend OpenGL ES, en transmettant un rappel pour résoudre les pointeurs de fonction OpenGL ES à partir du contexte GL de la plate-forme.

  • Surface FBO encapsulée : au lieu de créer sa propre fenêtre, Impeller effectue le rendu dans un objet de tampon de trame OpenGL (FBO) existant fourni par la phase 4. Pour ce faire, appelez Surface::create_wrapped_fbo.

3.2 Gestion des ressources

  • Images : prend en charge les formats standards et les textures compressées KTX2. Elles sont importées dans des textures GPU et gérées par une structure Resources interne.

  • Polices : les polices TrueType et OpenType sont chargées et enregistrées auprès de TypographyContext pour le rendu du texte.

  • Images externes : la gestion spécialisée des textures externes (par exemple, les flux de caméras et les moteurs de rendu 3D externes) implique la liaison d'instances EGLImage ou de textures OpenGL externes à des objets Texture Impeller pour un rendu sans copie.

3.3 Passe de rendu

La boucle render construit une instance DisplayList Impeller (à ne pas confondre avec le Vec<DisplayListEntry> généré par la phase de prerender) à l'aide de DisplayListBuilder :

  1. Efface le tampon et applique des transformations globales pour la mise à l'échelle DPI et la rotation de l'écran.

  2. Parcourt les éléments DisplayListEntry d'entrée :

    • État : save() et restore() sont utilisés pour envoyer et extraire des transformations et des régions de clip.
    • Primitifs : Rect et RoundedRect sont dessinés à l'aide d'opérations de peinture standards.
    • Chemins : les chemins vectoriels complexes (y compris les instances Arc dynamiques) sont créés et dessinés.
    • Texte : Text et StyledText sont affichés à l'aide de TypographyContext.
    • Images : les images standards et externes sont dessinées à l'aide de draw_texture_rect.
  3. Envoie la liste d'affichage Impeller créée à la surface à l'aide de surface.draw_display_list(), en générant les commandes GL sous-jacentes.

  4. Appelle swap_buffers() sur le contexte sous-jacent pour déclencher la phase 4.

Phase 4 : Présentation

Cette phase finale gère l'interaction avec le matériel d'affichage pour afficher la trame rendue. HAR utilise un chemin de rendu direct robuste sur le véhicule défini par logiciel (SDV, Software-Defined Vehicle) Android Automotive OS (AAOS).

Le composant clé de cette phase est HarDirectRenderingContext (dans le crate har-gl-context).

4.1 Architecture

La couche de présentation utilise une approche à double tampon avec une cible de dessin hors écran :

  1. Tampon de dessin : FBO hors écran où Impeller affiche la scène.

  2. Tampon de résolution (facultatif) : tampon auxiliaire facultatif pour prendre en charge l'anticrénelage multisampling (MSAA)

    • Il peut être activé si nécessaire par l'implémentation ou la configuration OpenGL ES sous-jacente. Dans ce cas, il sert de cible intermédiaire pour résoudre le tampon de dessin multisampling avant le blitting (transfert de bloc de bits) vers le tampon de rendu.
  3. Tampon de rendu : tampon générique soutenu par un objet GBM, qui correspond au tampon arrière d'une chaîne d'échange graphique typique.

  4. Tampon avant : tampon GBM analysé sur l'écran.

4.2 Chaîne d'échange

Lorsque swap_buffers est appelé, HAR suit les étapes suivantes :

  1. Effectue un blitting du contenu du tampon de dessin vers le tampon de rendu (avec un blitting intermédiaire vers le tampon de résolution, si nécessaire pour l'implémentation).

  2. Appelle glFlush() sur le contexte GL et crée une instance de EGL_SYNC_NATIVE_FENCE_ANDROID pour suivre l'achèvement du GPU.

  3. Crée une requête atomique DRM pour échanger le tampon de rendu avec l'écran. Cette requête contient le FD de la barrière GPU (appelée barrière d'entrée) pour empêcher le contrôleur d'affichage de montrer le tampon de rendu avant que le GPU n'ait terminé le dessin.

  4. Demande simultanément une nouvelle barrière à DRM (appelée barrière de sortie) afin de signaler quand le tampon précédent (le tampon avant de la trame précédente) n'est plus à l'écran.

  5. Valide la requête atomique à l'aide de l'indicateur non bloquant, afin de permettre au thread principal de continuer pendant que les sous-systèmes graphiques restent synchronisés.

  6. Stocke la nouvelle barrière de sortie dans le contexte afin que HAR puisse attendre qu'elle soit signalée au début du processus swap_buffers sur la trame suivante. Cela empêche le GPU de dessiner dans un tampon qui est toujours affiché.

4.3 Définition du mode direct

HAR interagit directement avec le noyau à l'aide des sous-systèmes DRM et KMS (Kernel Mode Setting) pour configurer la résolution d'affichage AAOS SDV, en contournant les interactions avec les gestionnaires de fenêtres tels que SurfaceFlinger (dans des configurations spécifiques), ce qui permet un contrôle exclusif et prioritaire du matériel d'affichage.

4.4 Rendu externe

HAR permet de déléguer le rendu d'éléments d'UI spécifiques (identifiés par des tags dans Figma) à des processus ou threads externes. Cela est utile pour intégrer des scènes 3D complexes (par exemple, une visualisation de voiture ego à partir de moteurs tels que Kanzi ou Unity) ou d'autres contenus nécessitant un contexte OpenGL dédié.

4.4.1 Composants clés

  • HarExternalRenderContext: contexte EGL hors écran dédié pour le service externe.
  • SurfacePool: gère un ensemble de tampons LocalSurface (Texture plus EGLImage) pour la mise en mémoire tampon double ou triple.
  • SharedSurfaceExternalImage: wrapper thread-safe pour transmettre des handles EGLImage entre le service externe et le moteur de rendu principal.

4.4.2 Workflow

Le workflow suit cette séquence :

  1. Le service externe démarre et s'enregistre auprès du looper principal, en identifiant les tags Figma (par exemple, #cluster/3d-car) qu'il affiche.

  2. Le service attend les signaux RenderStart du looper pour aligner son rendu sur le signal VSYNC de l'écran.

  3. Hors écran, le service affiche son contenu dans un tampon de trame fourni par SurfacePool.

  4. Le service appelle swap_buffers sur son contexte, ce qui fait pivoter le pool et met la trame terminée à disposition en tant qu'instance de SharedSurface.

  5. SharedSurface est encapsulé dans ExternalImage et envoyé via un canal MPSC Rust au looper.

  6. Le moteur de rendu Impeller principal (phase 3) reçoit l'image externe. Au lieu de copier les données de pixels, il lie directement l'EGLImage sous-jacent à une texture et le dessine dans le cadre de la scène principale, ce qui permet une composition sans copie.

4.5 Plates-formes de développement et de test (har-platform-linux)

À des fins de développement et de test, les applications HAR peuvent cibler des environnements de bureau Linux standards et des configurations sans interface graphique. Ces plates-formes sont implémentées dans le crate crates/reference/platforms/har-platform-linux.

Contrairement à la cible de production AAOS SDV, ces plates-formes n'utilisent pas le sous-système direct-rendering de har-gl-context pour la sortie d'affichage. Au lieu de cela, elles s'appuient sur des crates Rust OpenGL standards :

  • Mode fenêtré : utilise winit pour la gestion des fenêtres et les boucles d'événements, et glutin pour créer des contextes OpenGL ES et s'intégrer au système de fenêtrage.

  • Mode sans interface graphique : utilise le crate har-gl-context pour créer un contexte pbuffer hors écran avec l'affichage EGL par défaut. Cela permet d'effectuer le rendu dans un tampon hors écran sans avoir besoin d'une fenêtre visible ni d'un accès direct au matériel d'affichage, principalement utilisé pour les tests automatisés ou le traitement backend.