Frame Pacing Library Android Game Development Kit 的一部分

Android Frame Pacing 程式庫 (又稱為 Swappy) 是 AGDK 程式庫的一部分。這個程式庫可讓您的 OpenGL 和 Vulkan 遊戲在 Android 裝置上流暢地顯示畫面並修正影格同步情形。這份文件會定義何謂影格同步、說明需要影格同步的情況,以及這個程式庫如何解決這些狀況。如要在遊戲中直接實作影格同步,請參閱後續步驟

背景

「影格同步」是指遊戲的邏輯和算繪迴圈,與 OS 螢幕子系統和基礎顯示硬體的同步處理程序。Android 螢幕子系統經過特別設計,可避免可能發生在顯示硬體切換至新影格途中的瑕疵 (又稱為「撕裂」) 情形。為了避免出現這類瑕疵,螢幕子系統會執行以下操作:

  • 在內部為已通過的影格預留緩衝區
  • 偵測延遲提交的影格
  • 偵測到影格延遲時重複顯示已通過的影格

遊戲會在螢幕子系統內通知 SurfaceFlinger 合成器,已提交了影格所需的所有繪製呼叫 (透過呼叫 eglSwapBuffersvkQueuePresentKHR)。SurfaceFlinger 會使用閂鎖向顯示硬體表示影格是否可供使用。顯示硬體接著就會顯示指定的影格。顯示硬體會以固定速率 (例如 60 Hz) 滴答,如果硬體需要新影格時沒有新的影格,硬體就會再次顯示前一個影格。

遊戲以與原生顯示硬體不同的速率算繪迴圈時,常常會出現不一致的影格時間。如果以每秒 30 個影格數執行的遊戲試圖在原生支援每秒 60 個影格數的裝置上執行,遊戲算繪迴圈就無法偵測到額外保留在螢幕上 16 毫秒的重複影格。這樣的中斷情形通常會導致影格時間嚴重不一致,例如:49 毫秒、16 毫秒、33 毫秒。過度複雜的場景會導致影格遭到遺漏,使得問題更加複雜。

非最佳解決方案

過去遊戲會採用下列適用於影格同步的解決方案,往往導致影格時間不一致且輸入的延遲時間增加。

以算繪 API 可允許的程度盡快提交影格

這個方法結合遊戲與 SurfaceFlinger 活動之間的變數,並導入額外的影格延遲時間。顯示管道包含影格佇列 (通常大小為 2 ),如果遊戲嘗試快速算繪影格,佇列就會填滿。由於佇列已無空間,遊戲迴圈 (或至少算繪執行緒) 會遭到 OpenGL 或 Vulkan 呼叫封鎖。接著,遊戲就必須等待顯示硬體顯示影格,此次背壓就會同步處理兩個元件。這種情況稱為「緩衝區填充」「佇列填充」。算繪程序無從得知發生什麼事,因此影格速率不一致的情況會越來越嚴重。如果遊戲範例在影格之前輸入,輸入延遲的情況就會更嚴重。

單獨使用 Android Choreographer

遊戲也會使用 Android Choreographer 進行同步處理。這個元件可透過 API 16 的 Java 和 API 24 的 C++ 提供,其滴答頻率與螢幕子系統相同。不過,相較於實際硬體 VSYNC,滴答提交的時間仍有些微差異,而且這些偏移會因裝置而異。長的影格可能仍會出現緩衝區填充的情形。

Frame Pacing 程式庫的優點

Frame Pacing 程式庫採用 Android Choreographer 進行同步處理,並處理所提交滴答中的各項變化。這項功能使用演示時間戳記,確保系統在適當時機顯示並同步處理時間籬,以避免緩衝區填充的情況。這個程式庫使用 NDK Choreographer (如果有的話)。如果沒有的話,則會改用 Java Choreographer

如果裝置支援該程式庫,這個程式庫就能處理多個刷新率,進而讓遊戲在顯示影格時更具彈性。舉例來說,對於支援 60 Hz 和 90 Hz 刷新率的裝置,為保持運作順暢,無法產生每秒 60 個影格數的遊戲,系統會將其每秒影格數調降至 45 而非 30。程式庫會偵測預期的遊戲畫面更新率,並據此自動調整影格顯示時間。

運作方式

以下各節說明 Frame Pacing 程式庫如何處理長和短的遊戲影格,讓影格能夠正確同步。

以 30 Hz 為單位修正影格同步速度

在 60 Hz 裝置上以 30 Hz 算繪時,Android 上的理想狀態如圖 1 所示。SurfaceFlinger 會鎖住新的圖形緩衝區,如果有的話 (圖表中的 NB 表示「沒有緩衝區」,且前一個緩衝區會重複)。

以 30 Hz 的理想影格同步速度在 60 Hz 裝置上執行

圖 1. 以 30 Hz 的理想影格同步速度在 60 Hz 裝置上執行

