關於 Viz 的系列文章

Mojo 快速入門

因為後面的內容需要讀者對 Mojo 有一定了解,如果以前沒接觸過的話,這部分內容提供了一個快速的入門。

簡單的說 Mojo 是一個 Client/Service 的通訊機制,支援跨程序。一個 Mojo 連結建立的過程一般如下:

Client 端建立一個 Mojo 介面物件;

Client 端為該介面物件生成一個連結請求物件;

Client 端將該連結請求物件傳送給 Service 端(一般是透過另外一個已經建立連結的 Mojo 介面物件);

Service 端接收到連結請求物件後,會跟一個對應 Mojo 介面的實現物件進行繫結,這樣 Client/Service 的連結就建立起來了;

Client 端透過上面的 Mojo 介面物件給 Service 端傳送訊息(透過介面的方法呼叫,可以在連結還沒建立之前就傳送);

Service 端接收到該訊息,會轉交給之前建立的 Mojo 介面的實現物件處理(呼叫實現物件的對應方法);

如果該訊息需要返回值,會將返回值回傳給 Client 端;

如果是 Client 端需要接收 Service 端的回撥,建立連結的過程則有所不同:

Client 端建立一個 Mojo 介面物件;

Client 端為該介面物件生成一個連結請求物件;

Client 端將該連結請求物件跟一個對應 Mojo 介面的實現物件進行繫結;

Client 端將上述的 Mojo 介面物件傳送給 Service 端(一般是透過另外一個已經建立連結的 Mojo 介面物件);

Service 端收到 Mojo 介面物件後,則可以使用該介面物件給 Client 端傳送訊息;

更多關於 Mojo 的內容可以閱讀官方的文件 Intro to Mojo & Services。

合成器架構

當我們考察 Chromium 的合成器架構的時候,總是會有一些疑問:

為什麼 Chromium 需要在 Layer Compositor 之上再構建一個 Display Compositor,並且還將 Display Compositor 包裝成 Viz Service 服務的一部分,為什麼 Layer Compositor 不能自己使用一個 Renderer 直出?

Surface 跟 Layer 有什麼不同?

我嘗試根據自己的理解來回答上述的問題:

Surface 的確某種程度跟 Layer 有相似之處,但是 Surface 提供了一個更簡單抽象的概念(CompositorFrame Queue),更容易用於結構簡單的非網頁內容的合成輸出,比如瀏覽器 UI,外掛等。

Surface 的父子關係並不是預先確定的,實際上 Surface 之間是相互獨立的,它們的父子關係是由某個 Surface 當前的 CompositorFrame 包含一個 SurfaceDrawQuad 指向另外一個 Surface 形成的巢狀關係來決定的,預設 Display Compositor 會主動建立一個 Surface 作為 Root Surface 來 Attach 外部來源的 Surface。這種方式比較 Layer 需要事先確定樹結構來說更為靈活,當然這也是因為 Surface 之間不會有太複雜的巢狀關係。

以 Surface/CompositorFrame 為基礎構建的 Display Compositor,並且被包裝成 Viz Service 服務的一部分,使得一棵 Surface 樹是可以支援跨程序的(父子 Surface 來源不同的程序),這對於 OOPIF(程序外 iframe)和外掛來說是必須的。

部分網頁元素如 Video,VR/AR,Offscreen Canvas 理論上也可以使用 Layer,但是使用獨立的 Surface,讓 Layer 作為 Surface 的 placeholder,可以使得它們的後續更新能夠跳過 Layer Compositor 冗長的處理步驟,直接透過 Display Composior 輸出。從而使得這些元素本身的更新更高效,輸出延遲更低,也更省電。這也是因為它們的內容來源不是網頁本身,不需要透過 Layer Compositor 光柵化,並且不需要跟其它 DOM 元素同時更新。

但是反過來又有另外的問題了,假設我們只需要合成輸出一個比較普通的網頁,不需要繪製瀏覽器 UI,沒有 Video,VR/AR,OffscreenCanvas 元素或者它們不使用 Surface,沒有或者不打算支援程序外 iframe 和外掛,目前 Chromium 複雜的合成器架構是不是反而增加了很多額外的開銷,導致可能的效能下降和耗電?

我個人覺得這個問題的答案,很不幸地會是 Yes,跨程序的 IPC,複雜的 Display Composior 架構的確會帶來一些額外的開銷,對普通的網頁來說,即使執行緒併發能夠抵消一部分效能損失,但是它的繪製輸出延遲和耗電還是會增加,有時魚和熊掌就是不可得兼。

下面的內容會繼續介紹 Chromium 目前在 Android 平臺的合成器架構,在普通網頁上的三種不同的實際應用方案。

Android WebView 的合成器架構

Android WebView 的合成器架構跟 Chromium 其它的 Configurations 有很大的差異,造成這種現象的原因主要是:

WebView 沒有自己獨立的 Window 作為 Display 的 OutputSurface,而是需要繪製到 WebView 所在 Activity 的 Window;

WebView 的 Display Compositor 執行在 Android Render 執行緒,也就是 Android UI 的 GPU 執行緒,它不能自己控制合成輸出的時機,只能等待 Android UI 的回撥;

Android Render 執行緒沒有訊息迴圈(或者說沒有對外暴露),WebView 無法控制該執行緒的訊息處理,只能透過在 UI 執行緒 Invalidate WebView 然後等待 Android Render 執行緒回撥的方式來獲得合成輸出的機會;

在 Android WebView,實際上是沒有 Viz Service 的,WebView 自己做了一套特殊的適配機制 SynchronousCompositor,將 Layer Compositor 和 Display Compositor 聯絡起來,而沒有透過 Viz Service 服務。

在 Android WebView 合成器架構中:

對接 Layer Compositor 的 LayerTreeFrameSink 的實現是 SynchronousLayerTreeFrameSink。Layer Compositor 輸出 CompositorFrame 並不是 cc 自己主動 Push,而是等待 SynchronousCompositor 的 Host 端向 Proxy 端發起請求。Host 端在 Browser 的 UI 執行緒(部分接收回傳訊息的物件在 IO 執行緒),Proxy 在 Renderer 的 Compositor 執行緒,之間透過 Mojo 通訊。Host 端在 WebView。onDraw 被呼叫的過程中,請求 Proxy 輸出 CompositorFrame,Proxy 透過 SynchronousLayerTreeFrameSink 的回撥獲得 CompositorFrame 並返回給 Host,這是一種 Pull 的模型,區別於其它 Configurations 使用的 Push 模型。

SurfacesInstance 包含了一個 Display Compositor 的例項,同時也包括一個 FrameSinkManager 的實現作為單機版的 Viz Service,這個 Display Compositor 的例項供所有 WebView 共享,它的 OutputSurface 實際上只是當前 GL Context Window Surface 的 Wrapper。

HardwareRenderer 會透過 SurfacesInstance 申請一個 Surface,它被一個 CompositorFrameSinkSupport 物件所持有,跟所屬 WebView 對應。當 HardwareRenderer 被 Android Render 執行緒回撥獲得合成輸出的機會時,會從 SynchronousCompositor 的 Host 端獲取 CompositorFrame,然後置入申請的 Surface,再請求 SurfacesInstance 的 Display Compositor 將該 Surface Attch 到 Root Surface 上,最後合成輸出。

因為缺少完整的 Viz Service 服務,也使得 Android WebView 當前的合成輸出功能有較大的侷限性,每個 WebView 現在只有一個對應 Surface 對應網頁本身,不支援 OOPIF,外掛,獨立 Surface 的 Video 等,當然 WebView 也不需要自己來繪製 UI。

理論上 WebView 應該也可以支援完整的合成輸出功能,主 Surface 繼續沿用目前 Pull 的模型,次級 Surface 可以使用標準的 Push 模型。不過目前來說還沒有更多的資訊。

非 OOP-D 的合成器架構

對於作為獨立瀏覽器的 Chrome for Android 來說,它的合成器架構跟 Android WebView 的特殊用法差異比較大,這才是 Viz 的標準用法。OOP-D(程序外 Display Compositor)是新的合成器架構,在 M75 版本已經預設開啟,不過在這裡我們還是先介紹一下非 OOP-D 的合成器架構。

無論是否使用 OOP-D,當 Renderer 建立 Layer Compositor 的時候,它都是建立一個 AsyncLayerTreeFrameSink 作為 CompositorFrame 的提交目標物件。AsyncLayerTreeFrameSink 在 Compositor 執行緒被初始化,同時建立了 CompositorFrameSink 介面跟 Viz Service 端的連結,後續會使用該介面向 Display Compositor 提交 CompositorFrame。

Browser 端的合成器位於 CompositorImpl,CompositorImpl 內部有一個 CompositorDependencies 單例,它包括了一個 HostFrameSinkManager 作為 FrameSinkManager 介面的 Wrapper。沒有開啟 OOP-D 時,CompositorDependencies 會直接建立一個 FrameSinkManagerImpl 物件作為 HostFrameSinkManager 的 Local Manager,在本地直接處理 Renderer 發過來的 CompositorFrameSink 介面的連結請求。

同時 CompositorImpl 還會在本地建立一個 Display Compositor,透過上面的 FrameSinkManagerImpl 來建立和訪問 Surface,當 Renderer 的 Layer Compositor 提交新的 CompositorFrame 的時候,就會觸發該 Display Compositor 的合成輸出。

Chromium Viz 淺析 - 合成器架構篇

為了除錯方便,截圖使用的 Chromium 都執行在單程序模式

上圖顯示了頁面被拖動時繪製一幀的流程,從 Browser UI 執行緒收到 VSync 開始,Layer Compositor 收到 BeginFrame 訊號後產生新的 CompositorFrame 傳送給位於 Browser UI 執行緒的 Display Compositor 合成輸出,合成輸出的 GL 指令在 GPU 執行緒被執行。

Chromium Viz 淺析 - 合成器架構篇

非 OOP-D 的合成器架構,繪圖來源自 Compositor Stack

參考上圖,實際的合成器架構比上面的描述要複雜,在 Chrome for Android,Browser 和 Renderer 都會各建立一個 Layer Compositor,Browser 建立的 Layer Compositor 會建立一個 SurfaceLayer 作為 Renderer Layer Compositor 對應 Surface 的 placeholder,Browser Layer Compositor 同時還可以 Attach 其它 UILayer 用來繪製瀏覽器 UI,當 Display Compositor 聚合 Surface 時,Browser Layer Compositor 對應的 Surface 會巢狀 Renderer Layer Compositor 對應的 Surface,網頁和瀏覽器 UI 最終被聚合成一個完整的 CompositorFrame 一起合成輸出。

OOP-D 的合成器架構

Chromium Viz 淺析 - 合成器架構篇

OOP-D 的合成器架構,繪圖來源自 Compositor Stack

對於 OOP-D 的合成器架構,CompositorDependencies 會發送一個 FrameSinkManager 介面的連結請求給在 Viz 程序 VizCompositor 執行緒的 Viz Main Service,然後把該 FrameSinkManager 介面傳遞給 HostFrameSinkManager。當 HostFrameSinkManager 接收到 Renderer 端對 CompositorFrameSink 介面的連結請求時,它會透過 FrameSinkManager 介面轉交給遠端的 Viz Service 處理。

void HostFrameSinkManager::CreateCompositorFrameSink(

const FrameSinkId& frame_sink_id,

mojom::CompositorFrameSinkRequest request,

mojom::CompositorFrameSinkClientPtr client) {

。。。

frame_sink_manager_->CreateCompositorFrameSink(

frame_sink_id, std::move(request), std::move(client));

}

frame_sink_manager_ 在開啟 OOP-D 時,是一個 Mojo 介面物件,如果沒有開啟 OOP-D,則指向一個 Local FrameSinkManagerImpl 物件

CompositorImpl 同樣需要為 Browser Layer Compositor 建立一個 CompositorFrameSink 介面的連結,它是一個 Root CompositorFrameSink,遠端的 Viz Service 在建立該 CompositorFrameSink 介面連結的同時,也建立了一個 Display Compositor 用於合成輸出。

Chromium Viz 淺析 - 合成器架構篇

為了除錯方便,截圖使用的 Chromium 都執行在單程序模式

從上圖我們可以看到跟非 OOP-D 相比,除了 VSync 的觸發,Display Compositor 的合成輸出都遷移到了 Viz 程序的 VizCompositor 執行緒外,其它並沒有太大的差異。不過 Display Compositor 的遷移僅僅是 OOP-D 的第一階段,真正的差異會在後續的改動中體現,包括:

Display Compositor 移除 Command Buffer 的使用,因為 VizCompositor 執行緒和 GPU 執行緒已經同在一個程序,它們之間可以使用執行緒通訊而不需要使用 Command Buffer;

Display Compositor 移除 Command Buffer 後,可以使用 Vulkan 來取代 GL,因為 Command Buffer 不支援 Vulkan 也不打算支援,所以 1 是 2 的前置條件;