Framework di sincronizzazione

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

Ad esempio, un'applicazione potrebbe mettere in coda il lavoro da eseguire nella GPU. La GPU inizia a disegnare l'immagine. Anche se l'immagine non è stata ancora disegnata nella memoria, il puntatore del buffer viene passato al compositore della finestra insieme a una recinzione che indica quando verrà completata l'attività della GPU. Il compositore della finestra avvia l'elaborazione in anticipo e gira il lavoro al controller del display. In modo simile, il lavoro della CPU viene eseguito in anticipo. Al termine della GPU, il controller 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 debugging.

Sincronizzazione esplicita

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

I vantaggi della sincronizzazione esplicita includono:

  • Meno variazioni di 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 in aumento monotonico che i fornitori devono implementare per ogni istanza del driver, ad esempio un contesto GL, un controller del display o un blitter 2D. sync_timeline conteggia i job inviati al kernel per un determinato hardware. sync_timeline fornisce garanzie sull'ordine delle operazioni e consente implementazioni specifiche per hardware.

Segui queste linee guida durante l'implementazione di sync_timeline:

  • Fornisci nomi utili per tutti i driver, le tempistiche e le recinti per semplificare il debugging.
  • Implementa gli operatori timeline_value_str e pt_value_str nelle sequenze temporali per rendere più leggibile l'output di debug.
  • Implementa il riempimento driver_data per concedere alle librerie dello spazio utente, come la libreria GL, l'accesso ai dati della cronologia privata, se vuoi. data_driver consente ai fornitori di passare informazioni sugli immutabili sync_fence e sync_pts per creare righe di comando basate su queste.
  • Non consentire allo spazio utente di creare o segnalare esplicitamente un recinto. La creazione esplicita di indicatori/recinzioni determina 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.

sincronizzazione_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 segnalati o di 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 principali diversi (ad esempio per il controllore dell'unità di visualizzazione e la GPU). sync_fence, sync_pt e sync_timeline sono le primitive principali utilizzate dai driver e dallo spazio utente per comunicare le loro dipendenze. Quando viene segnalato un recinto, tutti i comandi emessi prima del recinto sono garantiti come completi 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 recinti sono supportati da un descrittore file e vengono passati dallo spazio kernel allo spazio utente. Ad esempio, una recinto può contenere due valori sync_pt che indicano quando due consumer di immagini separati hanno finito di leggere un buffer. Quando viene segnalato il recinto, i produttori di immagini sanno che entrambi i consumatori hanno terminato di consumare.

I recinti, come i valori sync_pt, iniziano attivi e cambiano stato in base allo stato dei relativi punti. Se tutti i valori sync_pt vengono segnalati, viene segnalato anche sync_fence. Se un sync_pt assume uno stato di errore, l'intero sync_fence ha uno stato di errore.

L'appartenenza 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 nel recinto di origine e l'altro no, anche il terzo recinto non sarà in uno stato segnalato.

Per implementare la sincronizzazione esplicita, fornisci quanto segue:

  • Un sottosistema nello spazio del kernel che implementa il framework di sincronizzazione per un determinato driver hardware. I driver che devono essere compatibili con la recinzione sono generalmente tutti i componenti che accedono o comunicano con il compositore hardware. I file principali 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 alle funzioni validateDisplay() e presentDisplay() nell'HAL.
  • Due estensioni GL relative ai recinti (EGL_ANDROID_native_fence_sync e EGL_ANDROID_wait_sync) e il supporto dei recinti nel driver grafico.

Case study: implementazione di 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, inseriva i buffer sul display e si bloccava mentre il buffer era visibile. 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 un recinto che indica quando sarà pronto. Puoi mettere in coda e iniziare il lavoro dopo che il recinto si è risolto.

L'inserimento in coda e l'avvio del lavoro dopo il completamento del recinto non bloccano nulla. Restituirai immediatamente la tua recinzione, che garantisce quando il buffer sarà spento sul display. Quando 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 nello spazio 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 dei file nello spazio utente.

Convenzioni di integrazione

Segui le convenzioni dell'interfaccia HAL di Android:

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

Un oggetto recinto viene rinominato ogni volta che passa attraverso BufferQueue. Il supporto delle barriere del kernel consente alle barriere di avere stringhe per i nomi, pertanto il framework di sincronizzazione utilizza il nome della finestra e l'indice del buffer in coda per assegnare un nome alla barriera, ad esempio SurfaceView:0. Ciò è utile per il debug e l'identificazione dell'origine di un deadlock come i nomi visualizzati nell'output di /d/sync e nelle segnalazioni di bug.

Integrazione di ANativeWindow

ANativeWindow è consapevole della recinzione. 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 eseguire il wrapping o creare descrittori file di recinti Android nativi negli oggetti EGLSyncKHR.
  • EGL_ANDROID_wait_sync consente interruzioni sul lato GPU piuttosto che sul 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. Poi, attiva l'EGL_ANDROID_wait_sync estensione nel driver. L'estensione EGL_ANDROID_native_fence_sync è costituita da un distinto tipo di oggetto recinto nativo EGLSyncKHR. 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 file di recinzione nativo corrispondente che può essere impostato solo al momento della creazione e su cui non è possibile eseguire query direttamente da un oggetto di sincronizzazione esistente. Questo attributo può essere impostato su una di due modalità:

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

Utilizza la chiamata di funzione DupNativeFenceFD() per estrarre l'oggetto EGLSyncKHR dal descrittore file di recinzione Android nativo. Questa operazione ha lo stesso risultato dell'esecuzione di una query sull'attributo impostato, ma è conforme alla convenzione secondo cui il destinatario chiude il recinto (da cui l'operazione di duplicazione). Infine, l'eliminazione dell'oggetto EGLSyncKHR chiude l'attributo recinto interno.

Integrazione di Hardware Composer

Il compositore hardware gestisce tre tipi di barriere di sincronizzazione:

  • I limiti di acquisizione vengono passati insieme ai buffer di input alle chiamate setLayerBuffer e setClientTarget. Questi rappresentano una scrittura in attesa nel buffer e devono essere segnalati prima che SurfaceFlinger o l'HWC tenti di leggere dal buffer associato per eseguire la composizione.
  • I sistemi di rilascio vengono recuperati dopo la chiamata a presentDisplay tramite la chiamata getReleaseFences. Questi rappresentano una lettura in attesa dal buffer precedente sullo stesso livello. Un confine di rilascio indica quando l'HWC non utilizza più il buffer precedente perché il buffer corrente ha sostituito quello precedente sul display. Le barriere di rilascio vengono ritrasmesse all'app insieme ai buffer precedenti che verranno sostituiti durante la composizione corrente. L'app deve attendere che un interruttore di rilascio indichi che è possibile scrivere nuovi contenuti nel buffer che è stato restituito.
  • I limiti di presentazione vengono restituiti, uno per frame, nell'ambito della chiamata a presentDisplay. I recinti attuali indicano quando la composizione di questo frame è stata completata oppure quando il risultato della composizione del frame precedente non è più necessario. Per i display fisici, presentDisplay restituisce le recinti presenti quando il frame corrente viene visualizzato sullo schermo. Dopo che vengono restituiti i recinti attuali, è possibile scrivere di nuovo nel buffer di destinazione di SurfaceFlinger, se applicabile. Per i display virtuali, i recinti attuali vengono restituiti quando è possibile leggere dal buffer di output.