HAR-Grafikpipeline

Auf dieser Seite wird die vollständige Grafikpipeline des High Availability Renderer (HAR) beschrieben. Dabei wird der Datenfluss von einem Figma-Designdokument bis zu den endgültigen Pixeln auf dem Bildschirm nachvollzogen.

Übersicht

Die Pipeline wandelt allgemeine UI-Definitionen in Low-Level-Grafikbefehle um und stellt sie effizient auf Hardware-Displays dar. Die Pipeline wurde für sicherheitskritische Automotive-Apps entwickelt und legt den Schwerpunkt auf deterministisches Rendern, effiziente Statusverwaltung und robuste Interaktion mit Grafiksubsystemen der Plattform wie Direct Rendering Manager (DRM) und Generic Buffer Management (GBM).

Die Pipeline lässt sich in vier Hauptphasen unterteilen:

  1. Vorrendern:Verarbeiten des Szenengraphen, Anwenden von Anpassungen und Auflösen des Layouts.
  2. Befehlsgenerierung:Der aufgelöste Szenengraph wird in eine backendunabhängige Anzeigeliste umgewandelt.
  3. Rendering:Ausführen von Zeichenbefehlen mit der Impeller-Grafik-Engine.
  4. Präsentation:Framebuffer verwalten und mit der Displayhardware synchronisieren.

HAR-Grafikablauf

Abbildung 1. HAR-Grafikfluss.

Phase 1: Vorrendern

In dieser Phase werden das statische Figma-Design und der dynamische App-Status in einen vollständig aufgelösten In-Memory-UI-Baum umgewandelt, der für das Rendern bereit ist. Diese Phase wird in einem separaten Reducer-Thread ausgeführt, der unabhängig von der Hauptschleife für die Darstellung ist.

1.1 DesignCompose-Grundlagen

