2014-10-11 18 views
20

A seguito di alcune altre domande su Stack Overflow, ho letto la guida alle parti interne del Android superfici, SurfaceViews, ecc da qui:Minimizzare Android GLSurfaceView lag

https://source.android.com/devices/graphics/architecture.html

Quella guida mi ha dato una gran una migliore comprensione di come tutti i diversi pezzi combaciano su Android. Spiega come eglSwapBuffers spinge semplicemente il frame renderizzato in una coda che verrà successivamente utilizzata da SurfaceFlinger quando prepara il prossimo frame per la visualizzazione. Se la coda è piena, allora aspetterà che un buffer diventi disponibile per il frame successivo prima di tornare. Il documento sopra lo descrive come "stuffing the queue" e si basa sulla "back-pressure" dei buffer di swap per limitare il rendering al vsync del display. Questo è ciò che accade usando la modalità di rendering continuo di default di GLSurfaceView.

Se il rendering è semplice e completo in molto meno del periodo di frame, l'effetto negativo di questo è un ritardo aggiuntivo causato da BufferQueue, poiché l'attesa su SwapBuffers non si verifica fino a quando la coda non è piena, e quindi il fotogramma che rendiamo è sempre destinato ad essere nella parte posteriore della coda e quindi non verrà visualizzato immediatamente sul prossimo vsync in quanto vi sono probabilmente dei buffer prima di esso nella coda.

In contrasto, il rendering su richiesta si verifica in genere molto meno frequentemente rispetto alla velocità di aggiornamento dello schermo, quindi in genere le BufferQueues per quelle viste sono vuote e pertanto gli eventuali aggiornamenti inseriti in tali code verranno afferrati da SurfaceFlinger sul successivo vsync .

Quindi, ecco la domanda: come posso impostare un renderer continuo, ma con un ritardo minimo? L'obiettivo è che la coda del buffer sia vuota all'inizio di ogni vsync, eseguo il rendering del contenuto in under 16ms, lo spingo in coda (buffer count = 1), e viene quindi consumato da SurfaceFlinger sul successivo vsync (conteggio buffer = 0), ripetere. Il numero di Buffer nella coda può essere visto in systrace, quindi l'obiettivo è di avere questa alternativa tra 0 e 1.

Il documento che ho menzionato sopra introduce Choreographer come modo per ottenere le callback su ogni vsync. Tuttavia non sono convinto che sia abbastanza per essere in grado di ottenere il minimo ritardo di comportamento che sto cercando. Ho provato a fare un requestRender() su un callback vsync con un minimo onDrawFrame() e in effetti mostra il comportamento del conteggio del buffer 0/1. Tuttavia, cosa succederebbe se SurfaceFlinger non fosse in grado di svolgere tutta la sua attività in un singolo periodo di fotogrammi (forse una notifica si apre o qualsiasi altra cosa)? In tal caso, mi aspetto che il mio renderer produrrà felicemente 1 frame per vsync, ma l'end-consumer di quel BufferQueue ha lasciato cadere un frame. Risultato: ora stiamo alternando tra 1 e 2 buffer nella nostra coda, e abbiamo ottenuto un frame di ritardo tra il rendering e la visualizzazione del frame.

Il documento sembra suggerire l'osservazione dell'offset orario tra l'ora vsync segnalata e quando viene eseguito il callback. Riesco a vedere come ciò può essere d'aiuto se il tuo callback è consegnato in ritardo a causa del tuo thread principale a causa di un passaggio di layout o qualcosa del genere. Tuttavia, non penso che consentirebbe il rilevamento di SurfaceFlinger saltando un battito e non riuscendo a consumare un frame. C'è un modo in cui l'app può capire che SurfaceFlinger ha lasciato cadere un frame? Sembra anche che l'incapacità di dire alla lunghezza della coda interrompa l'idea di usare il tempo vsync per gli aggiornamenti dello stato del gioco, poiché c'è un numero sconosciuto di frame nella coda prima che quello che si sta visualizzando verrà effettivamente visualizzato.

Ridurre la lunghezza massima della coda e fare affidamento sulla contropressione sarebbe un modo per ottenere ciò, ma non penso che ci sia un'API per impostare il numero massimo di buffer in GLSurfaceView BufferQueue?

+1

Questa è una grande domanda! –

+0

Buona domanda! –

risposta

18

Ottima domanda.

po 'veloce di sfondo per chiunque altro leggendo questo:

L'obiettivo è quello di ridurre al minimo la latenza del display, vale a dire il tempo che intercorre tra quando l'applicazione esegue il rendering di un fotogramma e quando il pannello del display si illumina i pixel. Se stai solo lanciando contenuti sullo schermo, non importa, perché l'utente non può dire la differenza. Se stai rispondendo all'input tattile, però, ogni fotogramma di latenza rende la tua app un po 'meno reattiva.

