Questa pagina descrive in dettaglio la pipeline grafica completa del renderer ad alta disponibilità (HAR), tracciando il flusso di dati da un documento di progettazione Figma ai pixel finali visualizzati sullo schermo.
Panoramica
La pipeline converte le definizioni dell'interfaccia utente di alto livello in comandi grafici di basso livello e li presenta in modo efficiente sui display hardware. La pipeline è progettata per le app automobilistiche mission-critical per la sicurezza, con particolare attenzione al rendering deterministico, alla gestione efficiente dello stato e all'interazione robusta con i sottosistemi grafici della piattaforma, come Direct Rendering Manager (DRM) e Generic Buffer Management (GBM).
La pipeline può essere suddivisa in quattro fasi principali:
- Prerender: elaborazione del grafico della scena, applicazione delle personalizzazioni e risoluzione del layout.
- Generazione dei comandi: conversione del grafico della scena risolto in un elenco di visualizzazione indipendente dal backend.
- Rendering: esecuzione dei comandi di disegno utilizzando il motore grafico Impeller.
- Presentazione: gestione dei framebuffer e sincronizzazione con l'hardware del display.
Figura 1. Flusso grafico HAR.
Fase 1: prerender
Questa fase trasforma la progettazione statica di Figma e lo stato dinamico dell'app in un albero dell'interfaccia utente completamente risolto in memoria, pronto per il rendering. Questa fase viene eseguita su un thread di riduzione dedicato, separato dal loop di visualizzazione principale.
1.1 Base di DesignCompose
La pipeline HAR è basata sull'ecosistema DesignCompose.
- Origine: l'interfaccia utente è progettata in Figma ed esportata utilizzando il plug-in DesignCompose.
- Definizione: l'output è un'istanza di
DesignComposeDefinition, una rappresentazione serializzata della progettazione (nodi, stili, varianti). - Data binding: il modello dell'interfaccia utente dell'app utilizza macro procedurali (ad esempio,
#[Design(node = "#speed")]) per associare esplicitamente i campi della struttura Rust a nodi denominati specifici nel documento Figma. In questo modo, lo stato dell'app guida automaticamente le proprietà degli elementi visivi.
I componenti chiave di questa base sono:
- Riduttore: funge da loop di eventi centrale, elaborando le azioni e aggiornando lo stato attuale. Il framework fornisce
DefaultReducer, ma se necessario è possibile fornire un'implementazione del riduttore personalizzata. - Presentatore: collega lo stato attuale al modello dell'interfaccia utente. Il
Presentertratto è specificato dalla crate del frameworkharrye un'implementazione di riferimento (UIModelPresenter) è fornita nella crateharry-app-core. - Modello dell'interfaccia utente: genera personalizzazioni in base allo stato attuale. Il codice del modello dell'interfaccia utente viene generato utilizzando la macro
DesignDocumentfornita dalla cratederive_customizations. La strutturaUIModelnella crateharry-app-corefornisce un esempio di questo. - Squoosh: fornisce la struttura dei dati
SquooshViewe il repository delle varianti, utilizzati per eseguire il rendering dell'interfaccia utente in base alla progettazione. Un documento di progettazione serializzato viene caricato dalla cratedc_bundledalla libreria DesignCompose e convertito in un albero di struttureSquooshViewper prestazioni di runtime efficienti.
1.2 Loop del riduttore
La pipeline è guidata dalle azioni. Il framework specifica il tipo enumerato Actions che definisce le azioni interne utilizzate dal framework stesso, ma include anche una variante CustomAction che consente agli utenti di definire azioni aggiuntive specifiche dell'app (ad esempio, UpdateVehicleSpeed o ButtonPress).
Il framework fornisce anche il tratto StateAction che semplifica l'implementazione delle azioni che influiscono sullo stato dell'app e, facoltativamente, genera effetti collaterali che vengono poi restituiti all'app dal riduttore per l'elaborazione. L'enumerazione CustomActions nella crate harry-app-core fornisce un esempio dettagliato di questo.
Di seguito è riportato uno schema di base del loop del riduttore:
- Elaborazione delle azioni:
Reducerriceve un'azione e aggiorna lo stato attuale. Si tratta dei dati non elaborati, come la velocità attuale o le spie (spie di avviso) attive. Potrebbe anche generare effetti collaterali (ad esempio, un segnale che riproduce un suono quando la spia della cintura di sicurezza lampeggia). - Presentazione:
Presentermappa il nuovo stato inUIModel.UIModelè un modello di visualizzazione che contiene dati formattati appositamente per l'interfaccia utente (ad esempio, la formattazione della velocità "120" in una stringa "65 mph"). - Generazione della personalizzazione: viene chiamato il metodo
applydel modello dell'interfaccia utente per generare un insieme di istanzeRenderCustomization. Si tratta di istruzioni esplicite per modificare la progettazione di Figma (ad esempio, "Imposta il testo del nodo #speed su '65 mph'"). UpdatePolicyper l'ottimizzazione: dopo ogni passaggio di prerender, viene restituito un valoreUpdatePolicy, che indica quando è necessario il prossimo aggiornamento del rendering. Se non sono in attesa modifiche dello stato e non sono in esecuzione animazioni,UpdatePolicysegnala che non sono necessari ulteriori aggiornamenti immediati. In questi casi, il riduttore smette di generare nuovi elenchi di visualizzazione, impedendo cicli di rendering non necessari e conservando le risorse finché una nuova azione o un nuovo evento non attiva una modifica.
1.3 Inserimento delle visualizzazioni e inizializzazione del repository
La pipeline inizia con un'istanza DesignComposeDefinition. Si tratta del documento di progettazione Figma serializzato da DesignCompose in una struttura di buffer di protocollo.
Caricamento iniziale: all'avvio, la progettazione principale (specificata dal nodo radice) viene convertita da
DesignComposeDefinitionin un alberoSquooshViewiniziale. Si tratta di un processo una tantum.Repository:
SquooshVariantRepositorygestisce le varianti dei componenti riutilizzabili e le visualizzazioni caricate inizialmente.Caricamento lazy: per ridurre al minimo il tempo di avvio e l'utilizzo della memoria, le visualizzazioni aggiuntive (quelle non incluse nell'albero del nodo radice iniziale) vengono caricate in modo lazy dal documento solo quando vengono referenziate esplicitamente e sono necessarie per la logica di rendering (ad esempio, durante una personalizzazione dell'elenco).
1.4 Passaggio di personalizzazione
L'albero SquooshView viene attraversato per applicare lo stato dinamico dell'app:
Scambi di varianti: le istanze dei componenti vengono scambiate con varianti specifiche (ad esempio, la modifica di un'icona che rappresenta la modalità di guida attuale da sport a eco) in base alla logica di runtime.
Espansione dell'elenco: un singolo elemento del modello in Figma viene sostituito da un elenco dinamico di elementi secondari. Per questi elementi secondari vengono generati nuovi ID univoci per verificare un'identità stabile per le animazioni.
Override di testo e stile: il contenuto del testo (ad esempio, il valore della velocità) e gli stili (ad esempio, opacità, colore) vengono aggiornati dallo stato attuale.
1.5 Risoluzione variabile
I token di progettazione e le variabili definiti in Figma o localmente nell'app vengono risolti.
- Binding: le proprietà
SquooshViewche fanno riferimento alle variabili (come colori o dimensioni) vengono sostituite con i relativi valori concreti per il frame corrente.
1.6 Calcolo del layout
Layout dinamico:
DynamicLayoutcalcola la posizione e le dimensioni finali (limiti) di ogni nodo nell'alberoSquooshView.Layout del testo:
TextHelperutilizza un'implementazione del trattoLayoutHelperper calcolare le metriche del testo, il wrapping e la forma. In questo modo, prima del rendering, è possibile verificare che il testo scorra correttamente all'interno dei vincoli.
1.7 Quadranti e indicatori
Si tratta di un passaggio specializzato per le interfacce utente automobilistiche.
MeterData: se un nodo ha dati di misurazione (definiti in Figma), la sua geometria viene modificata dinamicamente in base ameter_value(ad esempio, la velocità del veicolo).- Archi: l'angolo di sweep viene regolato.
- Rotazioni: la trasformazione di rotazione viene calcolata in base agli angoli di inizio e fine.
- Barre di avanzamento: la larghezza o l'altezza di un rettangolo viene scalata.
- Vettori di avanzamento: la lunghezza di un percorso vettoriale viene regolata.
1.8 Animazione
Diffing: l'oggetto
SquooshViewcorrente viene confrontato conprevious_squoosh_viewdaPreRenderCache.Interpolazione: se le proprietà sono cambiate,
Squooshcrea interpolatori per la transizione graduale dei valori (ad esempio, opacità o trasformazione) nel tempo.
Fase 2: generazione dei comandi
Una volta che l'albero SquooshView è completamente risolto e animato, viene convertito in una sequenza lineare di comandi di disegno.
Il componente chiave di questa fase è la crate DisplayList:
generate_dl: questa funzione attraversa in modo ricorsivo l'alberoSquooshView.Traduzione:
- Forme e percorsi: convertiti in
DisplayListEntrycon la varianteDisplayListAppearanceappropriata (ad esempio,RectoPath) - Testo: convertito con
TextHelperin voci di disegno del testo. - Trasformazioni e clip: convertiti in coppie
PushTransform3DePopTransform3DoPushClipRegionePopClipRegionper gestire lo stack dello stato di disegno. - Mascheramento: convertito in coppie
PushMaskLayerePopMaskLayerper creare e combinare correttamente i livelli.
- Forme e percorsi: convertiti in
Il risultato finale è un'istanza di Vec<DisplayListEntry> che descrive cosa
disegnare, indipendentemente da come disegnarlo.
2.1 Trasferimento al looper
Dopo aver generato DisplayList, il riduttore lo racchiude in un'istanza di ViewDescriptor e lo invia tramite un canale MPSC Rust (LooperMessage) al thread del looper. Il Looper è responsabile delle fasi di rendering e visualizzazione, il che impedisce al thread del riduttore di bloccare la pipeline grafica.
Fase 3: rendering
L'oggetto DisplayList indipendente dalla piattaforma viene trasferito al backend di rendering, dove i comandi astratti vengono tradotti in istruzioni GPU.
HAR utilizza Impeller, un motore di rendering originariamente creato per Flutter. Impeller è progettato per risolvere il problema dei glitch del frame rate dovuti alla compilazione degli shader precompilando un insieme di shader piccolo ed efficiente in fase di build. Questo approccio, combinato con un batching efficace e un backend altamente ottimizzato, offre:
- Prestazioni deterministiche: elimina virtualmente i glitch di compilazione degli shader in fase di runtime.
- Avvio rapido: riduce l'overhead di inizializzazione.
- Ingombro ridotto: produce una dimensione binaria compatta.
Per un'introduzione completa all'architettura di Impeller, guarda il video Introducing Impeller - Flutter's new rendering engine. Sebbene il video tratti di Flutter, questi vantaggi principali potenziano direttamente lo stack automobilistico HAR.
I componenti chiave della fase di rendering sono:
ImpellerRenderer: converte l'elenco di visualizzazione dalla fase di prerender in comandi di rendering Impeller.API Impeller Rust: esegue il wrapping della libreria Impeller per l'utilizzo in Rust (le crate
impellereimpeller-rs-bindgen).TypographyContext: gestisce la registrazione dei caratteri e la forma del testo.
3.1 Inizializzazione e gestione delle superfici
Creazione del contesto: il renderer inizializza un'istanza di
impeller::Contextcon un backend OpenGL ES, passando un callback per risolvere i puntatori delle funzioni OpenGL ES dal contesto GL della piattaforma.Superficie FBO con wrapping: anziché creare la propria finestra, Impeller esegue il rendering in un oggetto framebuffer OpenGL (FBO) esistente fornito dalla Fase 4. Questa operazione viene eseguita chiamando
Surface::create_wrapped_fbo.
3.2 Gestione delle risorse
Immagini: supporta i formati standard e le texture compresse KTX2. Queste vengono caricate nelle texture della GPU e gestite da una struttura
Resourcesinterna.Caratteri: i caratteri TrueType e OpenType vengono caricati e registrati con
TypographyContextper il rendering del testo.Immagini esterne: la gestione specializzata delle texture esterne (ad esempio, feed della fotocamera e renderer 3D esterni) comporta l'associazione di istanze
EGLImageo texture OpenGL esterne agli oggettiTexturedi Impeller per il rendering senza copia.
3.3 Passaggio di rendering
Il loop render crea un'istanza DisplayList di Impeller (da non confondere con Vec<DisplayListEntry> generata dalla fase di prerender) utilizzando DisplayListBuilder:
Cancella il buffer e applica le trasformazioni globali per la scalabilità DPI e la rotazione del display.
Esegue l'iterazione degli elementi
DisplayListEntrydi input:- Stato:
save()erestore()vengono utilizzati per eseguire il push e il pop delle trasformazioni e delle regioni di clip. - Primitive:
RecteRoundedRectvengono disegnati utilizzando le operazioni di disegno standard. - Percorsi: vengono creati e disegnati percorsi vettoriali complessi (incluse le istanze
Arcdinamiche). - Testo:
TexteStyledTextvengono sottoposti a rendering utilizzandoTypographyContext. - Immagini: le immagini standard ed esterne vengono disegnate utilizzando
draw_texture_rect.
- Stato:
Invia l'elenco di visualizzazione Impeller creato alla superficie utilizzando
surface.draw_display_list(), generando i comandi GL sottostanti.Chiama
swap_buffers()sul contesto sottostante per attivare la Fase 4.
Fase 4: presentazione
Questa fase finale gestisce l'interazione con l'hardware del display per mostrare il frame sottoposto a rendering. HAR utilizza un percorso di rendering diretto robusto su Android Automotive OS (AAOS) Software-Defined Vehicle (SDV).
Il componente chiave di questa fase è HarDirectRenderingContext (nella crate har-gl-context).
4.1 Architettura
Il livello di presentazione utilizza un approccio con doppio buffer con una destinazione di disegno offscreen:
Buffer di disegno: FBO offscreen in cui Impeller esegue il rendering della scena.
Buffer di risoluzione (facoltativo): buffer ausiliario facoltativo per supportare l'anti-aliasing multisampling (MSAA)
- Può essere abilitato quando necessario dall'implementazione o dalla configurazione OpenGL ES sottostante. In questi casi, funge da destinazione intermedia per risolvere il buffer di disegno multisampling prima di eseguire il blitting (trasferimento di blocchi di bit) nel buffer di rendering.
Buffer di rendering: buffer generico supportato da un oggetto GBM, che corrisponde al buffer di back-end in una tipica catena di scambio grafico.
Buffer frontale: buffer GBM sottoposto a scansione sul display.
4.2 Catena di scambio
Quando viene chiamato swap_buffers, HAR esegue i seguenti passaggi:
Esegue il blitting dei contenuti del buffer di disegno nel buffer di rendering (con un blitting intermedio nel buffer di risoluzione, se necessario per l'implementazione).
Chiama
glFlush()sul contesto GL e crea un'istanza diEGL_SYNC_NATIVE_FENCE_ANDROIDper monitorare il completamento della GPU.Crea una richiesta atomica DRM per scambiare il buffer di rendering sullo schermo. Questa richiesta contiene l'FD della barriera GPU (denominata barriera in ingresso) per impedire al controller del display di mostrare il buffer di rendering prima che la GPU abbia terminato il disegno.
Richiede contemporaneamente una nuova barriera da DRM (denominata barriera in uscita) per segnalare quando il buffer precedente (il buffer frontale per il frame precedente) non è più sullo schermo.
Esegue il commit della richiesta atomica utilizzando il flag non bloccante, per consentire al thread principale di continuare mentre i sottosistemi grafici rimangono sincronizzati.
Memorizza la nuova barriera in uscita nel contesto in modo che HAR possa attendere che venga segnalata all'inizio del processo
swap_buffersnel frame successivo. In questo modo, la GPU non disegna in un buffer ancora visualizzato.
4.3 Impostazione della modalità diretta
HAR interagisce direttamente con il kernel utilizzando i sottosistemi DRM e KMS (Kernel Mode Setting) per configurare la risoluzione del display AAOS SDV, bypassando le interazioni con i gestori di finestre come SurfaceFlinger (in configurazioni specifiche), consentendo un controllo esclusivo e prioritario dell'hardware del display.
4.4 Rendering esterno
HAR supporta la delega del rendering di elementi specifici dell'interfaccia utente (identificati dai tag in Figma) a processi o thread esterni. Questa funzionalità è utile per integrare scene 3D complesse (ad esempio, una visualizzazione dell'auto ego da motori come Kanzi o Unity) o altri contenuti che richiedono un contesto OpenGL dedicato.
4.4.1 Componenti chiave
HarExternalRenderContext: un contesto EGL offscreen dedicato per il servizio esterno.SurfacePool: gestisce un insieme di bufferLocalSurface(Texture+EGLImage) per il buffering doppio o triplo.SharedSurfaceExternalImage: un wrapper thread-safe per il passaggio di handleEGLImagetra il servizio esterno e il renderer principale.
4.4.2 Flusso di lavoro
Il flusso di lavoro segue questa sequenza:
Il servizio esterno viene avviato e registrato con il looper principale, identificando i tag Figma (ad esempio,
#cluster/3d-car) di cui esegue il rendering.Il servizio attende i segnali
RenderStartdal looper per allineare il rendering al segnale VSYNC del display.Offscreen, il servizio esegue il rendering dei contenuti in un framebuffer fornito da
SurfacePool.Il servizio chiama
swap_bufferssul relativo contesto, che ruota il pool e rende disponibile il frame completato come istanza diSharedSurface.SharedSurfaceviene racchiuso inExternalImagee inviato tramite un canale MPSC Rust al looper.Il renderer Impeller principale (Fase 3) riceve l'immagine esterna. Anziché copiare i dati dei pixel, associa l'oggetto
EGLImagesottostante direttamente a una texture e la disegna come parte della scena principale, ottenendo una composizione senza copia.
4.5 Piattaforme di sviluppo e test (har-platform-linux)
Per scopi di sviluppo e test, le app HAR possono scegliere come target gli ambienti desktop Linux standard e le configurazioni headless. Queste piattaforme sono implementate nella crate crates/reference/platforms/har-platform-linux.
A differenza della destinazione di produzione AAOS SDV, queste piattaforme non utilizzano il sottosistema direct-rendering di har-gl-context per l'output del display. Si basano invece sulle crate OpenGL Rust standard:
Modalità finestra: utilizza
winitper la gestione delle finestre e i loop di eventi eglutinper la creazione di contesti OpenGL ES e l'integrazione con il sistema di finestre.Modalità headless: utilizza la crate
har-gl-contextper creare un contesto pbuffer offscreen con il display EGL predefinito. In questo modo, è possibile eseguire il rendering in un buffer offscreen senza la necessità di una finestra visibile o di un accesso diretto all'hardware del display, utilizzato principalmente per i test automatici o l'elaborazione di backend.