Android VSYNC與圖形系統中的撕裂、雙緩衝、三緩衝淺析

先接觸兩個圖形概念: 幀率(Frame Rate,單位FPS)–GPU顯卡生成幀的速率,也可以認爲是數據處理的速度), 屏幕刷新頻率 (Refresh Rate單位赫茲/HZ):是指硬件設備刷新屏幕的頻率。屏幕刷新率一般是固定的,比如60Hz的每16ms就刷一次屏幕,可以類比一下黑白電視的電子掃描槍,每16ms電子槍從上到下從左到右一行一行逐漸把圖片繪製出來,如果GPU顯卡性能非常強悍,幀率可以非常高,甚至會高於屏幕刷新頻率。

本文參考視頻 Google IO

單緩存畫面撕裂與(垂直同步+雙緩衝)

什麼是畫面撕裂?如下:用兩幀的部分數據合成一幀。

image.png

The display (LCD, AMOLED, whatever) gets each frame from the graphics chip, and starts drawing it line by line. Ideally, you want the display to get a new frame from the graphics chip after it is finished drawing the previous frame. Tearing occurs when the graphics chip loads a new frame in the middle of the LCD draw, so you get half of one frame and half of another.

如果只有一塊緩存,在沒有加鎖的情況下,容易出現。即:在屏幕更新的時候,如果顯卡輸出幀率很高,在A幀的數據上半部分剛更新完時,B幀就到了,如果沒采取同步鎖機制,可以認爲幀到了就可用,在繼續刷新下半部分時,由於只有一塊存儲,A被B覆蓋,繪製用的數據就是B幀,此時就會出現上半部分是A下半部分是B,這就是屏幕撕裂,個人覺得描述成顯卡瞬時幀率過高也許更好。同正常幀繪製相比,正常的幀給時間才就能完整繪製一幀,但撕裂的幀沒有機會補全。

image.png

相比較畫面撕裂場景如下:

image.png

不過按照Android官方指導的說法,屏幕撕裂還有另外一種解釋,那就是顯示器用了半成品的幀,不過我是不太理解他說的這點。參考視頻

以上說的是隻有一塊顯示存儲的情況,其實只要加鎖就能解決。那麼如果多增加一塊顯示存儲區能解決嗎?顯卡繪製成功後,先寫入BackBuffer,不影響當前正在展示的FrameBuffer,這就是雙緩衝,但是理論上其實也不行,因爲BackBuffer畢竟也是要展示的,也要”拷貝“到FrameBuffer,在A幀沒畫完,BackBuffer如果不加干預,直接”拷貝“到FrameBuffer同樣出現撕裂。所以同步鎖的機制纔是關鍵,必須有這麼一個機制告訴GPU顯卡,要等待當前幀繪完整,才能替換當前幀。但如果僅僅單緩存加鎖的話GPU顯卡會被掛啊?這就讓效率低了,那就一邊加同步鎖,同時再多加一個緩存,垂直同步(VSYNC)就可看做是這麼個東西,其實兩者是配合使用的。

image.png

再來看下VSYNC,屏幕刷新從左到右水平掃描(Horizontal Scanning),從上到下垂直掃描Vertical Scanning,垂直掃描完成則整個屏幕刷新完畢,這便是告訴外界可以繪製下一幀的時機,在這裏發出VSync信號,通知GPU給FrameBuffer傳數據,完成後,屏幕便可以開始刷新,所以或許稱之爲幀同步更合適。VSYNC強制幀率和顯示器刷新頻率同步,如果當前幀沒繪製完,即使下一幀準備好了,也禁止使用下一幀,直到顯示器繪製完當前幀,等下次刷新的時候,纔會用下一幀。比如:如果顯示器的刷新頻率是60HZ顯示器,開了垂直同步後,顯示幀率就會被鎖60,即使顯卡輸出高,也沒用。對Android系統而言,垂直同步信號除了強制幀率和顯示器刷新頻率同步外,還有其他很多作用,VSYNC是APP端重繪、SurfaceFlinger圖層合成的觸發點,只有收到VSYNC信號,它們纔會工作,以上便是個人對引入VSYNC與雙緩衝的見解。

雙緩衝的進階:三緩衝

在Android系統裏,除了雙緩衝,還有個三緩衝,不過這個三緩衝是對於屏幕硬件刷新之外而言,它關注的是整個Android圖形系統的消費者模型,跟Android自身的VSYNC用法有關係,在 Jelly Bean 中Android擴大了VSYNC使用場景與效果,不僅用在屏幕刷新防撕裂,同時也用在APP端繪製及SurfaceFlinger合成那,此時對VSYNC利用有點像Pipeline流水線,貫穿整個繪製流程,對比下VSYNC擴展使用的區別:

image.png

如果想要達到60FPS的流暢度,每16毫秒必須刷新一幀,否則動畫、視頻就沒那麼絲滑,擴展後:

image.png

對於沒采用VSYNC做調度的系統來說,比如Project Butter之前的系統(4.1以下),CPU的對於顯示幀的處理是凌亂的,優先級也沒有保障,處理完一幀後,CPU可能並不會及時處理下一幀,可能會優先處理其他消息,等到它開始處理UI生成幀的時候,可能已經處於VSYNC的中間,這樣就很容易跨兩個VYSNC信號,導致掉幀。在Jelly Bean中,下一幀的處理被限定在VSync信號到達時,並且看Android的處理UI重繪消息的優先級是比較高的,其他的同步消息均不會執行,從而保證每16ms處理一幀有序進行,同時由於是在每個VSYNC信號到達時就處理幀,可以儘量避免跨越兩幀的情況出現

上面的流程中,Android已經採用了雙緩衝,**雙緩衝不僅僅是兩份存儲,它是一個概念,雙緩衝是一條鏈路,不是某一個環節,是整個系統採用的一個機制,需要各個環節的支持,從APP到SurfaceFlinger、到圖像顯示都要參與協作。**對於APP端而言,每個Window都是一個雙緩衝的模型,一個Window對應一個Surface,而每個Surface裏至少映射兩個存儲區,一個給圖層合成顯示用,一個給APP端圖形處理,這便是應於上層的雙緩衝。Android4.0之後基本都是默認硬件加速,CPU跟GPU都是併發處理任務的,CPU處理完之後就完工,等下一個VSYNC到來就可以進行下一輪操作。也就是CPU、GPU、顯示都會用到Buffer,VSYNC+雙緩衝在理想情況下是沒有問題的,但如果某個環節出現問題,那就不一樣瞭如下(幀耗時超過16ms):

雙緩衝jank

可以看到在第二個階段,存在CPU資源浪費,爲什麼呢?雙緩衝Surface只會提供兩個Buffer,一個Buffer被DisPlay佔用(SurfaceFlinger用完後不會釋放當前的Buffer,只會釋放舊的Buffer,直觀的想一下,如果新Buffer生成受阻,那麼肯定要保留一個備份給SF用,才能不阻礙合成顯示,就必定要一直佔用一個Buffer,新的Buffer來了才釋放老的),另一個被GPU處理佔用,所以,CPU就無法獲取到Buffer處理當前UI,在Jank的階段空空等待。一般出現這種場景都是連續的:比如複雜視覺效果每一幀可能需要20ms(CPU 8ms +GPU 12ms),GPU可能會一直超負荷,CPU跟GPU一直搶Buffer,這樣帶來的問題就是滾雪球似的掉幀,一直浪費,完全沒有利用CPU與GPU並行處理的效率,成了串行處理,如下所示

image.png

如何處理呢?讓多增加一個Buffer給CPU用,讓它提前忙起來,這樣就能做到三方都有Buffer可用,CPU跟GPU不用爭一個Buffer,真正實現並行處理。如下:

image.png

如上圖所示,雖然即使每幀需要20ms(CPU 8ms +GPU 12ms),但是由於多加了一個Buffer,實現了CPU跟GPU並行,便可以做到了只在開始掉一幀,後續卻不掉幀,雙緩衝充分利用16ms做到低延時,三緩衝保障了其穩定性,爲什麼4緩衝沒必要呢?因爲三個既可保證並行,四個徒增資源浪費。

雙緩衝保證低延時,三緩衝保證穩定性,雙緩衝不在16ms中間開始,有足夠時間繪製 三緩衝增加其韌性。

總結

  • 同步是防止畫面撕裂的關鍵,VSYNC同步能防止畫面撕裂
  • VSYNC+雙緩衝在Android中能有序規劃渲染流程,降低延時
  • Android已經採用了雙緩衝,雙緩衝不僅僅是兩份存儲,它是一個概念,雙緩衝是一條鏈路,不是某一個環節,是整個系統採用的一個機制,需要各個環節的支持,從APP到SurfaceFlinger、到圖像顯示都要參與協作
  • 三緩衝在UI複雜情況下能保證畫面的連續性,提高柔韌性

作者:看書的小蝸牛

Android VSYNC與圖形系統中的雙緩衝、三緩衝淺析

僅供參考,歡迎指正

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章