2014-10-11 110 views
20

繼Stack Overflow上的一些其他問題,我已經在這裏閱讀指導到Android的表面,SurfaceViews等的內部:最小化的Android GLSurfaceView滯後

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

該指南給了我一個很大進一步理解Android上所有不同部分如何組合在一起。它涵蓋了eglSwapBuffers如何將渲染幀推入一個隊列,稍後在準備顯示下一幀時由SurfaceFlinger使用。如果隊列已滿,那麼它將等待,直到下一幀的緩衝區可用,然後再返回。上面的文檔將此描述爲「填充隊列」並依靠交換緩衝區的「背壓」將渲染限制爲顯示的vsync。這是使用GLSurfaceView的默認連續渲染模式發生的情況。

如果您的渲染比較簡單,並且完成時間遠遠少於幀週期,則負面影響是BufferQueue引起的額外延遲,因爲SwapBuffers上的等待直到隊列滿了纔會發生,因此我們渲染的幀始終註定要位於隊列的後面,因此不會立即顯示在下一個vsync上,因爲隊列中可能存在緩衝區。

相比之下,按需渲染通常比顯示更新頻率發生得少得多,所以通常這些視圖的BufferQueues是空的,因此推入這些隊列的所有更新都將由SurfaceFlinger在下一個vsync中抓取。

所以,這裏是一個問題:我如何設置一個連續的渲染器,但延遲最小?我們的目標是在每個vsync開始時緩衝區隊列是空的,我在16ms內渲染我的內容,將它推送到隊列(緩衝區計數= 1),然後SurfaceFlinger在下一個vsync(緩衝區計數) = 0),重複。隊列中緩衝區的數量可以在systrace中看到,因此我們的目標是在0和1之間交替。

上面提到的文檔介紹了Choreographer作爲獲取每個vsync的回調方式。然而,我不相信這足以實現我追求的最小滯後行爲。我已經測試了一個使用非常小的onDrawFrame()進行vsync回調的requestRender(),它確實表現出0/1緩衝區計數行爲。然而,如果SurfaceFlinger無法在單幀時間內完成所有工作(可能是通知彈出或其他)?在這種情況下,我希望我的渲染器會很高興地爲每個vsync生成1幀,但是BufferQueue的消費者結束了一幀。結果:我們現在在隊列中交替使用1到2個緩衝區,並且在渲染和查看幀之間獲得了一段時滯。

該文檔似乎建議查看報告的vsync時間與回調運行時間之間的時間偏移。我可以看到如果您的回調由於佈局過期或其他原因而由於主線程提交延遲而可能會有所幫助。但我不認爲這將允許檢測SurfaceFlinger跳過節拍並且不能消耗幀。有沒有什麼方法可以解決SurfaceFlinger放棄框架的問題?似乎無法判斷隊列的長度是否會破壞將vsync時間用於遊戲狀態更新的想法,因爲在渲染實際顯示的隊列之前隊列中存在未知數量的幀。

減少隊列的最大長度並依賴背壓將是實現此目的的一種方式,但我不認爲有一個API來設置GLSurfaceView BufferQueue中的最大緩衝區數量?

+1

這是一個很棒的問題! – 2014-11-24 17:11:23

+0

好問題! – 2015-07-26 02:10:21

回答

18

偉大的問題。背景的其他任何人閱讀本

快速位:

這裏的目標是最小化顯示等待時間,即,當該應用程序呈現的幀,並且當顯示面板點亮的像素之間的時間。如果您只是在屏幕上投擲內容,則無關緊要,因爲用戶無法區分差異。但是,如果您對觸摸輸入做出響應,則每一幀的延遲都會讓您的應用感覺反應遲鈍。

問題類似於A/V同步,您需要在屏幕上顯示視頻幀時需要與幀關聯的音頻才能從揚聲器出來。在這種情況下,只要在音頻和視頻輸出上始終保持相同的狀態,總延遲就無關緊要。但是,這會遇到非常類似的問題,因爲如果SurfaceFlinger停頓並且您的視頻一直顯示在一幀之後,則會失去同步。