Die HAR-Pipeline basiert auf dem DesignCompose-Ökosystem.

  • Quelle:Die Benutzeroberfläche wird in Figma entworfen und mit dem DesignCompose-Plug-in exportiert.
  • Definition:Die Ausgabe ist eine Instanz von DesignComposeDefinition, einer serialisierten Darstellung des Designs (Knoten, Stile, Varianten).
  • Datenbindung:Das UI-Modell der App verwendet prozedurale Makros (z. B. #[Design(node = "#speed")]), um Rust-Struct-Felder explizit an bestimmte benannte Knoten im Figma-Dokument zu binden. Dadurch können die Eigenschaften der visuellen Elemente automatisch durch den App-Status gesteuert werden.

Die wichtigsten Komponenten dieser Grundlage sind:

  • Reducer:Fungiert als zentrale Ereignisschleife, verarbeitet Aktionen und aktualisiert den aktuellen Status. Das Framework bietet DefaultReducer, aber bei Bedarf kann eine benutzerdefinierte Reducer-Implementierung bereitgestellt werden.
  • Presenter:Stellt die Verbindung zwischen dem aktuellen Status und dem UI-Modell her. Das Presenter-Trait wird durch das harry-Framework-Crate angegeben und eine Referenzimplementierung (UIModelPresenter) wird im harry-app-core-Crate bereitgestellt.
  • UI-Modell:Generiert Anpassungen basierend auf dem aktuellen Status. Der UI-Modellcode wird mit dem Makro DesignDocument generiert, das vom derive_customizations-Crate bereitgestellt wird. Die UIModel-Struktur im harry-app-core-Crate ist ein Beispiel dafür.
  • Squoosh:Stellt die Datenstruktur SquooshView und das Variant-Repository bereit, die zum Rendern der Benutzeroberfläche gemäß dem Design verwendet werden. Ein serialisiertes Designdokument wird von der dc_bundle-Crate aus der DesignCompose-Bibliothek geladen und zur effizienten Laufzeitleistung in einen Baum von SquooshView-Structs konvertiert.

1.2 Reducer-Schleife

Die Pipeline wird durch Aktionen gesteuert. Das Framework gibt den Actionsaufgezählten Typ an, der interne Aktionen definiert, die vom Framework selbst verwendet werden. Er enthält aber auch eine CustomAction-Variante, mit der Nutzer zusätzliche app-spezifische Aktionen definieren können (z. B. UpdateVehicleSpeed oder ButtonPress).

Das Framework bietet auch das StateAction-Trait, das die Implementierung von Aktionen vereinfacht, die sich auf den App-Status auswirken und optional Nebeneffekte erzeugen, die dann vom Reducer zur Verarbeitung an die App zurückgegeben werden. Das CustomActions-Enum im harry-app-core-Crate bietet ein detailliertes Beispiel dafür.

Hier ist ein grundlegender Überblick über den Reducer-Loop:

  • Aktionsverarbeitung:Reducer empfängt eine Aktion und aktualisiert den aktuellen Status. Das sind die Rohdaten, z. B. die aktuelle Geschwindigkeit oder welche Kontrollleuchten aktiv sind. Dies kann auch Nebenwirkungen haben (z. B. ertönt ein Signal, wenn die Sicherheitsgurt-Kontrollleuchte blinkt).
  • Darstellung:Presenter wird in UIModel abgebildet. UIModel ist ein Ansichtsmodell, das Daten enthält, die speziell für die Benutzeroberfläche formatiert sind (z. B. die Formatierung der Geschwindigkeit „120“ in den String „65 mph“).
  • Generierung der Anpassung:Die Methode apply des UI-Modells wird aufgerufen, um eine Reihe von RenderCustomization-Instanzen zu generieren. Dies sind explizite Anweisungen zum Ändern des Figma-Designs (z. B. „Setze den Text des Knotens #speed auf ‚65 mph‘“).
  • UpdatePolicy für die Optimierung:Nach jedem Prerendering-Durchlauf wird ein UpdatePolicy-Wert zurückgegeben, der angibt, wann das nächste Rendering-Update erforderlich ist. Wenn keine Statusänderungen ausstehen und keine Animationen ausgeführt werden, signalisiert UpdatePolicy, dass keine weiteren Updates erforderlich sind. In solchen Fällen generiert der Reducer keine neuen Anzeigelisten mehr, wodurch unnötige Rendering-Zyklen vermieden und Ressourcen geschont werden, bis eine neue Aktion oder ein neues Ereignis eine Änderung auslöst.

1.3 Aufnahme und Repository-Initialisierung ansehen

Die Pipeline beginnt mit einer DesignComposeDefinition-Instanz. Dies ist das Figma-Designdokument, das von DesignCompose in eine Protokollzwischenspeicherstruktur serialisiert wird.

  • Erstes Laden:Beim Start wird das Hauptdesign (durch den zugehörigen Stammknoten angegeben) von DesignComposeDefinition in einen ersten SquooshView-Baum konvertiert. Das ist ein einmaliger Vorgang.

  • Repository:SquooshVariantRepository verwaltet Varianten wiederverwendbarer Komponenten und die anfänglich geladenen Ansichten.

  • Lazy Loading:Um die Startzeit und die Speichernutzung zu minimieren, werden zusätzliche Ansichten (die nicht Teil des ursprünglichen Stammknotenbaums sind) nur dann aus dem Dokument geladen, wenn sie explizit referenziert und von der Rendering-Logik benötigt werden (z. B. bei einer Listenanpassung).

1.4 Anpassungsdurchlauf

Der SquooshView-Baum wird durchlaufen, um den dynamischen App-Status anzuwenden:

  • Variantenwechsel:Komponenteninstanzen werden basierend auf der Laufzeitlogik durch bestimmte Varianten ersetzt. So kann beispielsweise das Symbol für den aktuellen Fahrmodus von „Sport“ zu „Eco“ geändert werden.

  • Listenerweiterung:Ein einzelnes Vorlagenelement in Figma wird durch eine dynamische Liste von untergeordneten Elementen ersetzt. Für diese untergeordneten Elemente werden neue eindeutige IDs generiert, um eine stabile Identität für Animationen zu gewährleisten.

  • Text- und Stilüberschreibungen:Textinhalte (z. B. Geschwindigkeitswert) und Stile (z. B. Deckkraft, Farbe) werden anhand des aktuellen Status aktualisiert.

1.5 Variable Auflösung

Design-Tokens und Variablen, die in Figma oder lokal in der App definiert sind, werden aufgelöst.

  • Binding:SquooshView-Attribute, die auf Variablen (z. B. Farben oder Dimensionen) verweisen, werden für den aktuellen Frame durch ihre konkreten Werte ersetzt.

1.6 Layoutberechnung

  • Dynamisches Layout:DynamicLayout berechnet die endgültige Position und Größe (Begrenzungen) jedes Knotens im SquooshView-Baum.

  • Textlayout:TextHelper verwendet eine Implementierung des LayoutHelper-Traits, um Textmesswerte, Umbruch und Formung zu berechnen. So lässt sich überprüfen, ob der Text innerhalb der Einschränkungen richtig fließt, bevor er gerendert wird.

1.7 Zifferblätter und Messgeräte

Dies ist ein spezieller Schritt für Benutzeroberflächen für Autos.

  • MeterData: Wenn ein Knoten Messdaten hat (in Figma definiert), wird seine Geometrie dynamisch auf Grundlage von meter_value (z. B. Fahrzeuggeschwindigkeit) geändert.
    • Bögen:Der Sweep-Winkel wird angepasst.
    • Drehungen:Die Drehungstransformation wird auf Grundlage von Start- und Endwinkeln berechnet.
    • Fortschrittsbalken:Die Breite oder Höhe eines Rechtecks wird skaliert.
    • Fortschrittsvektoren:Die Länge eines Vektorpfads wird angepasst.

1.8 Animation

  • Vergleich:Der aktuelle SquooshView wird mit previous_squoosh_view aus PreRenderCache verglichen.

  • Interpolation:Wenn sich Eigenschaften geändert haben, erstellt Squoosh Interpolatoren, um Werte (z. B. Deckkraft oder Transformation) im Zeitverlauf sanft zu ändern.

Phase 2: Befehlsgenerierung

Nachdem der SquooshView-Baum vollständig aufgelöst und animiert wurde, wird er in eine lineare Folge von Zeichenbefehlen umgewandelt.

Die Hauptkomponente dieser Phase ist das DisplayList-Crate:

  • generate_dl: Diese Funktion durchläuft den SquooshView-Baum rekursiv.

  • Übersetzung:

    • Formen und Pfade:In DisplayListEntry mit der entsprechenden DisplayListAppearance-Variante (z. B. Rect oder Path) konvertiert
    • Text:Mit TextHelper in Texteinträge für Zeichnungen umgewandelt.
    • Transformationen und Clips:In PushTransform3D- und PopTransform3D- oder PushClipRegion- und PopClipRegion-Paare konvertiert, um den Stapel des Zeichenstatus zu verwalten.
    • Maskierung:In PushMaskLayer- und PopMaskLayer-Paare konvertiert, um Ebenen richtig zu erstellen und zu mischen.

Das Endergebnis ist eine Instanz von Vec<DisplayListEntry>, die was gezeichnet werden soll, unabhängig davon, wie es gezeichnet werden soll.

2.1 Übergabe an Looper

Nachdem DisplayList generiert wurde, umschließt der Reducer es in einer Instanz von ViewDescriptor und sendet es über einen Rust-MPSC-Kanal (LooperMessage) an den Looper-Thread. Der Looper ist für die Rendering- und Anzeigevorgänge verantwortlich, wodurch verhindert wird, dass der Reducer-Thread die Grafikpipeline blockiert.

Phase 3: Rendern

Die plattformunabhängige DisplayList wird an das Rendering-Backend übergeben, wo abstrakte Befehle in GPU-Anweisungen übersetzt werden.

HAR verwendet Impeller, eine Rendering-Engine, die ursprünglich für Flutter entwickelt wurde. Impeller wurde entwickelt, um das Problem von Frame-Rate-Glitches aufgrund der Shader-Kompilierung zu lösen. Dazu wird eine kleine, effiziente Gruppe von Shadern zur Build-Zeit vorkompiliert. Dieser Ansatz in Kombination mit effektiver Batch-Verarbeitung und einem hochoptimierten Backend bietet folgende Vorteile:

  • Deterministische Leistung:Laufzeit-Shader-Kompilierungsfehler werden praktisch eliminiert.
  • Schneller Start:Der Initialisierungsaufwand wird reduziert.
  • Geringer Speicherbedarf:Die Binärdatei ist kompakt.

Eine ausführliche Einführung in die Impeller-Architektur finden Sie im Video Introducing Impeller - Flutter's new rendering engine. Im Video geht es zwar um Flutter, aber diese grundlegenden Vorteile wirken sich direkt auf den HAR-Automotive-Stack aus.

Die wichtigsten Komponenten der Rendering-Phase sind:

  • ImpellerRenderer: Konvertiert die Anzeigeliste aus der Vorrenderphase in Impeller-Rendering-Befehle.

  • Impeller Rust API:Umschließt die Impeller-Bibliothek zur Verwendung in Rust (die Crates impeller und impeller-rs-bindgen).

  • TypographyContext: Verwaltet die Schriftregistrierung und die Textformatierung.

3.1 Initialisierung und Oberflächenverwaltung

  • Kontexterstellung:Der Renderer initialisiert eine Instanz von impeller::Context mit einem OpenGL ES-Backend und übergibt einen Callback, um OpenGL ES-Funktionszeiger aus dem GL-Kontext der Plattform aufzulösen.

  • Wrapped FBO surface:Anstatt ein eigenes Fenster zu erstellen, rendert Impeller in ein vorhandenes OpenGL-Framebuffer-Objekt (FBO), das von Phase 4 bereitgestellt wird. Dazu wird Surface::create_wrapped_fbo aufgerufen.

3.2 Ressourcenverwaltung

  • Bilder:Unterstützt Standardformate und KTX2-komprimierte Texturen. Diese werden in GPU-Texturen hochgeladen und von einer internen Resources-Struktur verwaltet.

  • Schriftarten:TrueType- und OpenType-Schriftarten werden geladen und bei der TypographyContext für das Rendern von Text registriert.

  • Externe Bilder:Die spezielle Verarbeitung externer Texturen (z. B. Kamera-Feeds und externe 3D-Renderer) umfasst das Binden von EGLImage-Instanzen oder externen OpenGL-Texturen an Impeller-Texture-Objekte für das Rendering ohne Kopieren.

3.3 Render-Pass

In der render-Schleife wird mit DisplayListBuilder eine Impeller-DisplayList-Instanz erstellt (nicht zu verwechseln mit dem Vec<DisplayListEntry>, das in der Vorrenderphase generiert wird):

  1. Leert den Puffer und wendet globale Transformationen für die DPI-Skalierung und die Displaydrehung an.

  2. Iteriert die Eingabe-DisplayListEntry-Elemente:

    • Status:save() und restore() werden verwendet, um Transformationen und Clip-Bereiche zu pushen und zu popen.
    • Grundelemente:Rect und RoundedRect werden mit Standard-Paint-Vorgängen gezeichnet.
    • Pfade:Komplexe Vektorpfade (einschließlich dynamischer Arc-Instanzen) werden erstellt und gezeichnet.
    • Text:Text und StyledText werden mit TypographyContext gerendert.
    • Bilder:Standard- und externe Bilder werden mit draw_texture_rect gezeichnet.
  3. Sendet die erstellte Impeller-Anzeigeliste mit surface.draw_display_list() an die Oberfläche und generiert die zugrunde liegenden GL-Befehle.

  4. Ruft swap_buffers() für den zugrunde liegenden Kontext auf, um Phase 4 auszulösen.

Phase 4: Präsentation

In dieser letzten Phase wird die Interaktion mit der Displayhardware verarbeitet, um den gerenderten Frame anzuzeigen. HAR verwendet einen robusten direkten Rendering-Pfad auf Software-Defined Vehicles (SDV) mit Android Automotive OS (AAOS).

Die Schlüsselkomponente dieser Phase ist HarDirectRenderingContext (im har-gl-context-Crate).

4.1 Architektur

Die Darstellungsschicht verwendet einen Double-Buffer-Ansatz mit einem Offscreen-Zeichenziel:

  1. Draw buffer (Zeichenpuffer): Offscreen-FBO, in dem Impeller die Szene rendert.

  2. Resolve-Puffer (optional): Optionaler Hilfspuffer zur Unterstützung von Multisample Anti-Aliasing (MSAA)

    • Dies kann bei Bedarf durch die zugrunde liegende OpenGL ES-Implementierung oder -Konfiguration aktiviert werden. In solchen Fällen dient er als Zwischenziel, um den Draw-Buffer mit mehreren Samples aufzulösen, bevor er in den Render-Buffer kopiert wird.
  3. Render-Puffer:Generischer Puffer, der von einem GBM-Objekt unterstützt wird, das dem Backbuffer in einer typischen Grafik-Swapchain entspricht.

  4. Front-Buffer:GBM-Buffer, der auf das Display gescannt wird.

4.2 Kette tauschen

Wenn swap_buffers aufgerufen wird, führt HAR die folgenden Schritte aus:

  1. Überträgt den Inhalt des Zeichenpuffers in den Renderpuffer (bei Bedarf mit einer Zwischenübertragung in den Resolve-Puffer).

  2. Ruft glFlush() im GL-Kontext auf und erstellt eine Instanz von EGL_SYNC_NATIVE_FENCE_ANDROID, um den GPU-Abschluss zu verfolgen.

  3. Erstellt eine atomare DRM-Anfrage, um den Renderpuffer auf den Bildschirm zu übertragen. Diese Anfrage enthält den GPU-Fence-FD (als „in fence“ bezeichnet), um zu verhindern, dass der Displaycontroller den Renderpuffer anzeigt, bevor die GPU mit dem Rendern fertig ist.

  4. Gleichzeitig wird vom DRM ein neuer Fence angefordert (der Out-Fence), um zu signalisieren, wann der vorherige Puffer (der Front-Buffer für den vorherigen Frame) nicht mehr auf dem Bildschirm zu sehen ist.

  5. Führt die atomare Anfrage mit dem nicht blockierenden Flag aus, damit der Hauptthread fortgesetzt werden kann, während die Grafiksubsysteme synchronisiert bleiben.

  6. Speichert den neuen Out-Fence im Kontext, damit HAR darauf warten kann, dass er zu Beginn des swap_buffers-Prozesses im nachfolgenden Frame signalisiert wird. Dadurch wird verhindert, dass die GPU in einen Puffer zeichnet, der noch angezeigt wird.

4.3 Direktmodus einstellen

HAR interagiert direkt mit dem Kernel über die DRM- und KMS-Subsysteme (Kernel Mode Setting), um die AAOS SDV-Displayauflösung zu konfigurieren. Dabei werden Interaktionen mit Fenstermanagern wie SurfaceFlinger (in bestimmten Konfigurationen) umgangen, was eine exklusive und prioritäre Steuerung der Displayhardware ermöglicht.

4.4 Externes Rendern

HAR unterstützt das Delegieren des Renderns bestimmter UI-Elemente (die durch Tags in Figma identifiziert werden) an externe Prozesse oder Threads. Das ist nützlich, um komplexe 3D-Szenen (z. B. eine Visualisierung des eigenen Autos aus Engines wie Kanzi oder Unity) oder andere Inhalte einzubinden, für die ein dedizierter OpenGL-Kontext erforderlich ist.

4.4.1 Wichtige Komponenten

  • HarExternalRenderContext: Ein dedizierter Offscreen-EGL-Kontext für den externen Dienst.
  • SurfacePool: Verwaltet eine Reihe von LocalSurface-Puffern (Texture + EGLImage) für Double- oder Triple-Buffering.
  • SharedSurfaceExternalImage: Ein threadsicherer Wrapper zum Übergeben von EGLImage-Handles zwischen dem externen Dienst und dem Haupt-Renderer.

4.4.2 Workflow

Der Workflow folgt dieser Reihenfolge:

  1. Der externe Dienst wird gestartet und beim Haupt-Looper registriert. Dabei wird angegeben, welche Figma-Tags (z. B. #cluster/3d-car) gerendert werden.

  2. Der Dienst wartet auf RenderStart-Signale vom Looper, um sein Rendering mit dem VSYNC-Signal des Displays abzustimmen.

  3. Der Dienst rendert seine Inhalte im Hintergrund in einen Framebuffer, der von SurfacePool bereitgestellt wird.

  4. Der Dienst ruft swap_buffers in seinem Kontext auf, wodurch der Pool rotiert wird und der fertig gerenderte Frame als Instanz von SharedSurface verfügbar ist.

  5. SharedSurface wird in ExternalImage eingeschlossen und über einen Rust-MPSC-Channel an den Looper gesendet.

  6. Der Haupt-Impeller-Renderer (Phase 3) empfängt das externe Bild. Anstatt Pixeldaten zu kopieren, wird das zugrunde liegende EGLImage direkt an eine Textur gebunden und als Teil der Hauptszene gezeichnet. So wird eine Komposition ohne Kopieren erreicht.

4.5 Entwicklungs- und Testplattformen (har-platform-linux)

Für Entwicklungs- und Testzwecke können HAR-Apps auf Standard-Linux-Desktopumgebungen und Headless-Setups ausgerichtet werden. Diese Plattformen sind im crates/reference/platforms/har-platform-linux-Crate implementiert.

Anders als beim AAOS-SDV-Ziel für die Produktion wird bei diesen Plattformen nicht das direct-rendering-Subsystem von har-gl-context für die Displayausgabe verwendet. Stattdessen werden Standard-Rust-OpenGL-Crates verwendet:

  • Fenstermodus:Hier werden winit für die Fensterverwaltung und Ereignisschleifen sowie glutin zum Erstellen von OpenGL ES-Kontexten und zur Integration in das Fenstersystem verwendet.

  • Headless-Modus:Hier wird das har-gl-context-Crate verwendet, um einen Offscreen-Pufferkontext mit dem standardmäßigen EGL-Display zu erstellen. Dadurch kann in einen Offscreen-Puffer gerendert werden, ohne dass ein sichtbares Fenster oder direkter Zugriff auf die Displayhardware erforderlich ist. Dies wird hauptsächlich für automatisierte Tests oder die Backend-Verarbeitung verwendet.