Chromium VIZ架構詳解

rmb_999發表於2023-11-02

1. VIZ的三個端

在設計層面上 viz 的架構如下圖所示:

在設計上 viz 分了三個端,分別是 client 端, host 端和 service 端。

client 端用於生成要顯示的畫面(CF)。應用中至少有一個 root client,可以有多個 child client,它們組成了一個 client 樹,每個 Client 都有一個 FrameSinkId 和一個 LocalSurfaceId,如果父子 client 之間的 UI 需要嵌入,則子 client 作為 SurfaceDrawQuad 嵌入到父 client 中。在 Chromium 中,每一個瀏覽器視窗都對應一個 client 樹,擁有一個 root client 和零個或多個子 client。比如,網頁中的一個 OOPIF 可以是一個子 client,OffscreenCanvas 也是一個子 client。每個 client 可以獨立進行重新整理,生成獨立的 CF。client 端的核心介面為 viz::mojom::CompositorFrameSinkClient, 開發者可以透過繼承該類來建立一個 client。

host 端用於將 client 註冊到 service,只能執行在特權端(沒有沙箱,比如瀏覽器程式),負責協助 client 建立起和 service 的連線,也負責建立 client 和 client 之間的樹形關係。所有的 client 想要生效必須經過 host 進行註冊。host 端的核心介面為 viz::mojom::FrameSinkManagerClient,以及其實現類 viz::HostFrameSinkManager

service 端執行 Viz 的核心,進行 UI 合成以及最終的渲染。它接收所有 client 端生成的 CF,然後把這些 CF 進行合成,並最終顯示在視窗中。它內部會為每個 client 建立一個 viz::CompositorFrameSink(Impl/Support),然後透過 viz::Display 將這些 CF 進行合成,最後透過 viz::DirectRenderer 將這些 CF 渲染到 viz::OutputSurface 中,viz::OutputSurface 包裝了各種渲染目標,比如視窗,記憶體等。

在 Chromium 的具體實現中 viz 的架構可以用下圖表示:

 綠色部分為 client 的實現,負責提交 CF 到 GPU, 橙色部分 ui::Compositor 為 host 的實現,負責將所有的 client 註冊到 service,紅色部分為 service 端的實現,負責接收、合成,並最終渲染 CF。

2. VIZ 的執行緒架構

 

viz 是多執行緒架構,其中最重要的有兩個執行緒,一個是 VizCompositorThread,也稱為(viz的) Compositor 執行緒(注意和 cc 中的 Compositor 執行緒區分,它們沒有關係)。另一個是 CrGpuMain (多程式架構下)或者 Chrome_InProcGpuThread(單程式架構下)執行緒,也稱為 GPU 執行緒(只有在開啟了硬體加速渲染時才存在)。幀率的排程,CF 的合成,DrawQuad 的繪製都發生在Compositor執行緒中,viz::Displayviz::DisplaySchedulerviz::SurfaceAggregatorviz::DirectRenderer 也執行在該執行緒中。而所有最終真實的繪製(比如 GL 的執行,Real SwapBuffer)最終都執行在 CrGpuMain 或 Chrome_InProcGpuThread 執行緒中。

 

如果要在架構上體現這兩個執行緒,可以這樣劃分:

虛線之上執行在 Compositor 執行緒,虛線之下執行在 GPU 執行緒,OutputSurface 需要對接 DirectRenderer 和最下層的渲染,所以不同部分執行在不同的執行緒中。

目前 Compositor 執行緒和 GPU 執行緒之間的資料傳遞有兩種方式,一種是先將 GL 呼叫放入程式內的 CommandBuffer,然後在 GPU 執行緒中取出 GL 命令並執行,GLOutputSurface 使用這種方式。另一種是直接透過 PostTask 將要渲染的操作傳送到 GPU 執行緒,SkiaOutputSurface 使用這種方式(關於 GLOutputSurface 和 SkiaOutputSurface 見下文)。

3. VIZ 的類圖

下面是 viz 更詳細的類圖:

 

 

  • viz::mojom::CompositorFrameSinkClient 作為 client,表示一個畫面的來源;
  • viz::CompositorFrameSink(Impl/Support) 用於處理 CF 的地方,一個 client 可以有多個 CompositorFrameSink;
  • viz::RootCompositorFrameSinkImpl 作用和 CompositorFrameSink 類似,只不過專門處理 root client,每個 client 有且只有一個該物件。它還負責為對應的 client 初始化渲染環境,包括 OutputSurface, Display 的建立。
  • viz::FrameSinkManagerImpl 用於管理 CompositorFrameSink, 包括其建立和銷燬;
  • viz::SurfaceAggregator 負責 Surface/CF 的合成,比如dirty區域的計算等,不負責繪製;
  • viz::OutputSurface 封裝渲染目標,和各平臺的渲染目標直接對接;
  • viz::DirectRenderer 封裝繪製 DrawQuad 的方式,負責將 DrawQuad 繪製到 OutputSurface 上;
  • viz::Display 一箇中控類,將 SurfaceAggregator/DirectRenderer 以及 Overlay 的功能串起來形成流水線;
  • viz::DisplayScheduler 排程 viz::Display 何時應該採取行動;

