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 app critiche per la sicurezza automobilistica, 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:
- Pre-rendering: elaborazione del grafico della scena, applicazione delle personalizzazioni e risoluzione del layout.
- Generazione di comandi:conversione del grafico della scena risolto in un elenco di visualizzazione indipendente dal backend.
- Rendering:esecuzione di comandi di disegno utilizzando il motore grafico Impeller.
- Presentazione:gestione dei framebuffer e sincronizzazione con l'hardware del display.
Figura 1. Flusso di grafici HAR.
Fase 1: pre-rendering
Questa fase trasforma il design statico di Figma e lo stato dinamico dell'app in un albero dell'interfaccia utente completamente risolto e in memoria, pronto per il rendering. Questa fase viene eseguita su un thread reducer dedicato, separato dal ciclo di visualizzazione principale.
1.1 DesignCompose foundation
La pipeline HAR è basata sull'ecosistema DesignCompose.
- Origine:la UI è progettata in Figma ed esportata utilizzando il plug-in DesignCompose.
- Definizione: l'output è un'istanza di
DesignComposeDefinition, una rappresentazione serializzata del design (nodi, stili, varianti). - Associazione di dati: il modello UI dell'app utilizza macro procedurali (ad esempio,
#[Design(node = "#speed")]) per associare esplicitamente i campi della struct Rust a nodi denominati specifici nel documento Figma. In questo modo, lo stato dell'app determina automaticamente le proprietà degli elementi visivi.
I componenti chiave di questa base sono:
- Reducer:funge da ciclo di eventi centrale, elaborando le azioni e aggiornando
lo stato attuale. Il framework fornisce
DefaultReducer, ma se necessario è possibile fornire un'implementazione personalizzata del riduttore. - Presenter:collega lo stato attuale al modello UI. Il tratto
Presenterè specificato dalla crate del frameworkharrye un'implementazione di riferimento (UIModelPresenter) è fornita nella crateharry-app-core. - Modello UI: genera personalizzazioni in base allo stato attuale. Il codice del modello UI viene generato utilizzando la macro
DesignDocumentfornita dal cratederive_customizations. La structUIModelnel crateharry-app-corefornisce un esempio. - Squoosh:fornisce la struttura dei dati
SquooshViewe il repository delle varianti, utilizzati per il rendering della UI in base al design. Un documento di progettazione serializzato viene caricato dalla cratedc_bundledalla libreria DesignCompose e convertito in un albero di structSquooshViewper 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 di azioni che influiscono sullo stato dell'app e, facoltativamente,
genera effetti collaterali che vengono poi restituiti all'app dal
reducer per l'elaborazione. L'enum CustomActions nel crate harry-app-core
fornisce un esempio dettagliato.
Di seguito è riportato uno schema di base del ciclo del reducer:
- Elaborazione dell'azione:
Reducerriceve un'azione e aggiorna lo stato attuale. Si tratta dei dati non elaborati, come la velocità attuale o le spie (luci di avviso) attive. Ciò potrebbe anche generare effetti collaterali (ad esempio, un segnale che riproduce un suono quando la spia della cintura di sicurezza lampeggia). - Presentazione:
Presenteresegue il mapping del nuovo stato inUIModel.UIModelè un modello di visualizzazione che contiene dati formattati appositamente per la UI (ad esempio, la formattazione della velocità "120" in una stringa "65 mph"). - Generazione della personalizzazione:viene chiamato il metodo
applydel modello UI per generare un insieme di istanzeRenderCustomization. Si tratta di istruzioni esplicite per modificare il progetto Figma (ad esempio, "Imposta il testo del nodo #speed su "65 mph"). UpdatePolicyper l'ottimizzazione:dopo ogni passaggio di prerendering, viene restituito un valoreUpdatePolicy, che indica quando è necessario il successivo aggiornamento del rendering. Se non sono in attesa modifiche dello stato e non sono in esecuzione animazioni,UpdatePolicyindica 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 risparmiando risorse finché una nuova azione o un nuovo evento non attivano una modifica.
1.3 Visualizzare l'importazione e l'inizializzazione del repository
La pipeline inizia con un'istanza DesignComposeDefinition. Questo è il documento di progettazione Figma serializzato da DesignCompose in una struttura di buffer di protocollo.
Caricamento iniziale:all'avvio, il design principale (specificato dal nodo radice) viene convertito da
DesignComposeDefinitionin un alberoSquooshViewiniziale. Si tratta di un processo che viene eseguito una sola volta.Repository:
SquooshVariantRepositorygestisce le varianti dei componenti riutilizzabili e le visualizzazioni caricate inizialmente.Caricamento differito:per ridurre al minimo il tempo di avvio e la memoria utilizzata, le visualizzazioni aggiuntive (quelle che non fanno parte dell'albero del nodo radice iniziale) vengono caricate in modo differito dal documento solo quando vengono esplicitamente referenziate e sono necessarie per la logica di rendering (ad esempio, durante la personalizzazione di un elenco).
1.4 Documento personalizzazione
L'albero SquooshView viene attraversato per applicare lo stato dell'app dinamico:
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 corrente da sport a eco) in base alla logica di runtime.
Espansione elenco:un singolo elemento modello in Figma viene sostituito da un elenco dinamico di elementi secondari. Per questi bambini vengono generati nuovi ID univoci per verificare un'identità stabile per le animazioni.
Override di testo e stile:i contenuti di 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 a variabili (come colori o dimensioni) vengono sostituite con i 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, il wrapping e la formattazione del testo. In questo modo, puoi verificare che il testo scorra correttamente all'interno dei suoi vincoli prima del rendering.
1.7 Quadranti e indicatori
Questo è un passaggio specializzato per le UI automobilistiche.
MeterData: se un nodo contiene dati del contatore (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 iniziale e finale.
- Barre di avanzamento:la larghezza o l'altezza di un rettangolo viene scalata.
- Vettori di avanzamento:la lunghezza di un percorso vettoriale viene modificata.
1.8 Animazione
Differenza: l'attuale
SquooshViewviene confrontato conprevious_squoosh_viewdiPreRenderCache.Interpolazione: se le proprietà sono cambiate,
Squooshcrea interpolatori per eseguire 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 tracciati:convertiti in
DisplayListEntrycon la varianteDisplayListAppearanceappropriata (ad esempio,RectoPath) - Testo:convertito con
TextHelperin voci di disegno di testo. - Trasformazioni e ritagli:convertiti in coppie
PushTransform3DePopTransform3DoPushClipRegionePopClipRegionper gestire lo stack di stati di disegno. - Mascheratura:convertita in coppie
PushMaskLayerePopMaskLayerper creare e unire i livelli correttamente.
- Forme e tracciati:convertiti in
Il risultato finale è un'istanza di Vec<DisplayListEntry> che descrive cosa
disegnare, indipendentemente da come disegnarlo.
2.1 Handoff al looper
Dopo la generazione di DisplayList, il riduttore lo racchiude in un'istanza di
ViewDescriptor e lo invia tramite un canale MPSC Rust (LooperMessage) al
thread looper. Looper è responsabile delle fasi di rendering e visualizzazione,
il che impedisce al thread Reducer di bloccare la pipeline grafica.
Fase 3: rendering
DisplayList indipendente dalla piattaforma viene trasferito al backend di rendering,
dove i comandi astratti vengono tradotti in istruzioni per la GPU.
HAR utilizza Impeller, un motore di rendering originariamente creato per Flutter. Impeller è progettato per risolvere il problema dei problemi di frequenza fotogrammi dovuti alla compilazione degli shader precompilando un insieme piccolo ed efficiente di shader al momento del tempo di compilazione. Questo approccio, combinato con un batching efficace e un backend altamente ottimizzato, offre:
- Prestazioni deterministiche: elimina praticamente i problemi di compilazione degli shader in fase di runtime.
- Avvio rapido:riduce l'overhead di inizializzazione.
- Ingombro ridotto:produce un file binario compatto.
Per un'introduzione completa all'architettura di Impeller, guarda [Introducing Impeller - Flutter's new rendering engine][impeller-video]. 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 prerendering in comandi di rendering di Impeller.API Impeller Rust:esegue il wrapping della libreria Impeller per l'utilizzo in Rust (i crate
impellereimpeller-rs-bindgen).TypographyContext: gestisce la registrazione dei caratteri e la formattazione del testo.
3.1 Inizializzazione e gestione della superficie
Creazione del contesto:il renderer inizializza un'istanza di
impeller::Contextcon un backend OpenGL ES, passando un callback per risolvere i puntatori di funzione OpenGL ES dal contesto GL della piattaforma.Superficie FBO di wrapping:anziché creare una propria finestra, Impeller esegue il rendering in un oggetto framebuffer OpenGL (FBO) esistente fornito da Phase 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. Questi vengono caricati nelle texture della GPU e gestiti 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 videocamera e renderer 3D esterni) prevede il binding di istanze
EGLImageo di texture OpenGL esterne a oggetti ImpellerTextureper il rendering senza copia.
3.3 Render pass
Il ciclo render crea un'istanza DisplayList di Impeller (da non confondere con Vec<DisplayListEntry> generato dalla fase di prerendering) 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 inserire ed estrarre trasformazioni e regioni di ritaglio. - Primitive:
RecteRoundedRectvengono disegnati utilizzando operazioni di pittura standard. - Tracciati:vengono creati e disegnati tracciati vettoriali complessi (incluse le istanze
Arcdinamiche). - Testo:
TexteStyledTextvengono visualizzati 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()nel 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 e affidabile su Android Automotive OS (AAOS) Software-Defined Vehicle (SDV).
Il componente chiave di questa fase è HarDirectRenderingContext (nel crate
har-gl-context).
4.1 Architettura
Il livello di presentazione utilizza un approccio a doppio buffer con una destinazione di disegno off-screen:
Buffer di disegno:FBO off-screen in cui Impeller esegue il rendering della scena.
Risoluzione buffer (facoltativo): buffer ausiliario facoltativo per supportare l'anti-aliasing multisample (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 del blitting (trasferimento di blocchi di bit) al buffer di rendering.
Buffer di rendering:buffer generico supportato da un oggetto GBM, che corrisponde al back buffer in una tipica catena di scambio di grafica.
Buffer anteriore:buffer GBM scansionato sul display.
4.2 Catena di scambio
Quando viene chiamato swap_buffers, HAR segue questi passaggi:
Trasferisce i contenuti del buffer di disegno al buffer di rendering (con un trasferimento intermedio al buffer di risoluzione, se necessario per l'implementazione).
Chiama
glFlush()nel 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 con lo schermo. Questa richiesta contiene il descrittore di file (FD) della barriera della GPU (chiamata barriera interna) per impedire al controller di visualizzazione di mostrare il buffer di rendering prima che la GPU abbia finito di disegnare.
Richiede contemporaneamente una nuova recinzione dalla gestione dei diritti digitali (chiamata recinzione esterna) per segnalare quando il buffer precedente (il buffer anteriore 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 il nuovo out fence nel contesto in modo che HAR possa attendere che venga segnalato 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 Kernel Mode Setting (KMS) per configurare la risoluzione del display AAOS SDV, bypassando le interazioni con i gestori di finestre come SurfaceFlinger (in configurazioni specifiche), consentendo il controllo esclusivo e prioritario dell'hardware del display.
4.4 Rendering esterno
HAR supporta la delega del rendering di elementi UI specifici (identificati da tag in Figma) a processi o thread esterni. Questa funzionalità è utile per integrare scene 3D complesse (ad esempio, una visualizzazione dell'auto 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(TexturepiùEGLImage) per il doppio o il triplo buffering.SharedSurfaceExternalImage: un wrapper thread-safe per il passaggio di handleEGLImagetra il servizio esterno e il renderer principale.
4.4.2 Workflow
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) che esegue il rendering.Il servizio attende i segnali
RenderStartdal looper per allineare il rendering con il segnale VSYNC del display.Fuori dallo schermo, il servizio esegue il rendering dei contenuti in un framebuffer fornito da
SurfacePool.Il servizio chiama
swap_bufferssul suo contesto, che ruota il pool e rende il frame completato disponibile come istanza diSharedSurface.SharedSurfaceè 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 il
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)
A scopo di sviluppo e test, le app HAR possono essere destinate a ambienti desktop Linux standard e configurazioni headless. Queste piattaforme sono implementate nel
crate crates/reference/platforms/har-platform-linux.
A differenza della destinazione SDV AAOS di produzione, queste piattaforme non utilizzano il
sottosistema direct-rendering di har-gl-context per l'output del display. ma si basano su 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 gestione delle finestre.Modalità headless:utilizza il crate
har-gl-contextper creare un contesto pbuffer off-screen con il display EGL predefinito. Ciò consente il rendering in un buffer off-screen senza la necessità di una finestra visibile o di un accesso diretto all'hardware di visualizzazione, utilizzato principalmente per test automatizzati o elaborazione di backend.