Framework di sincronizzazione

Il framework di sincronizzazione descrive esplicitamente le dipendenze tra diverse operazioni asincrone nel sistema grafico Android. Il framework fornisce un'API che consente ai componenti di indicare quando vengono rilasciati i buffer. Il framework consente inoltre di passare primitive di sincronizzazione tra i driver dal kernel allo spazio utente e tra i processi dello spazio utente stessi.

Ad esempio, un'applicazione può mettere in coda il lavoro da eseguire nella GPU. La GPU inizia a disegnare l'immagine. Sebbene l'immagine non sia ancora stata disegnata in memoria, il puntatore del buffer viene passato al compositore della finestra insieme a una barriera che indica quando terminerà l'attività della GPU. Il compositore della finestra inizia l'elaborazione in anticipo e trasferisce il lavoro al controller del display. In modo simile, il lavoro della CPU viene svolto in anticipo. Al termine dell'operazione della GPU, il controller del display visualizza immediatamente l'immagine.

Il framework di sincronizzazione consente inoltre agli implementatori di sfruttare le risorse di sincronizzazione nei propri componenti hardware. Infine, il framework fornisce visibilità sulla pipeline grafica per facilitare il debug.

Sincronizzazione esplicita

La sincronizzazione esplicita consente ai producer e ai consumer di buffer grafici di segnalare quando hanno finito di utilizzare un buffer. La sincronizzazione esplicita viene implementata nello spazio del kernel.

I vantaggi della sincronizzazione esplicita includono:

  • Minore variazione del comportamento tra i dispositivi
  • Migliore supporto per il debug
  • Metriche di test migliorate

Il framework di sincronizzazione ha tre tipi di oggetti:

  • sync_timeline
  • sync_pt
  • sync_fence

sync_timeline

sync_timeline è una sequenza temporale che aumenta in modo monotono e che i fornitori devono implementare per ogni istanza del driver, ad esempio un contesto GL, un controller di visualizzazione o un blitter 2D. sync_timeline conteggia i job inviati al kernel per un particolare componente hardware. sync_timeline fornisce garanzie sull'ordine delle operazioni e consente implementazioni specifiche per l'hardware.

Segui queste linee guida durante l'implementazione di sync_timeline:

  • Fornisci nomi utili per tutti i conducenti, le tempistiche e i recinti per semplificare il debug.
  • Implementa gli operatori timeline_value_str e pt_value_str nelle sequenze temporali per rendere l'output di debug più leggibile.
  • Implementa il riempimento driver_data per consentire alle librerie di spazio utente, come la libreria GL, di accedere ai dati della cronologia privati, se vuoi. data_driver consente ai fornitori di trasmettere informazioni su sync_fence e sync_pts per creare righe di comando in base a queste informazioni.
  • Non consentire allo spazio utente di creare o segnalare esplicitamente una recinzione. La creazione esplicita di segnali/recinzioni comporta un attacco Denial of Service che interrompe la funzionalità della pipeline.
  • Non accedere esplicitamente agli elementi sync_timeline, sync_pt o sync_fence. L'API fornisce tutte le funzioni richieste.

sync_pt

sync_pt è un singolo valore o punto su un sync_timeline. Un punto ha tre stati: attivo, segnalato ed errore. I punti iniziano nello stato attivo e passano agli stati segnalato o errore. Ad esempio, quando un consumatore di immagini non ha più bisogno di un buffer, viene segnalato un sync_pt in modo che un produttore di immagini sappia che può scrivere di nuovo nel buffer.

sync_fence

sync_fence è una raccolta di valori sync_pt che spesso hanno sync_timeline genitori diversi (ad esempio per il controller del display e la GPU). sync_fence, sync_pt e sync_timeline sono le primitive principali che i driver e lo spazio utente utilizzano per comunicare le proprie dipendenze. Quando una barriera viene segnalata, tutti i comandi emessi prima della barriera vengono completati perché il driver del kernel o il blocco hardware esegue i comandi in ordine.

Il framework di sincronizzazione consente a più consumatori o produttori di segnalare quando hanno terminato di utilizzare un buffer, comunicando le informazioni sulle dipendenze con un parametro di funzione. Le recinzioni sono supportate da un descrittore di file e vengono passate dallo spazio del kernel allo spazio utente. Ad esempio, una barriera può contenere due valori sync_pt che indicano quando due consumatori di immagini separati hanno finito di leggere un buffer. Quando viene segnalata la barriera, i produttori di immagini sanno che entrambi i consumatori hanno terminato la visione.

