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:
- Vorrendern:Verarbeiten des Szenengraphen, Anwenden von Anpassungen und Auflösen des Layouts.
- Befehlsgenerierung:Der aufgelöste Szenengraph wird in eine backendunabhängige Anzeigeliste umgewandelt.
- Rendering:Ausführen von Zeichenbefehlen mit der Impeller-Grafik-Engine.
- Präsentation:Framebuffer verwalten und mit der Displayhardware synchronisieren.
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 dasharry-Framework-Crate angegeben und eine Referenzimplementierung (UIModelPresenter) wird imharry-app-core-Crate bereitgestellt. - UI-Modell:Generiert Anpassungen basierend auf dem aktuellen Status. Der UI-Modellcode wird mit dem Makro
DesignDocumentgeneriert, das vomderive_customizations-Crate bereitgestellt wird. DieUIModel-Struktur imharry-app-core-Crate ist ein Beispiel dafür. - Squoosh:Stellt die Datenstruktur
SquooshViewund das Variant-Repository bereit, die zum Rendern der Benutzeroberfläche gemäß dem Design verwendet werden. Ein serialisiertes Designdokument wird von derdc_bundle-Crate aus der DesignCompose-Bibliothek geladen und zur effizienten Laufzeitleistung in einen Baum vonSquooshView-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:
Reducerempfä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:
Presenterwird inUIModelabgebildet.UIModelist 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
applydes UI-Modells wird aufgerufen, um eine Reihe vonRenderCustomization-Instanzen zu generieren. Dies sind explizite Anweisungen zum Ändern des Figma-Designs (z. B. „Setze den Text des Knotens #speed auf ‚65 mph‘“). UpdatePolicyfür die Optimierung:Nach jedem Prerendering-Durchlauf wird einUpdatePolicy-Wert zurückgegeben, der angibt, wann das nächste Rendering-Update erforderlich ist. Wenn keine Statusänderungen ausstehen und keine Animationen ausgeführt werden, signalisiertUpdatePolicy, 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
DesignComposeDefinitionin einen erstenSquooshView-Baum konvertiert. Das ist ein einmaliger Vorgang.Repository:
SquooshVariantRepositoryverwaltet 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:
DynamicLayoutberechnet die endgültige Position und Größe (Begrenzungen) jedes Knotens imSquooshView-Baum.Textlayout:
TextHelperverwendet eine Implementierung desLayoutHelper-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 vonmeter_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
SquooshViewwird mitprevious_squoosh_viewausPreRenderCacheverglichen.Interpolation:Wenn sich Eigenschaften geändert haben, erstellt
SquooshInterpolatoren, 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 denSquooshView-Baum rekursiv.Übersetzung:
- Formen und Pfade:In
DisplayListEntrymit der entsprechendenDisplayListAppearance-Variante (z. B.RectoderPath) konvertiert - Text:Mit
TextHelperin Texteinträge für Zeichnungen umgewandelt. - Transformationen und Clips:In
PushTransform3D- undPopTransform3D- oderPushClipRegion- undPopClipRegion-Paare konvertiert, um den Stapel des Zeichenstatus zu verwalten. - Maskierung:In
PushMaskLayer- undPopMaskLayer-Paare konvertiert, um Ebenen richtig zu erstellen und zu mischen.
- Formen und Pfade:In
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
impellerundimpeller-rs-bindgen).TypographyContext: Verwaltet die Schriftregistrierung und die Textformatierung.
3.1 Initialisierung und Oberflächenverwaltung
Kontexterstellung:Der Renderer initialisiert eine Instanz von
impeller::Contextmit 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_fboaufgerufen.
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
TypographyContextfü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):
Leert den Puffer und wendet globale Transformationen für die DPI-Skalierung und die Displaydrehung an.
Iteriert die Eingabe-
DisplayListEntry-Elemente:- Status:
save()undrestore()werden verwendet, um Transformationen und Clip-Bereiche zu pushen und zu popen. - Grundelemente:
RectundRoundedRectwerden mit Standard-Paint-Vorgängen gezeichnet. - Pfade:Komplexe Vektorpfade (einschließlich dynamischer
Arc-Instanzen) werden erstellt und gezeichnet. - Text:
TextundStyledTextwerden mitTypographyContextgerendert. - Bilder:Standard- und externe Bilder werden mit
draw_texture_rectgezeichnet.
- Status:
Sendet die erstellte Impeller-Anzeigeliste mit
surface.draw_display_list()an die Oberfläche und generiert die zugrunde liegenden GL-Befehle.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:
Draw buffer (Zeichenpuffer): Offscreen-FBO, in dem Impeller die Szene rendert.
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.
Render-Puffer:Generischer Puffer, der von einem GBM-Objekt unterstützt wird, das dem Backbuffer in einer typischen Grafik-Swapchain entspricht.
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:
Überträgt den Inhalt des Zeichenpuffers in den Renderpuffer (bei Bedarf mit einer Zwischenübertragung in den Resolve-Puffer).
Ruft
glFlush()im GL-Kontext auf und erstellt eine Instanz vonEGL_SYNC_NATIVE_FENCE_ANDROID, um den GPU-Abschluss zu verfolgen.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.
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.
Führt die atomare Anfrage mit dem nicht blockierenden Flag aus, damit der Hauptthread fortgesetzt werden kann, während die Grafiksubsysteme synchronisiert bleiben.
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 vonLocalSurface-Puffern (Texture+EGLImage) für Double- oder Triple-Buffering.SharedSurfaceExternalImage: Ein threadsicherer Wrapper zum Übergeben vonEGLImage-Handles zwischen dem externen Dienst und dem Haupt-Renderer.
4.4.2 Workflow
Der Workflow folgt dieser Reihenfolge:
Der externe Dienst wird gestartet und beim Haupt-Looper registriert. Dabei wird angegeben, welche Figma-Tags (z. B.
#cluster/3d-car) gerendert werden.Der Dienst wartet auf
RenderStart-Signale vom Looper, um sein Rendering mit dem VSYNC-Signal des Displays abzustimmen.Der Dienst rendert seine Inhalte im Hintergrund in einen Framebuffer, der von
SurfacePoolbereitgestellt wird.Der Dienst ruft
swap_buffersin seinem Kontext auf, wodurch der Pool rotiert wird und der fertig gerenderte Frame als Instanz vonSharedSurfaceverfügbar ist.SharedSurfacewird inExternalImageeingeschlossen und über einen Rust-MPSC-Channel an den Looper gesendet.Der Haupt-Impeller-Renderer (Phase 3) empfängt das externe Bild. Anstatt Pixeldaten zu kopieren, wird das zugrunde liegende
EGLImagedirekt 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
winitfür die Fensterverwaltung und Ereignisschleifen sowieglutinzum 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.