Il problema è simile alla sincronizzazione A/V, in cui è necessario l'audio associato a una cornice per far uscire l'altoparlante mentre il frame video viene visualizzato sullo schermo. In tal caso, la latenza complessiva non è importante a patto che sia costantemente uguale su entrambe le uscite audio e video. Tuttavia, ci sono problemi molto simili, perché perdi la sincronizzazione se SurfaceFlinger si blocca e il tuo video viene visualizzato in modo coerente un fotogramma successivo.

SurfaceFlinger viene eseguito con priorità elevata e svolge relativamente poco lavoro, quindi è improbabile che possa perdere un colpo da solo ... ma può succedere. Inoltre, compositing frame da più fonti, alcuni dei quali usano recinzioni per segnalare il completamento asincrono. Se un frame video in tempo reale è composto con l'output OpenGL e il rendering GLES non è completato quando arriva la deadline, l'intera composizione verrà posticipata al successivo VSYNC.

Il desiderio di ridurre al minimo la latenza è stato abbastanza forte che la versione Android KitKat (4.4) ha introdotto la funzione "DispSync" in SurfaceFlinger, che radono mezzo fotogramma di latenza rispetto al solito ritardo a due fotogrammi. (Questo è brevemente menzionato nel documento dell'architettura grafica, ma non è molto diffuso.)

Quindi questa è la situazione. In passato questo era meno di un problema per il video, perché il video a 30 fps aggiorna l'altro fotogramma. I singhiozzi si risolvono naturalmente perché non stiamo cercando di mantenere la coda piena. Stiamo iniziando a vedere video a 48Hz e 60Hz, quindi questo è più importante.

La domanda è: come possiamo rilevare se i frame che inviamo a SurfaceFlinger vengono visualizzati al più presto possibile, o stiamo spendendo un frame extra in attesa dietro un buffer che abbiamo inviato in precedenza?

La prima parte della risposta è: non è possibile. Non ci sono query di stato o richiamate su SurfaceFlinger che ti diranno qual è il suo stato. In teoria è possibile interrogare lo stesso BufferQueue, ma questo non ti dirà necessariamente quello che devi sapere.

Il problema con le query e callback è che non si può dire ciò che lo stato è, solo ciò che lo Stato era . Nel momento in cui l'app riceve le informazioni e agisce su di esse, la situazione potrebbe essere completamente diversa. L'app verrà eseguita con priorità normale, pertanto è soggetta a ritardi.

Per la sincronizzazione A/V è leggermente più complicato, perché l'app non può conoscere le caratteristiche del display. Ad esempio, alcuni display hanno "pannelli intelligenti" con memoria incorporata. (Se ciò che appare sullo schermo non si aggiorna spesso, è possibile risparmiare molta energia non avendo il pannello scansionato i pixel attraverso il bus di memoria 60x al secondo.) Questi possono aggiungere un ulteriore frame di latenza che deve essere tenuto in considerazione.

La soluzione in cui Android si sta muovendo verso la sincronizzazione A/V deve fare in modo che l'app comunichi SurfaceFlinger quando desidera che venga visualizzata la cornice. Se SurfaceFlinger salta la scadenza, abbassa la cornice. Questo è stato aggiunto sperimentalmente in 4.4, anche se non è destinato a essere usato fino alla prossima release (dovrebbe funzionare abbastanza bene in "L preview", anche se non so se questo include tutti i pezzi necessari per usarlo completamente).

Il modo in cui un'applicazione utilizza questo è chiamare l'estensione eglPresentationTimeANDROID() prima di eglSwapBuffers(). L'argomento della funzione è il tempo di presentazione desiderato, in nanosecondi, utilizzando la stessa base dei tempi di Choreographer (in particolare, Linux CLOCK_MONOTONIC). Quindi, per ogni fotogramma, si prende il timestamp ottenuto dal coreografo, si aggiunge il numero desiderato di fotogrammi moltiplicato per la frequenza di aggiornamento approssimativa (che è possibile ottenere interrogando l'oggetto Visualizza - vedere MiscUtils#getDisplayRefreshNsec()) e passarlo a EGL. Quando si scambiano i buffer, il tempo di presentazione desiderato viene passato insieme al buffer.

Ricordare che SurfaceFlinger si riattiva una volta per VSYNC, esamina la raccolta di buffer in sospeso e consegna un set all'hardware di visualizzazione tramite Hardware Composer. Se si richiede la visualizzazione all'ora T e SurfaceFlinger crede che un frame passato all'hardware del display verrà visualizzato al tempo T-1 o precedente, il fotogramma verrà tenuto premuto (e il fotogramma precedente verrà nuovamente visualizzato). Se la cornice apparirà al tempo T, verrà inviata al display. Se il frame apparirà al tempo T + 1 o successivo (cioè mancherà alla sua scadenza), e c'è un altro frame dietro di esso nella coda che è pianificata per un secondo momento (es. Il frame inteso per il tempo T + 1) , quindi il frame inteso per il tempo T verrà abbandonato.

La soluzione non si adatta perfettamente al tuo problema. Per la sincronizzazione A/V, è necessaria una latenza costante, non una latenza minima. Se osservi l'attività "scheduled swap" di Grafika puoi trovare del codice che utilizza eglPresentationTimeANDROID() in un modo simile a quello che farebbe un video player. (Nel suo stato attuale è poco più di un "generatore di suoni" per la creazione dell'output di systrace, ma i pezzi di base sono lì.) La strategia è di renderizzare alcuni fotogrammi in anticipo, quindi SurfaceFlinger non si esaurisce mai, ma è esattamente sbagliato app.