Le recinzioni, come i valori sync_pt, iniziano attive e cambiano stato in base allo stato dei loro punti. Se tutti i valori di sync_pt vengono segnalati, viene segnalato anche sync_fence. Se uno dei sync_pt entra in uno stato di errore, l'intero sync_fence si trova in uno stato di errore.

L'iscrizione a un sync_fence è immutabile dopo la creazione del recinto. Per ottenere più di un punto in un recinto, viene eseguita un'unione in cui i punti di due recinti distinti vengono aggiunti a un terzo recinto. Se uno di questi punti è stato segnalato nella recinzione di origine e l'altro no, anche la terza recinzione non sarà in uno stato segnalato.

Per implementare la sincronizzazione esplicita, fornisci quanto segue:

  • Un sottosistema dello spazio kernel che implementa il framework di sincronizzazione per un particolare driver hardware. I driver che devono essere fence-aware sono generalmente tutto ciò che accede o comunica con Hardware Composer. I file chiave includono:
    • Implementazione di base:
      • kernel/common/include/linux/sync.h
      • kernel/common/drivers/base/sync.c
    • Documentazione all'indirizzo kernel/common/Documentation/sync.txt
    • Libreria per comunicare con lo spazio del kernel in platform/system/core/libsync
  • Il fornitore deve fornire le barriere di sincronizzazione appropriate come parametri delle funzioni validateDisplay() e presentDisplay() nell'HAL.
  • Due estensioni GL correlate alle recinzioni (EGL_ANDROID_native_fence_sync e EGL_ANDROID_wait_sync) e supporto delle recinzioni nel driver grafico.

Case study: implementare un driver di visualizzazione

Per utilizzare l'API che supporta la funzione di sincronizzazione, sviluppa un driver di visualizzazione con una funzione di buffer di visualizzazione. Prima dell'esistenza del framework di sincronizzazione, questa funzione riceveva oggetti dma-buf, li inseriva nel buffer di visualizzazione e bloccava la visualizzazione del buffer. Per esempio:

/*
 * assumes buffer is ready to be displayed.  returns when buffer is no longer on
 * screen.
 */
void display_buffer(struct dma_buf *buffer);

Con il framework di sincronizzazione, la funzione display_buffer è più complessa. Quando viene visualizzato un buffer, questo viene associato a una recinzione che indica quando sarà pronto. Puoi mettere in coda e iniziare il lavoro dopo che la recinzione è stata eliminata.

La messa in coda e l'avvio del lavoro dopo che la barriera è stata superata non bloccano nulla. Restituisci immediatamente la tua recinzione, che garantisce quando il buffer non sarà più visualizzato. Man mano che metti in coda i buffer, il kernel elenca le dipendenze con il framework di sincronizzazione:

/*
 * displays buffer when fence is signaled.  returns immediately with a fence
 * that signals when buffer is no longer displayed.
 */
struct sync_fence* display_buffer(struct dma_buf *buffer, struct sync_fence
*fence);

Integrazione della sincronizzazione

Questa sezione spiega come integrare il framework di sincronizzazione dello spazio del kernel con le parti dello spazio utente del framework Android e i driver che devono comunicare tra loro. Gli oggetti dello spazio del kernel sono rappresentati come descrittori di file nello spazio utente.

Convenzioni di integrazione

Segui le convenzioni dell'interfaccia HAL di Android:

  • Se l'API fornisce un descrittore di file che fa riferimento a un sync_pt, il driver del fornitore o l'HAL che utilizza l'API deve chiudere il descrittore di file.
  • Se il driver del fornitore o l'HAL passa un descrittore di file che contiene un sync_pt a una funzione API, il driver del fornitore o l'HAL non deve chiudere il descrittore di file.
  • Per continuare a utilizzare il descrittore del file di recinzione, il driver del fornitore o l'HAL deve duplicare il descrittore.

