Auf dieser Seite wird die vollständige Grafikpipeline des Renderers mit hoher Verfügbarkeit (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 UI-Definitionen auf hoher Ebene in Grafikbefehle auf niedriger Ebene um und präsentiert sie effizient auf Hardware-Displays. Die Pipeline wurde für sicherheitskritische Apps für Fahrzeuge entwickelt. Dabei wird der Schwerpunkt auf deterministisches Rendering, effiziente Statusverwaltung und robuste Interaktion mit Grafiksubsystemen der Plattform wie Direct Rendering Manager (DRM) und Generic Buffer Management (GBM) gelegt.
Die Pipeline kann in vier Hauptphasen unterteilt werden:
- Vorrendern:Verarbeitung des Szenengraphen, Anwenden von Anpassungen und Auflösen des Layouts
- Befehlsgenerierung:Umwandlung des aufgelösten Szenengraphen in eine Backend-unabhängige Anzeigeliste
- Rendering:Ausführung von Zeichenbefehlen mit der Impeller-Grafik-Engine
- Präsentation:Verwaltung von Framebuffern und Synchronisierung mit der Displayhardware
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 UI-Baum im Arbeitsspeicher umgewandelt, der für das Rendering bereit ist. Diese Phase wird in einem separaten Reducer-Thread ausgeführt, der von der Hauptanzeigeschleife getrennt ist.
1.1 DesignCompose-Grundlage
Die HAR-Pipeline basiert auf dem DesignCompose-Ökosystem.
- Quelle:Die UI 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-Strukturfelder explizit an bestimmte benannte Knoten im Figma-Dokument zu binden. So 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:Überbrückt den aktuellen Status zum UI-Modell. Das
PresenterTrait wird durch dieharryFramework-Crate angegeben und eine Referenz Implementierung (UIModelPresenter) wird in derharry-app-coreCrate bereitgestellt. - UI-Modell:Generiert Anpassungen basierend auf dem aktuellen Status. Der UI-Modellcode wird mit dem Makro
DesignDocumentgeneriert, das von derderive_customizations-Crate bereitgestellt wird. DieUIModel-Struktur in derharry-app-core-Crate bietet ein Beispiel dafür. - Squoosh:Bietet die Datenstruktur
SquooshViewund das Varianten-Repository, mit denen die UI gemäß dem Design gerendert wird. Ein serialisiertes Designdokument wird von derdc_bundle-Crate aus der DesignCompose-Bibliothek geladen und zur effizienten Laufzeitleistung in einen Baum vonSquooshView-Strukturen umgewandelt.
1.2 Reducer-Schleife
Die Pipeline wird durch Aktionen gesteuert. Das Framework gibt den aufgezählten Typ Actions an, der interne Aktionen definiert, die vom Framework selbst verwendet werden. Er enthält aber auch eine CustomAction-Variante, mit der Nutzer zusätzliche appspezifische 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 generieren, die dann vom Reducer zur Verarbeitung an die App zurückgegeben werden. Die CustomActions-Enum in der harry-app-core-Crate bietet ein detailliertes Beispiel dafür.
Hier ist ein grundlegender Überblick über die Reducer-Schleife:
- Aktionsverarbeitung:
Reducerempfängt eine Aktion und aktualisiert den aktuellen Status. Das sind die Rohdaten wie die aktuelle Geschwindigkeit oder welche Kontrollleuchten aktiv sind. Dadurch können auch Nebeneffekte entstehen (z. B. ein Signal, das einen Gong ausgibt, wenn die Kontrollleuchte für den Sicherheitsgurt blinkt). - Präsentation:
Presenterordnet den neuen StatusUIModelzu.UIModelist ein Ansichtsmodell, das Daten enthält, die speziell für die UI formatiert sind (z. B. Formatieren der Geschwindigkeit „120“ in den String „65 mph“). - Generierung von Anpassungen:Die Methode
applydes UI-Modells wird aufgerufen, um eine Reihe vonRenderCustomization-Instanzen zu generieren. Das sind explizite Anweisungen zum Ändern des Figma-Designs (z. B. „Text des Knotens #speed auf ‚65 mph‘ setzen“). UpdatePolicyzur Optimierung:Nach jedem Vorrenderdurchlauf wird einUpdatePolicy-Wert zurückgegeben, der angibt, wann die nächste Renderingaktualisierung erforderlich ist. Wenn keine Statusänderungen ausstehen und keine Animationen ausgeführt werden, signalisiertUpdatePolicy, dass keine weiteren Aktualisierungen erforderlich sind. In solchen Fällen beendet der Reducer die Generierung neuer Anzeigelisten, wodurch unnötige Renderingzyklen vermieden und Ressourcen geschont werden, bis eine neue Aktion oder ein neues Ereignis eine Änderung auslöst.
1.3 Ansichtserfassung und Repository-Initialisierung
Die Pipeline beginnt mit einer DesignComposeDefinition-Instanz. Das ist das Figma-Designdokument, das von DesignCompose in eine Protokollpufferstruktur serialisiert wurde.
Erstes Laden:Beim Start wird das Hauptdesign (angegeben durch den Stammknoten) von
DesignComposeDefinitionin einen anfänglichenSquooshView-Baum umgewandelt. Das ist ein einmaliger Vorgang.Repository:
SquooshVariantRepositoryverwaltet wiederverwendbare Komponentenvarianten und die anfänglich geladenen Ansichten.Lazy Loading:Um die Startzeit und die Arbeitsspeichernutzung zu minimieren, werden zusätzliche Ansichten (die nicht Teil des anfänglichen Stammknotenbaums sind) nur dann aus dem Dokument geladen, wenn sie explizit referenziert und von der Renderinglogik 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 (z. B. Ändern eines Symbols, das den aktuellen Fahrmodus darstellt, von „Sport“ zu „Eco“).
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 Variablenauflösung
In Figma oder lokal in der App definierte Designtokens und Variablen werden aufgelöst.
- Bindung:
SquooshView-Eigenschaften, die auf Variablen verweisen (z. B. Farben oder Dimensionen), 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 vor dem Rendering überprüfen, ob der Text innerhalb seiner Einschränkungen korrekt fließt.
1.7 Zifferblätter und Messgeräte
Das ist ein spezieller Schritt für Fahrzeug-UIs.
MeterData: Wenn ein Knoten Messgerätedaten hat (in Figma definiert), wird seine Geometrie dynamisch basierend aufmeter_value(z. B. Fahrzeuggeschwindigkeit) geändert.- Bögen:Der Winkel wird angepasst.
- Rotationen:Die Rotationstransformation wird basierend auf 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
Diffing:Die aktuelle
SquooshViewwird mitprevious_squoosh_viewausPreRenderCacheverglichen.Interpolation:Wenn sich Eigenschaften geändert haben, erstellt
SquooshInterpolatoren, um Werte (z. B. Deckkraft oder Transformation) im Zeitverlauf reibungslos zu überblenden.
Phase 2: Befehlsgenerierung
Nachdem der SquooshView-Baum vollständig aufgelöst und animiert wurde, wird er in eine lineare Sequenz von Zeichenbefehlen umgewandelt.
Die Schlüsselkomponente dieser Phase ist die DisplayList-Crate:
generate_dl: Diese Funktion durchläuft denSquooshView-Baum rekursiv.Übersetzung:
- Formen und Pfade:In
DisplayListEntrymit der entsprechendenDisplayListAppearance-Variante umgewandelt (z. B.RectoderPath) - Text:Mit
TextHelperin Texteinträge umgewandelt - Transformationen und Clips:In
PushTransform3DundPopTransform3DoderPushClipRegionundPopClipRegion-Paare umgewandelt, um den Stapel des Zeichenstatus zu verwalten - Maskierung:In
PushMaskLayerundPopMaskLayer-Paare umgewandelt, um Ebenen korrekt zu erstellen und zu mischen
- Formen und Pfade:In
Das Endergebnis ist eine Instanz von Vec<DisplayListEntry>, die beschreibt, was
gezeichnet werden soll, unabhängig davon, wie es gezeichnet werden soll.
2.1 Übergabe an Looper
Nachdem die DisplayList generiert wurde, umschließt der Reducer sie in einer Instanz von ViewDescriptor und sendet sie über einen Rust-MPSC-Kanal (LooperMessage) an den Looper-Thread. Der Looper ist für die Rendering- und Anzeigepahsen verantwortlich, wodurch verhindert wird, dass der Reducer-Thread die Grafikpipeline blockiert.
Phase 3: Rendering
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-Störungen aufgrund der Shader-Kompilierung zu lösen, indem eine kleine, effiziente Gruppe von Shadern zur Build-Zeit vorkompiliert wird. Dieser Ansatz in Kombination mit effektiver Batchverarbeitung und einem hochoptimierten Backend bietet folgende Vorteile:
- Deterministische Leistung:Störungen bei der Shader-Kompilierung zur Laufzeit werden praktisch ausgeschlossen.
- Schneller Start:Reduziert den Initialisierungsaufwand.
- Geringer Speicherbedarf:Erzeugt eine kompakte Binärgröße.
Eine ausführliche Einführung in die Architektur von Impeller finden Sie im Video [Introducing Impeller - Flutter's new rendering engine][impeller-video]. Obwohl das Video Flutter behandelt, kommen diese Hauptvorteile direkt dem HAR-Automotive-Stack zugute.
Die wichtigsten Komponenten der Renderingphase sind:
ImpellerRenderer: Wandelt die Anzeigeliste aus der Vorrenderphase in Impeller-Renderingbefehle um.Impeller Rust API: Umschließt die Impeller-Bibliothek zur Verwendung in Rust (die
impellerundimpeller-rs-bindgenCrates).TypographyContext: Verwaltet die Schriftartregistrierung und die Textformung.
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.Umschlossene FBO-Oberfläche: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 mit
TypographyContextfür das Textrendering registriert.Externe Bilder:Die spezielle Verarbeitung externer Texturen (z. B. Kamerafeeds und externe 3D-Renderer) umfasst das Binden von
EGLImage-Instanzen oder externen OpenGL-Texturen anTexture-Objekte von Impeller für das Rendering ohne Kopieren.
3.3 Renderingdurchlauf
Die render Schleife erstellt mit DisplayListBuilder eine Impeller-DisplayList-Instanz (nicht zu
verwechseln mit Vec<DisplayListEntry>, die in der Vorrenderphase generiert wurde)
:
Leert den Puffer und wendet globale Transformationen für die DPI-Skalierung und die Displayrotation an.
Durchläuft die Eingabeelemente
DisplayListEntry:- Status:
save()undrestore()werden verwendet, um Transformationen und Clipbereiche zu pushen und zu poppen. - Primitive:
RectundRoundedRectwerden mit Standard-Malvorgängen gezeichnet. - Pfade:Komplexe Vektorpfade (einschließlich dynamischer
Arc-Instanzen) werden erstellt und gezeichnet. - Text:
TextundStyledTextwerden mitTypographyContextgerendert. - Bilder:Standardbilder und externe Bilder werden mit
draw_texture_rectgezeichnet.
- Status:
Überträgt die erstellte Impeller-Anzeigeliste mit
surface.draw_display_list()an die Oberfläche und generiert die zugrunde liegenden GL-Befehle.Ruft
swap_buffers()im 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 Renderingpfad auf Android Automotive OS (AAOS) Software-Defined Vehicle (SDV).
Die Schlüsselkomponente dieser Phase ist HarDirectRenderingContext (in der har-gl-context-Crate).
4.1 Architektur
Die Präsentationsebene verwendet einen Ansatz mit doppelter Pufferung und einem Offscreen-Zeichenziel:
Zeichenpuffer:Offscreen-FBO, in dem Impeller die Szene rendert.
Auflösungspuffer (optional) : Optionaler Hilfspuffer zur Unterstützung von Multisample Anti-Aliasing (MSAA)
- Dieser kann bei Bedarf durch die zugrunde liegende OpenGL ES-Implementierung oder -Konfiguration aktiviert werden. In solchen Fällen dient er als Zwischenziel, um den Multisample-Zeichenpuffer aufzulösen, bevor er in den Renderingpuffer kopiert wird.
Renderingpuffer:Generischer Puffer, der von einem GBM-Objekt unterstützt wird und dem Back-Buffer in einer typischen Grafik-Swap-Chain entspricht.
Front-Buffer:GBM-Puffer, der auf dem Display gescannt wird.
4.2 Swap-Chain
Wenn swap_buffers aufgerufen wird, führt HAR die folgenden Schritte aus:
Kopiert den Inhalt des Zeichenpuffers in den Renderingpuffer (mit einer Zwischenkopie in den Auflösungspuffer, falls von der Implementierung erforderlich).
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 Renderingpuffer auf dem Bildschirm zu tauschen. Diese Anfrage enthält die GPU-Fence-FD (als In-Fence bezeichnet), um zu verhindern, dass der Displaycontroller den Renderingpuffer anzeigt, bevor die GPU mit dem Zeichnen fertig ist.
Gleichzeitig wird von DRM ein neues Fence angefordert (als Out-Fence bezeichnet), um zu signalisieren, wann der vorherige Puffer (der Front-Buffer für den vorherigen Frame) nicht mehr auf dem Bildschirm angezeigt wird.
Überträgt die atomare Anfrage mit dem nicht blockierenden Flag, damit der Hauptthread fortgesetzt werden kann, während die Grafiksubsysteme synchronisiert bleiben.
Speichert das neue Out-Fence im Kontext, damit HAR darauf warten kann, dass es zu Beginn des
swap_buffers-Prozesses für den nachfolgenden Frame signalisiert wird. So wird verhindert, dass die GPU in einen Puffer zeichnet, der noch angezeigt wird.
4.3 Einstellung des direkten Modus
HAR interagiert direkt mit dem Kernel über die DRM- und KMS-Subsysteme (Kernel Mode Setting), um die Displayauflösung von AAOS SDV 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 Rendering
HAR unterstützt das Delegieren des Renderings bestimmter UI-Elemente (die durch Tags in Figma identifiziert werden) an externe Prozesse oder Threads. Das ist nützlich für die Einbindung komplexer 3D-Szenen (z. B. eine Visualisierung des eigenen Fahrzeugs aus Engines wie Kanzi oder Unity) oder anderer Inhalte, die einen dedizierten OpenGL-Kontext erfordern.
4.4.1 Schlüsselkomponenten
HarExternalRenderContext: Ein dedizierter Offscreen-EGL-Kontext für den externen Dienst.SurfacePool: Verwaltet eine Reihe vonLocalSurface-Puffern (TextureplusEGLImage) für die doppelte oder dreifache Pufferung.SharedSurfaceExternalImage: Ein threadsicherer Wrapper zum Übergeben vonEGLImage-Handles zwischen dem externen Dienst und dem Hauptrenderer.
4.4.2 Workflow
Der Workflow folgt dieser Sequenz:
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 das Rendering mit dem VSYNC-Signal des Displays abzustimmen.Offscreen rendert der Dienst seine Inhalte in einen Framebuffer, der von
SurfacePoolbereitgestellt wird.Der Dienst ruft
swap_buffersin seinem Kontext auf, wodurch der Pool rotiert und der abgeschlossene Frame als Instanz vonSharedSurfaceverfügbar wird.SharedSurfacewird inExternalImageumschlossen und über einen Rust-MPSC-Kanal an den Looper gesendet.Der Haupt-Impeller-Renderer (Phase 3) empfängt das externe Bild. Anstatt Pixeldaten zu kopieren, bindet er das zugrunde liegende
EGLImagedirekt an eine Textur und zeichnet es als Teil der Hauptszene. 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 werden in der crates/reference/platforms/har-platform-linux-Crate implementiert.
Anders als beim Produktionsziel AAOS SDV verwenden diese Plattformen nicht das direct-rendering-Subsystem von har-gl-context für die Displayausgabe. Stattdessen verwenden sie Standard-Rust-OpenGL-Crates:
Fenstermodus:Verwendet
winitfür die Fensterverwaltung und Ereignisschleifen sowieglutinzum Erstellen von OpenGL ES-Kontexten und zur Integration in das Fenstersystem.Headless-Modus:Verwendet die
har-gl-context-Crate, um einen Offscreen-Pufferkontext mit dem Standard-EGL-Display zu erstellen. So kann in einen Offscreen-Puffer gerendert werden, ohne dass ein sichtbares Fenster oder ein direkter Zugriff auf die Displayhardware erforderlich ist. Das wird hauptsächlich für automatisierte Tests oder die Backend-Verarbeitung verwendet.