Il meccanismo di presentazione, tuttavia, fornisce un modo per rilasciare i frame anziché consentire loro di eseguire il backup. Se ti capita di sapere che ci sono due frame di latenza tra il tempo riportato da Choreographer e il momento in cui il tuo frame può essere visualizzato, puoi usare questa funzione per assicurarti che i frame vengano rilasciati piuttosto che in coda se sono troppo lontani passato. L'attività Grafika consente di impostare la frequenza dei fotogrammi e la latenza richiesta, quindi visualizzare i risultati in systrace.

Sarebbe utile per un'applicazione sapere quanti frame di latenza SurfaceFlinger ha effettivamente, ma non c'è una query per quello. (Questo è piuttosto difficile da gestire in ogni caso, poiché i "pannelli intelligenti" possono cambiare modalità, cambiando la latenza del display, ma a meno che non si stia lavorando alla sincronizzazione A/V, tutto ciò che interessa davvero è ridurre al minimo la latenza di SurfaceFlinger.) ragionevolmente sicuro di assumere due frame su 4.3+. Se non sono due fotogrammi, potresti avere prestazioni non ottimali, ma l'effetto netto non sarà peggiore di quello che otterresti se non impostassi affatto il tempo di presentazione.

Si potrebbe provare a impostare il tempo di presentazione desiderato uguale al timestamp di Choreographer; un timestamp nel passato recente significa "mostra ASAP". Questo garantisce una latenza minima, ma può ritorcersi contro la scorrevolezza. SurfaceFlinger ha il ritardo di due fotogrammi perché dà a tutto il sistema tempo sufficiente per completare il lavoro. Se il carico di lavoro è irregolare, oscillerai tra la latenza a fotogramma singolo e doppio fotogramma e l'uscita apparirà janky alle transizioni. (Questo era un problema per DispSync, che riduce il tempo totale a 1,5 frame.)

Non ricordo quando è stata aggiunta la funzione eglPresentationTimeANDROID(), ma nelle versioni precedenti dovrebbe essere un no-op.

Riga inferiore: per "L" e in parte 4.4, dovresti essere in grado di ottenere il comportamento desiderato usando l'estensione EGL con due frame di latenza. Nelle versioni precedenti non c'è aiuto dal sistema. Se vuoi assicurarti che non ci sia un buffer sulla tua strada, puoi lasciar cadere deliberatamente un frame ogni tanto per lasciare che la coda del buffer si esaurisca.

Aggiornamento: un modo per evitare l'accodamento dei frame è chiamare eglSwapInterval(0). Se si inviava l'output direttamente a un display, la chiamata disabilitava la sincronizzazione con VSYNC, annullando la limitazione della frequenza fotogrammi dell'applicazione. Quando si esegue il rendering tramite SurfaceFlinger, questo pone il BufferQueue in "modalità asincrona", che provoca la caduta dei fotogrammi se inviati più velocemente di quanto il sistema possa visualizzarli.

Nota: il buffer è ancora triplo: viene visualizzato un buffer, uno è trattenuto da SurfaceFlinger per essere visualizzato sul successivo capovolgimento e uno viene prelevato dall'applicazione.

+1

Grazie per la risposta dettagliata. Avere SurfaceFlinger rilasciare il frame se ce n'è un altro in coda sembra il comportamento giusto per me, e impostare il tempo di presentazione sul tempo del vsync suona come un modo ragionevole per raggiungere questo obiettivo. – tangobravo

+0

Seguito da altri punti: il "Pointer Trail" diventa piuttosto lento con lunghi percorsi sul mio Moto G su 4.4, e sembra che sia all'interno di SurfaceFlinger che fa il disegno, quindi è un modo semplice per riprodurre il comportamento. Nell'argomento smoothness, non appena non è possibile eseguire il rendering su 60FPS, sei destinato a mostrare alcuni fotogrammi due volte, il che causerà una jank visibile. In tal caso è probabilmente meglio scendere a 30 FPS e iniziare a eseguire rendering ogni altro vsync. Se il tuo rendering impiega meno di 16ms, il riempimento della coda completa acquista un periodo di frame extra per recuperare da eventuali picchi. – tangobravo

+0

Quindi direi che il miglior consiglio del circuito di gioco è di scegliere tra "riempire la coda" per comprarti un periodo di frame extra per gestire picchi ma con un ulteriore frame di latenza, o per andare con un rendering innescato da coreografo con un tempo di presentazione per una latenza minima. Pensieri? – tangobravo