Un oggetto fence viene rinominato ogni volta che passa attraverso BufferQueue. Il supporto delle barriere del kernel consente alle barriere di avere stringhe per i nomi, quindi il framework di sincronizzazione utilizza il nome della finestra e l'indice del buffer in coda per denominare la barriera, ad esempio SurfaceView:0. Questo è utile per il debug per identificare l'origine di un deadlock, poiché i nomi vengono visualizzati nell'output di /d/sync e nei report sui bug.

Integrazione di ANativeWindow

ANativeWindow è compatibile con Fences. dequeueBuffer, queueBuffer e cancelBuffer hanno parametri di recinzione.

Integrazione OpenGL ES

L'integrazione della sincronizzazione OpenGL ES si basa su due estensioni EGL:

  • EGL_ANDROID_native_fence_sync fornisce un modo per incorporare o creare descrittori di file di recinzione nativi di Android negli oggetti EGLSyncKHR.
  • EGL_ANDROID_wait_sync consente gli stalli lato GPU anziché lato CPU, facendo attendere la GPU per EGLSyncKHR. L'estensione EGL_ANDROID_wait_sync è uguale all'estensione EGL_KHR_wait_sync.

Per utilizzare queste estensioni in modo indipendente, implementa l'estensione EGL_ANDROID_native_fence_sync insieme al supporto del kernel associato. Successivamente, attiva l'estensione EGL_ANDROID_wait_sync nel driver. L'estensione EGL_ANDROID_native_fence_sync è costituita da un tipo di oggetto EGLSyncKHR di recinzione nativa distinto. Di conseguenza, le estensioni che si applicano ai tipi di oggetti EGLSyncKHR esistenti non si applicano necessariamente agli oggetti EGL_ANDROID_native_fence, evitando interazioni indesiderate.

L'estensione EGL_ANDROID_native_fence_sync utilizza un attributo descrittore di recinzione nativo corrispondente che può essere impostato solo al momento della creazione e non può essere interrogato direttamente da un oggetto di sincronizzazione esistente. Questo attributo può essere impostato su una delle due modalità:

  • Un descrittore di file di recinzione valido racchiude un descrittore di file di recinzione Android nativo esistente in un oggetto EGLSyncKHR.
  • -1 crea un descrittore di file di recinzione Android nativo da un oggetto EGLSyncKHR.

Utilizza la chiamata di funzione DupNativeFenceFD() per estrarre l'oggetto EGLSyncKHR dal descrittore del file di recinzione nativo di Android. Il risultato è lo stesso di una query sull'attributo set, ma rispetta la convenzione secondo cui il destinatario chiude la recinzione (da cui l'operazione duplicata). Infine, la distruzione dell'oggetto EGLSyncKHR chiude l'attributo barriera interna.

Integrazione di Hardware Composer

Hardware Composer gestisce tre tipi di barriere di sincronizzazione:

  • Acquire fences vengono passati insieme ai buffer di input alle chiamate setLayerBuffer e setClientTarget. Questi rappresentano una scrittura in attesa nel buffer e devono segnalare prima che SurfaceFlinger o HWC tentino di leggere dal buffer associato per eseguire la composizione.
  • I recinti di rilascio vengono recuperati dopo la chiamata a presentDisplay utilizzando la chiamata getReleaseFences. Rappresentano una lettura in attesa dal buffer precedente sullo stesso livello. Un release fence segnala quando l'HWC non utilizza più il buffer precedente perché il buffer corrente ha sostituito quello precedente sul display. Le barriere di rilascio vengono restituite all'app insieme ai buffer precedenti che verranno sostituiti durante la composizione corrente. L'app deve attendere i segnali di rilascio prima di scrivere nuovi contenuti nel buffer che le è stato restituito.
  • Le recinzioni presenti vengono restituite, una per frame, nell'ambito della chiamata a presentDisplay. Le recinzioni attuali rappresentano il momento in cui la composizione di questo frame è stata completata oppure, in alternativa, il momento in cui il risultato della composizione del frame precedente non è più necessario. Per i display fisici, presentDisplay restituisce le recinzioni presenti quando il frame corrente viene visualizzato sullo schermo. Dopo che le recinzioni presenti sono state restituite, è possibile scrivere di nuovo nel buffer di destinazione di SurfaceFlinger, se applicabile. Per i display virtuali, le recinzioni presenti vengono restituite quando è sicuro leggere dal buffer di output.