4. VIZ 的渲染目標

 

viz::DirectRenderer 和 viz::OutputSurface 用於管理渲染目標 。他們對理解 Chromium UI 的呈現方式至關重要。這兩個類並不是相互獨立的,在 Chromium 中他們有以下組合:

 

    1. viz::GLRenderer + viz::GLOutputSurface
      GLRenderer 已經被標記為 deprecated, 未來會被 SkiaRenderer 取代。它使用基於 CommandBuffer 的 GL Context 來渲染 DrawQuad 到 GLOutputSurface 上,GLOutputSurface 使用視窗控制程式碼建立 Native GL Context。GL 呼叫發生在 VizCompositorThread 執行緒中,透過 InProcessCommandBuffer 這些 GL 呼叫最終在 CrGpuMain 執行緒中執行。關於 CommandBuffer 相關內容可以參考 Chromium Command Buffer
      GLOutputSurface 有一系列的子類,不同的子類對接不同平臺的渲染目標,比如 GLOutputSurfaceAndroid 用於對接Android平臺的渲染,GLOutputSurfaceOffscreen 用於支援 GL 的離屏渲染等。

    2. viz::SkiaRenderer + viz::SkiaOutputSurface(Impl) + viz::SkiaOutputDevice
      SkiaRenderer 是未來的發展方向,以後所有其他的渲染方式都會被這種方式取代。因為它具有最大的靈活性,同時支援GL渲染,Vulkan渲染,離屏渲染等。
      SkiaRenderer 將 DrawQuad 繪製到由 SkiaOutputSurfaceImpl 提供的 canvas 上,但是該 canvas 並不會進行真正的繪製動作,而是透過 skia 的 ddl(SkDeferredDisplayListRecorder) 機制把這些繪製操作記錄下來,等到所有的 RenderPass 繪製完成,這些被記錄下來的繪製操作會被透過 SkiaOutputSurfaceImpl::SubmitPaint 傳送到 SkiaOutputSurfaceImplOnGpu 中進行真實的繪製,根據名字可知該類執行在 GPU 執行緒中。
      SkiaOutputSurface 對渲染目標的控制是透過 SkiaOutputDevice 實現的,後者有很多子類,其中 SkiaOutputDeviceOffscreen 用於實現離屏渲染,SkiaOutputDeviceGL 用於GL渲染。

    3. viz::SoftwareRenderer + viz::SoftwareOutputSurface + viz::SoftwareOutputDevice
      SoftwareRenderer 用於純軟體渲染,當關閉硬體加速的時候使用該種渲染方式。這種方式邏輯相對簡單,因此留給讀者去探索

5. VIZ 的資料流

viz 中的核心資料為 CF,當使用者和網頁進行互動時,會觸發 UI 的改變,這最終會導致 viz client (比如 cc::LayerTreeHostImpl) 建立一個新的 CF 並使用 viz::mojom::CompositorFrameSink 介面將該 CF 提交到 service,service 中的 viz::CompositorFrameSinkSupport 會為該 CF 所屬的 client 建立一個 viz::Surface,然後把 CF 放入該 viz::Surface 中。當 servcie 中的 viz::DisplayScheduler 到達下一個排程週期的時候,會通知 viz::Display 取出當前的 viz::Surface,並交給 viz::SurfaceAggregator 進行合成,合成的結果會被交給 viz::DirectRenderer 進行渲染。viz::DirectRenderer 並不直接渲染,它從 viz::OutputSurface 中取出渲染表面,然後讓其子類在該渲染表面上進行繪製。viz::OutputSurface 封裝了渲染表面,比如視窗,記憶體bitmap等。繪製完成之後,viz::Display 會呼叫 viz::DirectRenderer::SwapBuffer 將該 CF 最終顯示出來。

service 繪製 CF 的核心邏輯位於 viz::Display::DrawAndSwap 中,下面是其主要的執行邏輯:

6. VIZ 的分層

從分層架構的角度看,viz 的 API 分了3層,分別為最底層核心實現,中間層mojo介面,最上層viz服務。

最底層是viz的核心實現,主要介面包括 viz::FrameSinkManager(Impl)viz::CompositorFrameSink(Support)viz::Displayviz::OutputSurface。 使用這些介面是使用 viz 最直接的方式,提供最大的靈活性。但這層介面不提供誇程式通訊的能力。 Chromium中直接使用這一層的介面的地方不多,具體demo參考 chromium_demo/demo_viz_offscreen.cc at c/80.0.3987 · keyou/chromium_demo

