opengl计算帧率_或许是迄今为止第一篇讲解 fps 计算原理的文章吧
前言
fps,是 frames per second 的簡稱,也就是我們常說的“幀率”。在游戲領域中,fps 作為衡量游戲性能的基礎指標,對于游戲開發和手機 vendor 廠商都是非常重要的數據,而計算游戲的 fps 也成為日常測試的基本需求。目前市面上有很多工具都能夠計算 fps,那么這些工具計算 fps 的方法是什么?原理是什么呢?本文將針對這些問題,深入源碼進行分析,力求找到一個詳盡的答案(源碼分析基于 Android Q)
計算方法
目前絕大部分幀率統計軟件,在網上能找到的各種統計 fps 的腳本,使用的信息來源有兩種:一種是基于?dumpsys SurfaceFlinger --latency Layer-name(注意是 Layer 名字,不是包名,不是 Activity 名字,至于為什么,下面會解答);另一種是基于?dumpsys gfxinfo。其實這兩種深究到原理基本上是一致的,本篇文章專注于分析第一種,市面上大部分幀率統計軟件用的也是第一種,只不過部分軟件為了避免被人反編譯看到,將這個計算邏輯封裝成 so 庫,增加反編譯的難度。然而經過驗證,這些軟件最后都是通過調用上面的命令來計算的 fps 的。
但是這個命令為什么能夠計算 fps 呢?先來看這個命令的輸出,以王者榮耀為例(王者榮耀這種游戲類的都是以 SurfaceView 作為控件,因此其 Layer 名字都以?SurfaceView -?打頭):
> adb shell dumpsys SurfaceFlinger --latency "SurfaceView - com.tencent.tmgp.sgame/com.tencent.tmgp.sgame.SGameActivity#0"1666666659069638658663 59069678041684 5906965415829859069653090955 59069695022100 5906967089423659069671034444 59069711403455 5906968794986159069688421840 59069728057361 5906970441512159069705420850 59069744773350 5906972076783059069719818975 59069761378975 5906973741600759069736702673 59069778060955 5906975456866359069753361528 59069794716007 5906977076163259069768766371 59069811380486 59069787649600......輸出的這一堆數字究竟是什么意思?首先,第一行的數字是當前的 VSYNC 間隔,單位是納秒。例如現在的屏幕是 60Hz 的,因此就是 16.6ms,然后下面的一堆數字,總共有 127 行(為什么是 127 行,下面也會說明),每一行有 3 個數字,每個數字都是時間戳,單位是納秒,具體的意義后文會說明。而在計算 fps 的時候,使用的是第二個時間戳。原因同樣會在后文進行解答。
fence 簡析
后面的原理分析涉及到 fence,但是 fence 囊括的內容眾多,因此這里只是對 fence 做一個簡單地描述。如果大家感興趣,后面我會專門給 fence 寫一篇詳細的說明文章。
fence 是什么
首先得先說明一下 App 繪制的內容是怎么顯示到屏幕的:
App 需要顯示的內容要要繪制在 Buffer 里,而這個 Buffer 是從 BufferQueue 通過?dequeueBuffer()?申請的。申請到 Buffer 以后,App 將內容填充到 Buffer 以后,需要通過?queueBuffer()?將 Buffer 還回去交給 SurfaceFlinger 去進行合成和顯示。然后,SurfaceFlinger 要開始合成的時候,需要調用?acquireBuffer()?從 BufferQueue 里面拿一個 Buffer 去合成,合成完以后通過?releaseBuffer()?將 Buffer 還給 BufferQueue,如下圖:
BufferQueue在上面的流程中,其實有一個問題,就是在 App 繪制完通過?queueBuffer()?將 Buffer 還回去的時候,此時僅僅只是 CPU 側的完成,GPU 側實際上并沒有真正完成。因此如果此時拿這個 Buffer 去進行合成/顯示的話,就會有問題(Buffer 可能還沒有完全地繪制完)。
事實上,由于 CPU 和 GPU 之前是異步的,因此我們在代碼里面執行一系列的 OpenGL 函數調用的時候,看上去函數已經返回了,實際上,只是把這個命令放在本地的 command buffer 里。具體什么時候這條 GL command 被真正執行完畢 CPU 是不知道的,除非使用?glFinish()?等待這些命令完全執行完,但是這樣會帶來很嚴重的性能問題,因為這樣會使得 CPU 和 GPU 的并行性完全喪失,CPU 會在 GPU 完成之前一直處于空等的狀態。因此,如果能夠有一種機制,在不需要對 Buffer 進行讀寫 的時候,大家各干各的;當需要對 Buffer 進行讀寫的時候,可以知道此時 Buffer 在 GPU 的狀態,必要的時候等一下,就不會有上面的問題了。
fence 就是這樣的同步機制,如它直譯過來的意思一樣——“柵欄”,用來把東西攔住。那么 fence 是要攔住什么東西呢?就是前面提到的 Buffer 了。Buffer 在整個繪制、合成、顯示的過程中,一直在 CPU,GPU 和 HWC 之前傳遞,某一方要使用 Buffer 之前,需要檢查之前的使用者是否已經移交了 Buffer 的“使用權”。而這里的“使用權”,就是 fence。當 fence 釋放(即?signal)的時候,說明 Buffer 的上一個使用者已經交出了使用權,對于 Buffer 進行操作是安全的。
fence in Code
在 Android 源碼里面,fence 的實現總共分為四部分:
?fence driver? ? 同步的核心實現?libsync? ? 位于?system/core/libsync,libsync?的主要作用是對 driver 接口的封裝?Fence 類? ? 這個?Fence?類位于?frameworks/native/libs/ui/Fence.cpp,主要是對 libsync 進行 C++ 封裝,方便 framework 調用?FenceTime 類? ? 這個?FenceTime?是一個工具類,是對?Fence?的進一步封裝,提供兩個主要的接口——isValid()?和?getSignalTime(),主要作用是針對需要多次查詢 fence 的釋放時間的場景(通過調用?Fence::getSignalTime()?來查詢 fence 的釋放時間)。通過對?Fence?進行包裹,當第一次調用?FenceTime::getSignalTime()?的時候,如果 fence 已經釋放,那么會將這個 fence 的釋放時間緩存起來,然后下次再調用?FenceTime::getSignal()?的時間,就能將緩存起來的釋放時間直接返回,從而減少對?Fence::getSignalTime()?不必要的調用(因為 fence 釋放的時間不會改變)。
fence in Android
在 Android 里面,總共有三類 fence —— acquire fence,release fence 和 present fence。其中,acquire fence 和 release fence 隸屬于 Layer,present fence 隸屬于幀(即 Layers):
?acquire fence? ? 前面提到, App 將 Buffer 通過?queueBuffer()?還給 BufferQueue 的時候,此時該 Buffer 的 GPU 側其實是還沒有完成的,此時會帶上一個 fence,這個 fence 就是?acquire fence。當 SurfaceFlinger/ HWC 要讀取 Buffer 以進行合成操作的時候,需要等?acquire fence?釋放之后才行。?release fence? ? 當 App 通過?dequeueBuffer()?從 BufferQueue 申請 Buffer,要對 Buffer 進行繪制的時候,需要保證 HWC 已經不再需要這個 Buffer 了,即需要等 release fence signal 才能對 Buffer 進行寫操作。?present fence? ? present fence 在 HWC1 的時候稱為 retire fence,在 HWC2 中改名為 present fence。當前幀成功顯示到屏幕的時候,present fence 就會 signal。
原理分析
簡單版
現在來看一下通過?dumpsys SurfaceFlinger --latency Layer-name?計算 Layer fps 的原理。dumpsys 的調用流程就不贅述了,最終會走到?SurfaceFlinger::doDump():
status_t SurfaceFlinger::doDump(int fd, const DumpArgs& args, bool asProto) NO_THREAD_SAFETY_ANALYSIS { ... static const std::unordered_map<std::string, Dumper> dumpers = { ...... {"--latency"s, argsDumper(&SurfaceFlinger::dumpStatsLocked)}, ...... };從這里可以看到,我們在執行?dumpsys SurfaceFlinger?后面加的那些?--xxx?參數最終都會在這里被解析,這里咱們是?--latency,因此看?SurfaceFlinger::dumpStatsLocked:
void SurfaceFlinger::dumpStatsLocked(const DumpArgs& args, std::string& result) const { StringAppendF(&result, "%" PRId64 "\n", getVsyncPeriod()); if (args.size() > 1) { const auto name = String8(args[1]); mCurrentState.traverseInZOrder([&](Layer* layer) { if (name == layer->getName()) { layer->dumpFrameStats(result); } }); } else { mAnimFrameTracker.dumpStats(result); }}從這里就能夠看到,這里先會打印當前的 VSYNC 間隔,然后遍歷當前的 Layer,然后逐個比較 Layer 的名字,如果跟傳進來的參數一致的話,那么就會開始 dump layer 的信息;否則命令就結束了。因此,很多人會遇到這個問題:
??為什么執行了這個命令卻只打印出一個數字?
? 其實這個時候你應該去檢查你的 Layer 參數是否正確。
接下來?layer->dumpFrameStats()?會去調?FrameTrack::dumpStats():
void FrameTracker::dumpStats(std::string& result) const { Mutex::Autolock lock(mMutex); processFencesLocked(); const size_t o = mOffset; for (size_t i = 1; i < NUM_FRAME_RECORDS; i++) { const size_t index = (o+i) % NUM_FRAME_RECORDS; base::StringAppendF(&result, "%" PRId64 "\t%" PRId64 "\t%" PRId64 "\n", mFrameRecords[index].desiredPresentTime, mFrameRecords[index].actualPresentTime, mFrameRecords[index].frameReadyTime); } result.append("\n");}NUM_FRAME_RECORDS?被定義為 128,因此輸出的數組有 127 個。每組分別有三個數字——?desiredPresentTime,actualPresentTime,frameReadyTime,每個數字的意義分別是:
?desiredPresentTime?? ? 下一個 HW-VSYNC 的時間戳?actualPresentTime?? ? retire fence signal 的時間戳?frameReadyTime?? ? acquire fence signal 的時間戳
結合前面對 present fence 的描述就可以看出?dumpsys SurfaceFlinger --latency?計算 fps 的原理:
從?dumpsys SurfaceFlinger --latency?獲取到最新 127 幀的 present fence 的 signal time,結合前面對于 present fence 的說明,當某幀 present fence 被 signal 的時候,說明這一幀已經被顯示到屏幕上了。因此,我們可以通過判斷一秒內有多少個 present fence?signal 了,來反推出一秒內有多少幀被刷到屏幕上,從而計算出 fps。
復雜版
我們已經知道了 fps 計算的原理了,但是呢,小朋友,你是否有很多問號?
?這個?actualPresentTime?是從哪來的??假設要統計 fps 的 Layer 沒有更新,但是別的 Layer 更新了,這種情況下 present fence 也會正常 signal,那這樣計算出來的 fps 是不是不準啊?
為了解答這些問題,我們還得接著看。
前面已經提到計算 fps 的時候使用的是第二個數值,因此后面的文章著重分析這個?actualPresentTime。那么?actualPresentTime?是在哪里賦值的呢?實際賦值的位置是在?FrameTracker::dumpStats()?調用的一個子函數——processFencesLocked():
void FrameTracker::processFencesLocked() const { FrameRecord* records = const_cast<FrameRecord*>(mFrameRecords); int& numFences = const_cast<int&>(mNumFences); for (int i = 1; i < NUM_FRAME_RECORDS && numFences > 0; i++) { size_t idx = (mOffset+NUM_FRAME_RECORDS-i) % NUM_FRAME_RECORDS; ... const std::shared_ptr<FenceTime>& pfence = records[idx].actualPresentFence; if (pfence != nullptr) { // actualPresentTime 是在這里賦值的 records[idx].actualPresentTime = pfence->getSignalTime(); if (records[idx].actualPresentTime < INT64_MAX) { records[idx].actualPresentFence = nullptr; numFences--; updated = true; } } ......其中,FrameRecord?的完整定義如下:
struct FrameRecord { FrameRecord() : desiredPresentTime(0), frameReadyTime(0), actualPresentTime(0) {} nsecs_t desiredPresentTime; nsecs_t frameReadyTime; nsecs_t actualPresentTime; std::shared_ptr<FenceTime> frameReadyFence; std::shared_ptr<FenceTime> actualPresentFence;};從上面的代碼可以看出,actualPresentTime?是調用?actualPresentFence?的?getSignalTime()?賦值的。而?actualPresentFence?是通過?setActualPresentFence()?賦值的:
void FrameTracker::setActualPresentFence( std::shared_ptr<FenceTime>&& readyFence) { Mutex::Autolock lock(mMutex); mFrameRecords[mOffset].actualPresentFence = std::move(readyFence); mNumFences++;}setActualPresentFence()?又是經過下面的調用流程最終被調用的:
SurfaceFlinger::postComposition() \_ BufferLayer::onPostCompostion()這里重點看一下?SurfaceFlinger::postComposition():
void SurfaceFlinger::postComposition(){ ...... mDrawingState.traverseInZOrder([&](Layer* layer) { bool frameLatched = layer->onPostComposition(displayDevice->getId(), glCompositionDoneFenceTime, presentFenceTime, compositorTiming); ......回憶一下我們前面的問題:
??假設要統計 fps 的 Layer 沒有更新,但是別的 Layer 更新了,這種情況下 present fence 也會正常 signal,那這樣計算出來的 fps 是不是不準啊?
答案就在?mDrawingState,在 Surfacelinger 中有兩個用來記錄當前系統中 Layers 狀態的全局變量:
?mDrawingState? ? mDrawingState 代表的是上次 “drawing” 時候的狀態?mCurrentState? ? mCurrentState 代表的是當前的狀態因此,如果當前 Layer 沒有更新,那么是不會被記錄到 mDrawingState 里的,因此這一次的 present fence 也就不會被記錄到該 Layer 的?FrameTracker?里的?actualPresentTime?了。
再說回來,?SurfaceFlinger::postComposition()?是 SurfaceFlinger 合成的最后階段。presentFenceTime?就是前面的?readyFence?參數了,它是在這里被賦值的:
mPreviousPresentFences[0] = mActiveVsyncSource ? getHwComposer().getPresentFence(*mActiveVsyncSource->getId()) : Fence::NO_FENCE;auto presentFenceTime = std::make_shared<FenceTime>(mPreviousPresentFences[0]);而?getPresentFence()?這個函數,就把這個流程轉移到了 HWC 了:
sp<Fence> HWComposer::getPresentFence(DisplayId displayId) const { RETURN_IF_INVALID_DISPLAY(displayId, Fence::NO_FENCE); return mDisplayData.at(displayId).lastPresentFence;}至此,我們一路輾轉,終于找到了這個 present fence 的真身,只不過這里它還蒙著一層面紗,我們需要在看一下這個?lastPresentFence?是在哪里賦值的,這里按照不同的合成方式位置有所不同:
DEVICE 合成
DEVICE 合成的?lastPresentFence?是在?HWComposer::prepare()?里賦值:
status_t HWComposer::prepare(DisplayId displayId, const compositionengine::Output& output) { ...... if (!displayData.hasClientComposition) { sp<Fence> outPresentFence; uint32_t state = UINT32_MAX; error = hwcDisplay->presentOrValidate(&numTypes, &numRequests, &outPresentFence , &state); if (error != HWC2::Error::HasChanges) { RETURN_IF_HWC_ERROR_FOR("presentOrValidate", error, displayId, UNKNOWN_ERROR); } if (state == 1) { //Present Succeeded. ...... displayData.lastPresentFence = outPresentFence;經常看 systrace 的同學對這個函數絕對不會陌生,就是 systrace 里面 SurfaceFlinger 的那個?prepare():
Systrace 中的 prepare這個函數非常重要,它通過一系列的調用:
HWComposer::prepare() \_ Display::presentOrValidate() \_ Composer::presentOrValidateDisplay() \_ CommandWriter::presentOrvalidateDisplay()最終通過 HwBinder 通知 HWC 的 Server 端開始進行 DEVICE 合成,Server 端在收到 Client 端的請求以后,會返回給 Client 端一個 present fence(時刻記住,fence 用于跨環境的同步,例如這里就是 Surfacelinger 和 HWC 之間的同步)。然后當下一個 HW-VSYNC 來的時候,會將合成好的內容顯示到屏幕上并且將該 present fence signal,標志著這一幀已經顯示在屏幕上了。
GPU 合成
GPU 合成的?lastPresentFence?是在?presentAndGetPresentFences()?里賦值:
status_t HWComposer::presentAndGetReleaseFences(DisplayId displayId) { ...... displayData.lastPresentFence = Fence::NO_FENCE; auto error = hwcDisplay->present(&displayData.lastPresentFence);后面的流程就跟 DEVICE 合成類似了,Display::present()?最終也會經過一系列的調用,通過 HwBinder 通知 HWC 的 Server 端,調用?presentDisplay()?將合成好的內容顯示到屏幕上。
總結
說了這么多,一句話總結計算一個 App 的 fps 的原理就是:
統計在一秒內該 App 往屏幕刷了多少幀,而在 Android 的世界里,每一幀顯示到屏幕的標志是:present fence signal 了,因此計算 App 的 fps 就可以轉換為:一秒內 App 的 Layer 有多少個有效 present fence signal 了(這里有效 present fence 是指,在本次 VSYNC?中該 Layer 有更新的 present fence)
尾巴
這篇文章在二月份其實就已經完成了一多半了,但是一直拖到了五月才最終寫完,因為其中涉及到很多我不知道的知識,例如 HWC。這塊領域涉及到硬件,文檔其實不多。因此在寫的過程會變得異常痛苦,很多東西不懂,我也不知道自己寫的東西究竟對不對,就需要花很多時間進行多方求證,找很多大佬提問。很多時候會卡在某一個地方很久,甚至會萌生隨便寫點糊弄過去算了的想法。而且,寫到什么程度也很難拿捏,寫淺了我自己過意不去,感覺對不起各位關注的讀者;寫深了我自己也是寫不下去,畢竟這個領域確實之前沒有接觸過。不過好在這個過程中有很多大佬給我提供了很大的幫助,在此對在這幾月中給我答疑解惑的各位大佬表示衷心的感謝。
寫作是一個孤獨的旅程,感謝各位大佬的指路,感謝各位讀者的關注,你們是星星太陽和月亮,陪著我一直寫下去,謝謝。
總結
以上是生活随笔為你收集整理的opengl计算帧率_或许是迄今为止第一篇讲解 fps 计算原理的文章吧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据结构--栈--浏览器前进后退应用
- 下一篇: php中括号的优先级是不是最高的,理解p