短的遊戲影格會導致播放不流暢

在大部分新型裝置上,遊戲引擎都仰賴平台編排器提交滴答,以驅動系統提交影格。不過如圖 2 所示,較短的影格可能仍會導致影格同步情況不佳。較長的影格接續著較短的影格出現會被玩家視為延遲。

較短的遊戲影格

圖 2. 短的遊戲影格 C 會使得影格 B 只顯示一個影格,而後接續出現多個影格 C

Frame Pacing 程式庫透過使用演示時間戳記來解決這個問題。程式庫會使用顯示時間戳記副檔名 EGL_ANDROID_presentation_timeVK_GOOGLE_display_timing,因此影格不會提早出現,如圖 3 所示。

演示時間戳記

圖 3. 遊戲影格 B 顯示兩次,讓遊戲流暢顯示

長的影格會導致播放不流暢或延遲

當顯示工作負載所花的時間超過應用程式工作負載的時間時,系統會將額外的影格新增至佇列。如此一來,畫面會再次出現延遲,也可能因為緩衝區填充而造成額外的影格延遲 (參閱圖 4)。程式庫會同時移除延遲和額外的影格延遲。

較長的遊戲影格

圖 4. 對於 2 個影格 (A 和 B),長的影格 B 同步處理不正確

透過同步處理時間籬 (EGL_KHR_fence_syncVkFence) 以便將等候插入的影格插入應用程式,該應用程式允許顯示管道擷取而不允許建構背壓,程式庫得以解決這個問題。影格 A 仍顯示額外影格,但影格 B 現在已經可以正確顯示,如圖 5 所示。

已新增至應用程式層的等候中影格

圖 5. 等候顯示的影格 C 和 D

支援的作業模式

您可以將 Frame Pacing 程式庫設為在下列任一模式中執行:

  • 自動模式已關閉 + 管道
  • 自動模式已開啟 + 管道
  • 自動模式已開啟 + 自動管道模式 (管道/非管道)

您可以嘗試使用自動模式和管道模式,但必須先關閉這些模式,並在初始化 Swappy 後納入以下內容:

  swappyAutoSwapInterval(false);
  swappyAutoPipelineMode(false);
  swappyEnableStats(false);
  swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);

管道模式

為了協調引擎工作負載,程式庫通常會使用管道模型,將 CPU 和 GPU 工作負載拆分至 VSYNC 邊界。

管道模式

圖 6. 管道模式

非管道模式

一般來說,這種做法會使得輸入畫面延遲的情況較少出現,且較容易預測。如果遊戲的影格時間非常短,則 CPU 和 GPU 工作負載可能都會融入單一交換間隔。在此情況下,非管道的方法實際上會縮短輸入畫面的延遲時間。

非管道模式

圖 7. 非管道模式

自動模式

大部分的遊戲不知道如何選擇替換間隔,也就是每幅影格顯示的時間長度 (例如為 30 Hz 選擇 33.3 毫秒)。在某些裝置上,遊戲能以每秒 60 個影格數算繪,而別部裝置則可能需要降到較低的值。自動模式會測量 CPU 和 GPU 時間,以便執行下列操作:

  • 自動選取替換間隔:在某些情況下,以 30 Hz 提交的遊戲和其他以 60 Hz 提交的遊戲可以允許程式庫動態調整這個間隔。
  • 停用超快影格中的管道:在任何情況下都能提交最佳的輸入畫面延遲時間。

多種刷新率

支援多種刷新率的裝置可讓使用者靈活切換替換間隔,讓畫面看起來更順暢:

  • 在 60 Hz 的裝置上:60 FPS / 30 FPS / 20 FPS
  • 在 60 Hz + 90 Hz 的裝置上:90 FPS / 60 FPS / 45 FPS / 30 FPS
  • 在 60 Hz + 90 Hz + 120 Hz 的裝置上:120 FPS / 90 FPS / 60 FPS / 45 FPS / 40 FPS / 30 FPS

程式庫會選擇與遊戲影格的實際顯示時間長度最相符的刷新率,以提供更優質的視覺體驗。

如要進一步瞭解多種刷新率的影格同步情形,請參閱高刷新率算繪的 Android 網誌文章

影格統計資料

Frame Pacing 程式庫提供下列統計資料供偵錯和剖析:

  • 顯示算繪完成後,在合成器佇列等待的影格其畫面重新整理次數的直方圖。
  • 顯示在要求的顯示時間與目前的實際時間之間,畫面重新整理次數的直方圖。
  • 顯示兩個連續影格間,通過的畫面重新整理次數直方圖。
  • 顯示 CPU 開始為這個影格運作與目前的實際時間之間,畫面重新整理次數的直方圖。

後續步驟

參閱以下任一指南,將 Android Frame Pacing 程式庫整合至遊戲中: