Pipeline grafica HAR

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:

  1. Pre-rendering: elaborazione del grafico della scena, applicazione delle personalizzazioni e risoluzione del layout.
  2. Generazione di comandi:conversione del grafico della scena risolto in un elenco di visualizzazione indipendente dal backend.
  3. Rendering:esecuzione di comandi di disegno utilizzando il motore grafico Impeller.
  4. Presentazione:gestione dei framebuffer e sincronizzazione con l'hardware del display.

Flusso di grafica HAR

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 framework harry e un'implementazione di riferimento (UIModelPresenter) è fornita nella crate harry-app-core.
  • Modello UI: genera personalizzazioni in base allo stato attuale. Il codice del modello UI viene generato utilizzando la macro DesignDocument fornita dal crate derive_customizations. La struct UIModel nel crate harry-app-core fornisce un esempio.
  • Squoosh:fornisce la struttura dei dati SquooshView e il repository delle varianti, utilizzati per il rendering della UI in base al design. Un documento di progettazione serializzato viene caricato dalla crate dc_bundle dalla libreria DesignCompose e convertito in un albero di struct SquooshView per 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: Reducer riceve 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: Presenter esegue il mapping del nuovo stato in UIModel. 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 apply del modello UI per generare un insieme di istanze RenderCustomization. Si tratta di istruzioni esplicite per modificare il progetto Figma (ad esempio, "Imposta il testo del nodo #speed su "65 mph").
  • UpdatePolicy per l'ottimizzazione:dopo ogni passaggio di prerendering, viene restituito un valore UpdatePolicy, che indica quando è necessario il successivo aggiornamento del rendering. Se non sono in attesa modifiche dello stato e non sono in esecuzione animazioni, UpdatePolicy indica 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 DesignComposeDefinition in un albero SquooshView iniziale. Si tratta di un processo che viene eseguito una sola volta.

  • Repository:SquooshVariantRepository gestisce 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à SquooshView che 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:DynamicLayout calcola la posizione e le dimensioni finali (limiti) di ogni nodo nell'albero SquooshView.

  • Layout del testo:TextHelper utilizza un'implementazione del tratto LayoutHelper per 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 a meter_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 SquooshView viene confrontato con previous_squoosh_view di PreRenderCache.

  • Interpolazione: se le proprietà sono cambiate, Squoosh crea 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'albero SquooshView.

  • Traduzione:

    • Forme e tracciati:convertiti in DisplayListEntry con la variante DisplayListAppearance appropriata (ad esempio, Rect o Path)
    • Testo:convertito con TextHelper in voci di disegno di testo.
    • Trasformazioni e ritagli:convertiti in coppie PushTransform3D e PopTransform3D o PushClipRegion e PopClipRegion per gestire lo stack di stati di disegno.
    • Mascheratura:convertita in coppie PushMaskLayer e PopMaskLayer per creare e unire i livelli correttamente.

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 impeller e impeller-rs-bindgen).

  • TypographyContext: gestisce la registrazione dei caratteri e la formattazione del testo.

impeller-video

3.1 Inizializzazione e gestione della superficie

  • Creazione del contesto:il renderer inizializza un'istanza di impeller::Context con 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 Resources interna.

  • Caratteri:i caratteri TrueType e OpenType vengono caricati e registrati con TypographyContext per 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 EGLImage o di texture OpenGL esterne a oggetti Impeller Texture per 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:

  1. Cancella il buffer e applica le trasformazioni globali per la scalabilità DPI e la rotazione del display.

  2. Esegue l'iterazione degli elementi DisplayListEntry di input:

    • Stato:save() e restore() vengono utilizzati per inserire ed estrarre trasformazioni e regioni di ritaglio.
    • Primitive:Rect e RoundedRect vengono disegnati utilizzando operazioni di pittura standard.
    • Tracciati:vengono creati e disegnati tracciati vettoriali complessi (incluse le istanze Arc dinamiche).
    • Testo: Text e StyledText vengono visualizzati utilizzando TypographyContext.
    • Immagini: le immagini standard ed esterne vengono disegnate utilizzando draw_texture_rect.
  3. Invia l'elenco di visualizzazione Impeller creato alla superficie utilizzando surface.draw_display_list(), generando i comandi GL sottostanti.

  4. 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:

  1. Buffer di disegno:FBO off-screen in cui Impeller esegue il rendering della scena.

  2. 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.
  3. Buffer di rendering:buffer generico supportato da un oggetto GBM, che corrisponde al back buffer in una tipica catena di scambio di grafica.

  4. Buffer anteriore:buffer GBM scansionato sul display.

4.2 Catena di scambio

Quando viene chiamato swap_buffers, HAR segue questi passaggi:

  1. Trasferisce i contenuti del buffer di disegno al buffer di rendering (con un trasferimento intermedio al buffer di risoluzione, se necessario per l'implementazione).

  2. Chiama glFlush() nel contesto GL e crea un'istanza di EGL_SYNC_NATIVE_FENCE_ANDROID per monitorare il completamento della GPU.

  3. 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.

  4. 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.

  5. Esegue il commit della richiesta atomica utilizzando il flag non bloccante, per consentire al thread principale di continuare mentre i sottosistemi grafici rimangono sincronizzati.

  6. Memorizza il nuovo out fence nel contesto in modo che HAR possa attendere che venga segnalato all'inizio del processo swap_buffers nel 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 buffer LocalSurface (Texture più EGLImage) per il doppio o il triplo buffering.
  • SharedSurfaceExternalImage: un wrapper thread-safe per il passaggio di handle EGLImage tra il servizio esterno e il renderer principale.

4.4.2 Workflow

Il flusso di lavoro segue questa sequenza:

  1. Il servizio esterno viene avviato e registrato con il looper principale, identificando i tag Figma (ad esempio #cluster/3d-car) che esegue il rendering.

  2. Il servizio attende i segnali RenderStart dal looper per allineare il rendering con il segnale VSYNC del display.

  3. Fuori dallo schermo, il servizio esegue il rendering dei contenuti in un framebuffer fornito da SurfacePool.

  4. Il servizio chiama swap_buffers sul suo contesto, che ruota il pool e rende il frame completato disponibile come istanza di SharedSurface.

  5. SharedSurface è racchiuso in ExternalImage e inviato tramite un canale MPSC Rust al looper.

  6. Il renderer Impeller principale (fase 3) riceve l'immagine esterna. Anziché copiare i dati dei pixel, associa il EGLImage sottostante 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 winit per la gestione delle finestre e i loop di eventi e glutin per la creazione di contesti OpenGL ES e l'integrazione con il sistema di gestione delle finestre.

  • Modalità headless:utilizza il crate har-gl-context per 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.