2015年1月28日 星期三

DispSync

如果你曾經打開過systrace的檔案並觀察SurfaceFlinger的process, 你可能看過下圖中用紅色標起來的區域. 這四個代表的是VSync相關的information.


HW_VSYNC_ON_0代表Primary Display的VSync被enable或disable. 0這個數字代表的是display的編號, 0是Primary Display, 如果是External monitor, 就會是HW_VSYNC_ON_1. 當SF要求HWComposer將Display的VSync打開或關掉時, 這個event就會記錄下來.

HW_VSYNC_0代表Primary Display的VSync發生時間點, 0代表display編號. 以下圖為例, A, B, C, D分別代表VSync發生的時間, 這個event並沒有記錄VSync結束的時間, 所以, 不能把它解讀為在A時間點VSync開始, 然後在B結束, C開始, D結束. 正確的解讀是是A-B代表的是一個frame, A是VSync發生點, B是下一個VSync開始的時間. 如果refresh rate是60Hz的話, A-B, B-C, C-D的長度應該在16.6ms左右 (會有latency), 之所以有0與1的變化是為了讓人比較容易讀, 但有時候也會造成解讀錯誤.


通常為了避免Tearing的現象, 畫面更新(Flip)的動作通常會在VSync開始的時候才做, 因為在VSync開始到它結束前, Display不會抓framebuffer資料顯示在display上, 所以在這段時間做Flip可以避免使用者同時看到前後兩個部份畫面的現象.

目前user看到畫面呈現的過程是這樣子的, app更新它的畫面後, 它需要透過BufferQueue通知SF, SF再將更新過的app畫面與其它的App或Status Bar/Navigation Bar組合後, 再顯示在使用者面前. 在這個過程裡, 有3個component牽涉進來, 分別是App, SF, 與Display. 以目前Android的設計, 這三個Component都是在VSync發生的時候才開始做事. 我們將它們想成一個有3個stage的pipeline, 這個pipeline的clock就是HW_VSYNC_0.
  1. T = 0時, App正在畫N, SF與Display都沒資料可用
  2. T = 1時, App正在畫N+1, SF組合N, Display沒Buffer可顯示
  3. T = 2時, App正在畫N+2, SF組合N+1, Display顯示N
  4. T = 3時, App正在畫N, SF組合N+2, Display顯示N+1
  5. ...

如果按照這個步驟, 當user改變一個畫面時, 要等到2個VSync後, 畫面才會顯示在user面前, latency大概是33ms (2個frame的時間). 但是對大部份的操作來講, 可能app加SF畫的時間一個frame (16.6ms)就可以完成. 因此, Android就從HW_VSYNC_0才產生出兩個VSync, VSYNC-app是給App用的, VSYNC-sf是給SF用的, Display則是使用HW_VSYNC_0. VSYNC-app與VSYNC-sf可以分別給定一個phase, 簡單的說
  • VSYNC-app = HW_VSYNC_0 + phase_app
  • VSYNC-sf = HW_VSYNC_0 + phase_sf


也就是說, 如果phase_app與phase_sf設定的好的話, 可能大部份user使用的狀況, App+SF可以在一個frame裡完成, 然後在下一個HW_VSYNC_0來的時候, 顯示在display上.
理論上透過VSYNC-sf與VSYNC-app的確是可以有機會在一個frame裡把App+SF做完, 但是實際上不是一定可以在一個frame裡完成. 因為有可能因為CPU被搶走, 或memory不夠, 以致App或SF沒辦法及時做完. 如果我們觀察systrace可以發現不是每次Render的時間都很穩定. 下圖是systrace裡Launcher3在畫一個frame所花的時間, 從每一段的長度可以看出時間其實每個frame有某種程度的變動. 但是除非有不正常的狀況, 不然其實變動的範圍不會太大.



如果不能在一個frame裡完成, 就會變成latency可能會有比較大的變化, 一旦latency不是很穩定, 對人的視覺系統而言, 就容易感覺不舒服, 可能比delay兩個VSync還差. 所以在調整這個參數時, 只要能將latency維持在一個固定的範圍, 應該可以改善user experience, 比方說將latency調整到1.5個frame.

如果你想使用這個功能, 只要在Makefile裡設定VSYNC_EVENT_PHASE_OFFSET_NS和SF_VSYNC_EVENT_PHASE_OFFSET_NS就可以了. Google是建議放在BoardConfig.mk裡. 前者代表VSYNC-app, 後者代表VSYNC-sf.

這樣子看起來在SF裡, 只要收到硬體VSYNC, 再等一些offset, 就可以觸發VSYNC-app與VSYNC-sf了. 但實際並不是這樣. 除非使用者在玩game或播影片, 在一般使用情況下, 畫面並不會一直更新, 比方我們在看一篇文章, 大部份時間, 畫面是維持一樣的, 如果我們在每個硬體VSYNC來的時候都把SF或APP叫起來, 就會浪費時間與增加Power的使用. 所以在Android裡, 通常SF與APP都會利用requestNextVsync()來告訴系統我要在下一個VSYNC被叫起來做事情. 也就是說, 雖然系統可能每秒有60個硬體VSYNC, 但不代表Software每個VSYNC都要做事情. 因此, 在Android裡, 我們是根據Software VSYNC來做事. Software VSYNC是根據硬體VSYNC過去發生的時間, 推測未來會發生的時間. 因此, 當APP或SF利用requestNextVsync時, Software VSYNC才會觸發VSYNC-sf或VSYNC-app. DispSync這個class就是Android使用的Software VSYNC.