中間mojo層主要將底層API包裝到 mojo 介面中,這一層的核心介面包括 viz::HostFrameSinkManager,viz::mojom::FrameSinkManager,viz::mojom::CompositorFrameSink,viz::mojom::CompositorFrameSinkClient,viz::HostDisplayClient。這些介面大多對應底層的 API 介面,將對底層介面的呼叫轉換為對mojo介面的呼叫,因此這層介面提供誇程式通訊的能力。Chromium中幾乎所有使用viz的地方都是使用該層介面。viz模組提供的官方demo也是使用該層介面,也可以參考我寫的單檔案demo,見 chromium_demo/demo_viz_gui.cc at c/80.0.3987 · keyou/chromium_demo

最上層viz服務介面主要將 viz 服務化,提供將viz執行在獨立程式的能力。這一層的主要介面包括 viz::GpuHostImplviz::GpuServiceImplviz::VizMainImplviz::Gpu。這些介面需要和中間層mojo介面配合才能起作用,在 Chromium 的多程式架構中使用了該層介面。使用該層介面的demo參考 chromium_demo/demo_viz_gui_gpu.cc at c/80.0.3987 · keyou/chromium_demo

另外,在 viz 中還有一套專門用於 viz 中 GPU 渲染的介面 viz::*ContextProvider。它主要負責為 viz 初始化 GL 環境,使 viz 可以使用 GPU 進行渲染

7. VIZ 的 ID 設計

每一個 client 都至少對應一個 FrameSinkId 和 LocalSurfaceId,在 client 的整個生命週期中所有 FrameSink 的 client_id(見下文)都是固定的,而 LocalSurfaceId 會根據 client 顯示畫面的 size 或 scale factor 的改變而改變。他們兩個共同組成了 SurfaceId,用於在 service 端全域性標識一個 Surface 物件。也就是說對於每一個 Surface,都可以獲得它是由誰在什麼 size 或 scale facotr 下產生的。

FrameSinkId

  • client_id = uint32_t, 每個 client 都會有唯一的一個 ClientId 作為識別符號,標識一個 CompositorFrame 是由哪個 client 產生的,也就是標識 CompositorFrame 的來源;
  • sink_id = uint32_t, 在 service 端標識一個 CompositorFrameSink 例項,Manager 會為每個 client 在 service 端建立一個 CompositorFrameSink,專門用於處理該 client 生成的 CompositorFrame,也就是標識 CompositorFrame 的處理端;
  • FrameSinkId = client_id + sink_id,將 CF 的來源和處理者關聯起來。

FrameSinkId 可以由 FrameSinkIdAllocator 輔助類生成。

在實際使用中,往往一個程式是一個 client,專門負責一個業務模組 UI 的實現,而一個業務往往由很多的 UI 元素組成,因此可以讓每個 FrameSink 負責一部分的 UI,此時就用到了單 client 多 FrameSink 機制,這種機制可以實現 UI 的區域性重新整理(多 client 也能實現這一點)。在 chrome 中瀏覽器主程式是一個 client,而每一個外掛都是一個獨立的 client,網頁中除了一些特殊的元素比如 iframe和offscreen canvas位於單獨的client中,其他元素全部都位於同一個client中。

LocalSurfaceId

  • parent_sequence_number = uint32_t, 當自己作為父 client,並且 surface 的 size 和 device scale factor 改變的時候改變;
  • child_sequence_number = uint32_t, 當自己作為子 client,並且 surface 的 size 和 device scale factor 改變的時候改變;
  • embed_token = 可以理解為一個隨機數, 用於避免 LocalSurafaceId 可猜測,當父 client 和子 client 的父子關係改變的時候改變;
  • LocalSurfaceId = parent_sequence_number + child_sequence_number + embed_token,當 client 的 size 當前 client 產生的畫面改變。

LocalSurfaceId 可以由 ParentLocalSurfaceIdAllocator 或者 ChildLocalSurfaceIdAllocator 這兩個輔助類生成。前者用於由父 client 負責生成自己的 LocalSurfaceId 的時候,後者用於由子 client 自己負責生成自己的 LocalSurfaceId 的時候。使用哪種方式要看自己的 UI 元件之間的依賴關係的設計。

LocalSurfaceId 在很多時候都包裝在 LocalSurfaceIdAllocation 內,該類記錄了 LocalSurfaceId 的建立時間。改時間在建立 CF 的時候需要用到。

SurfaceId

SurfaceId = FrameSinkId + LocalSurfaceId

SurfaceId 全域性唯一記錄一個顯示畫面,它可以被嵌入其他的 CF 或者 RenderPass 中,從而實現顯示介面的嵌入和區域性重新整理。除了在 Client 端使用 SurfaceDrawQuad 進行 Surface 巢狀之外,大部分應用場景都在 service 端。

8. 參考文獻

https://keyou.github.io/blog/2020/07/29/how-viz-works/

 

 

相關文章