SurfaceFlinger以高優先級運行,並且工作量相對較少,所以不會錯過自己的節拍...但它可能會發生。此外,它還合成來自多個來源的幀,其中一些使用柵欄來表示異步完成。如果準時視頻幀由OpenGL輸出組成,並且GLES渲染在截止日期到達時尚未完成,則整個合成將被推遲到下一個VSYNC。

最小化等待時間的願望足夠強大,Android KitKat(4.4)版本在SurfaceFlinger中引入了「DispSync」功能,該功能可減少通常兩幀延遲的半幀延遲。 (這在圖形體系結構文檔中有所提及,但並未廣泛使用。)

所以就是這樣的情況。在過去,這對於視頻來說不是什麼問題,因爲30fps的視頻會更新其他每一幀。打嗝自然而然地發生了,因爲我們並沒有試圖保持隊列滿。儘管我們開始看到48Hz和60Hz的視頻,所以這更重要。

現在的問題是,我們如何檢測我們發送到SurfaceFlinger的幀是否儘快顯示,或者在我們之前發送的緩衝區後面花費額外的幀?

答案的第一部分是:你不能。 SurfaceFlinger上沒有狀態查詢或回調,它會告訴你它的狀態是什麼。理論上你可以查詢BufferQueue本身,但這並不一定告訴你你需要知道什麼。

與查詢和回調的問題是,他們不能告訴你什麼狀態,只是什麼狀態。當應用程序收到信息並採取行動時,情況可能會完全不同。該應用程序將以正常優先級運行,因此可能會延遲。

對於A/V同步,它稍微複雜一些,因爲應用程序無法知道顯示特性。例如,一些顯示器具有內置內存的「智能面板」。 (如果屏幕上的內容不經常更新,您可以通過不讓面板每秒掃描內存總線60x上的像素來節省大量電量。)這些可能會增加額外的必須考慮的延遲幀。

Android正在向A/V同步方向發展的解決方案是讓應用程序在需要顯示幀時告訴SurfaceFlinger。如果SurfaceFlinger錯過了截止日期,它會丟棄幀。這是在4中通過實驗添加的。4,儘管直到下一個版本纔會被使用(儘管我不知道它是否包含了完全使用它的所有部分),但它在「L預覽」中應該已經足夠了。

應用程序使用此方法的方式是在eglSwapBuffers()之前調用eglPresentationTimeANDROID()擴展名。該函數的參數是所需的呈現時間,以納秒爲單位,使用與Choreographer相同的時基(特別是Linux CLOCK_MONOTONIC)。因此,對於每一幀,您需要從編排器中獲取時間戳,將所需的幀數乘以近似的刷新率(您可以通過查詢Display對象獲得 - 請參閱MiscUtils#getDisplayRefreshNsec()),然後將其傳遞給EGL。交換緩衝區時,所需的顯示時間會隨緩衝區一起傳遞。

回想一下,SurfaceFlinger每VSYNC喚醒一次,查看待定緩衝區的集合,並通過Hardware Composer將一組集合傳遞給顯示硬件。如果您在時間T請求顯示,並且SurfaceFlinger認爲在時間T-1或更早時間顯示傳遞給顯示硬件的幀,則會保留該幀(並重新顯示前一幀)。如果幀在時間T出現,它將被髮送到顯示器。如果幀將在時間T + 1或更晚的時間出現(即它將錯過它的最後期限),則在隊列中存在另一個幀,該隊列被安排在稍後的時間(例如,用於時間T + 1的幀) ,那麼用於時間T的幀將被丟棄。

該解決方案並不完全適合您的問題。對於A/V同步,您需要不間斷延遲,而不是最小延遲。如果你看看Grafika的「scheduled swap」活動,你可以找到一些使用eglPresentationTimeANDROID()的代碼,其方式與視頻播放器相似。 (在目前的狀態下,它僅僅是創建systrace輸出的「音調生成器」,但基本部分就在那裏)。戰略是提前幾幀,所以SurfaceFlinger永遠不會幹燥,但這對你的應用程序。

然而,演示時機制提供了一種丟棄幀而不是讓它們備份的方法。如果您碰巧知道編舞者報告的時間和可以顯示幀的時間之間存在兩幀延遲,則可以使用此功能確保幀將被放下而不是排隊,如果它們距離攝像機太遠過去。 Grafika活動允許您設置幀速率和請求延遲,然後以systrace查看結果。

這對於應用程序瞭解SurfaceFlinger實際具有多少幀延遲將會有所幫助,但是沒有查詢。 (無論如何,這對於處理有些尷尬,因爲「智能面板」可以改變模式,從而改變顯示延遲;但除非您正在處理A/V同步,否則您真正關心的是將SurfaceFlinger延遲降至最低。)合理安全地假設4.3+上有兩幀。如果不是兩幀,你可能會有不理想的表現,但如果你根本沒有設置演示時間,那麼淨效果不會比你得到的差。

您可以嘗試設置所需的演示時間等於編排時間戳;最近的時間戳意味着「儘快顯示」。這確保了最小的等待時間,但可以適應平穩。 SurfaceFlinger具有兩幀延遲,因爲它使系統中的所有內容都有足夠的時間完成工作。如果您的工作負載不均衡,您將在單幀和雙幀延遲之間擺動,並且輸出在轉換時看起來會很複雜。 (這是DispSync的一個問題,它將總時間減少到1.5幀。)

我不記得何時添加了eglPresentationTimeANDROID()函數,但在較早的版本中,它應該是無操作的。

底線:'L',在某種程度上4。4,你應該能夠使用帶有兩幀延遲的EGL擴展來獲得你想要的行爲。在早期版本中,系統沒有任何幫助。如果你想確保你的方式沒有緩衝區,你可以每隔一段時間故意刪除一個幀,讓緩衝區隊列消失。

更新:一種避免排隊幀的方法是呼叫eglSwapInterval(0)。如果您直接將輸出發送到顯示器,則該調用將禁用與VSYNC同步,取消應用程序的幀速率。當通過SurfaceFlinger進行渲染時,這會將BufferQueue置於「異步模式」,如果提交的速度比系統能夠顯示的速度快,則會導致幀丟失。

請注意,您仍然是三重緩衝區:正在顯示一個緩衝區,SurfaceFlinger持有一個緩衝區以顯示在下一個翻轉屏幕上,另一個正在由應用程序繪製。

+1

感謝您的詳細回覆。讓SurfaceFlinger放棄框架,如果有另一個排隊似乎對我來說是正確的行爲,並將展示時間設置爲vsync的時間聽起來像是一個明智的方式來實現這一點。 – tangobravo 2014-10-14 08:10:10

+0

跟隨其他一些要點:「指針路徑」在4.4版本的Moto G上有很長的路徑,而且它看起來在SurfaceFlinger內部執行繪圖,所以這是重現行爲的一種簡單方法。關於平滑性的論點,只要你不能在60FPS渲染,你註定要顯示一些幀兩次,這將導致可見的結果。在這種情況下,最好下降到30FPS,並開始渲染每隔一個vsync。如果您的渲染時間在16ms以下,那麼填滿隊列的隊列會額外花費一段時間以趕上任何尖峯。 – tangobravo 2014-10-14 08:19:02

+0

所以我會說最好的遊戲循環建議是在「填充隊列」之間進行選擇,爲您購買一個額外的幀週期來處理峯值,但帶有額外的延遲幀,或者與編排器觸發的渲染一起進行呈現時間最短的延遲。思考? – tangobravo 2014-10-14 08:21:16