底下是SurfaceFlinger裡EventThread::requestNextVsync(), 做的事很簡單, 只是檢查connection的counter, 小於0就將它設成0, 代表只要下一個VSYNC通知它就好 (one-shot)
     void EventThread::requestNextVsync(
             const sp& connection) {
         Mutex::Autolock _l(mLock);
         if (connection->count < 0) {
             connection->count = 0;
             mCondition.broadcast();
         }
     }

因為它connection的counter >= 0時, EventThread的threadLoop就會認為這個connection需要SW_VSYNC, 相關的code可以參考EventThread::waitForEvent()
     Vector< sp > EventThread::waitForEvent(
             DisplayEventReceiver::Event* event)
     {
         ...
         do {
             size_t count = mDisplayEventConnections.size();
             for (size_t i=0; i
                 if (connection != NULL) {
                     bool added = false;
                     if (connection->count >= 0) {
                         waitForVSync = true;
                         ...
                         if (connection->count == 0) {
                             connection->count = -1;
                             signalConnections.add(connection);
                             added = true;
                         }
                         ...
                     }
                 }
             }
         } while (signalConnections.isEmpty());
     }


我們可以用下圖來了解DispSync的架構, DispSync像是一個PLL, 它的input是硬體VSYNC的時間, 它的feedback是Present Fence的時間, 它的輸出就是SW_VSYNC, SW_VSYNC再根據SF與APP的phase offset做調整, 分別輸出VSYNC-sf與VSYNC-app.


當SF從HWComposer收到VSYNC時, 它會利用DispSync::addResyncSample將新的VSYNC時間交給DispSync. addResyncSample會決定是否還需要HW_VSYNC的輸入, 如果不需要, 就會將硬體VSYNC關掉.

     void SurfaceFlinger::onVSyncReceived(int type, nsecs_t timestamp) {
         bool needsHwVsync = false;

         { // Scope for the lock
             Mutex::Autolock _l(mHWVsyncLock);
             if (type == 0 && mPrimaryHWVsyncEnabled) {
                 needsHwVsync = mPrimaryDispSync.addResyncSample(timestamp);
             }
         }

         if (needsHwVsync) {
             enableHardwareVsync();
         } else {
             disableHardwareVsync(false);
         }
     }

在SurfaceFlinger::postComposition()裡, 會將Present Fence的時間透過addPresentFence交給DispSync, 這用來檢查SW_VSYNC是否需要校正, 如果需要, 就會將硬體VSYNC打開.
     void SurfaceFlinger::postComposition()
     {
         ....
         const HWComposer& hwc = getHwComposer();
         sp presentFence = hwc.getDisplayFence(HWC_DISPLAY_PRIMARY);

         if (presentFence->isValid()) {
             if (mPrimaryDispSync.addPresentFence(presentFence)) {
                 enableHardwareVsync();
             } else {
                 disableHardwareVsync(false);
             }
         }
         ...
     }

DispSync是利用最近的硬體VSYNC來做預測, 最少要3個, 最多是32個, 實際上要用幾個則不一定, DispSync拿到3個VSYNC後就會計算出SW_VSYNC, 只要收到的Present Fence沒有超過誤差, 硬體VSYNC就會關掉, 不然會繼續接收硬體VSYNC計算SW_VSYNC的值, 直到誤差小於threshold. 它計算的Model是這樣的 (DispSync::updateModelLocked)

  1. 計算目前收到VSYNC間隔, 取平均值 (AvgPeriod)
  2. 將每個收到的VSYNC時間與AvgPeriod算出誤差. (Delta = Time % AvgPeriod)
  3. 將Delta轉換成角度(DeltaPhase), 如果AvgPeriod是360度, DeltaPhase = 2*PI*Delta/AvgPeriod.
  4. 從DeltaPhase可以得到DeltaX與DeltaY (DeltaX = cos(DeltaPhase), DeltaY = sin(DeltaPhase))
  5. 將每個收到的VSYNC的DeltaX與DeltaY取平均, 可以得到AvgX與AvgY
  6. 利用atan與AvgX, AvgY可以得到平圴的phase (AvgPhase)
  7. AvgPeriod + AvgPhase就是SW_VSYNC.


當DispSync收到addPresentFence時 (最多記錄8個sample), 每一個fence的時間算出 (Time % AvgPeriod)的平方當作誤差, 將所有的Fence誤差加總起來如果大於某個Threshold, 就表示需要校正 (DispSync::updateErrorLocked). 校正的方法是呼叫DispSync::beginResync()將所有的VSYNC清掉, 等至少3個VSYNC再重新計算.










1 則留言: