Chromium Canvas工作流

2023-10-31 18:02:35

blink 中實現了2種 canvas,分別是 blink::HTMLCanvasElement 和 blink::OffscreenCanvas ,前者對應 html/dom 中的 canvas,後者對應 js 中的 OffscrenCanvas。

html canvas 有兩種模式,一種是常規模式,這種模式下 canvas 的繪製時機受 viz/cc 的排程,和網頁上的其他 dom 繪製的時機一致。另一種是低延遲模式 desynchronized = true,此時 canvas 的繪製會脫離 dom,它會作為一個獨立的 viz client 使用 CanvasResourceDispatcher 來自主向 viz 提交要顯示的畫面(MAC 下還不支援低延遲模式 crbug.com/945835)。

OffscreenCanvas 可以脫離 dom 存在,原理類似 html canvas 的低延遲模式,也是作為一個獨立的 viz client 存在,可以自主向 viz 提交要顯示的畫面。不同的是它可以跑在 worker 執行緒中,從而避免阻塞 blink 執行緒(執行緒名 CrRenderMain,cc 的繪製執行緒),而 html canvas 的低延遲模式只能跑在 blink 執行緒。

要在 canvas 上繪製內容,需要先獲取繪製 context,最常用的就是 2d context,它在 html canvas 和 OffscreenCanvas 下有不同的實現, 分別為 blink::CanvasRenderingContext2D 和 blink::OffscreenCanvasRenderingContext2D,區別可以理解為後者只支援低延遲渲染模式,而前者不僅支援低延遲渲染模式,同時支援常規 canvas 渲染模式。

除了 2d context,以下這些 context 在兩種 canvas 中都可以使用:

 1. 網頁渲染流程簡介

由於 canvas 是網頁內容的一部分,很難在不瞭解網頁渲染流程的情況下單獨理解 canvas 的渲染,因此這裡先介紹下網頁渲染的一般流程。

網頁的渲染鏈路非常長,由於這裡的重點是 canvas,因此只做簡單介紹,不會過多展開,後續會有專門的文章介紹。

下面是網頁渲染的全鏈路流程簡圖 blink-1000:

 

下面簡單介紹整個流程:

  • vsync: 瀏覽器一幀的渲染從 vsync 訊號開始,它會通知 render 程序中的 cc compositor 執行緒(或者叫 cc impl 執行緒)開始新的一幀;
  • BeginFrame: cc compositor 執行緒緊接著通知 cc render 執行緒進行內容的繪製;
  • DOM: 此時 blink 開始工作,它會先解析 html 生成 DOM 樹;
  • Javascript: 此時如果註冊有 requestAnimationFrame 回撥或者互動事件回撥,則會在此時執行(樁點1);
  • Styles + Layout: 然後計算每個節點的樣式以及對每個節點進行佈局排版;
  • Paint: 之後開始繪製,不同類別的 DOM 元素採用不同的繪製方法(樁點2),繪製完成之後進行合成,最終產出 cc::Layer 樹,然後 blink 通知 cc compositor 執行緒繪製完成;
  • Commit: cc compositor 會從 cc::Layer 樹構建自己的 cc::LayerImpl 樹;
  • Tiles: 然後根據網頁視口的範圍/頁面的縮放比例將 cc::LayerImpl 進行分塊(Tiles);CompositorFrame: 回到 cc compositor 執行緒,他在分發完 raster 任務之後會根據 cc::LayerImpl 樹構建 viz::CompositorFrame 物件,該物件表示一幀繪製內容(並不一定是整個網頁,參考後面的canvas低延遲模式介紹),它會被提交(submit)到 viz compsoitor 執行緒中進行合成;
    • Raster Tasks: 這些分塊會被送往 worker 執行緒進行 raster;
    • Raster: worker 會把raster任務序列化到 commandbuffer, 並通知 CrGpuMain 執行緒進行真正的 raster 。
  • viz Composite: viz compositor 把多個 CF 合成為完整的頁面(樁點3),然後提交到 compositor gpu 執行緒中;
  • Display: compositor gpu 呼叫 GL 進行真正的繪製以及上屏。

我在上面的流程中埋了3個樁點,這三個樁點就是 canvas 渲染涉及到的三個重要節點。下面會把 canvas 的不同流程插入到這些節點中去。

2. Canvas 類圖

為了講清楚 canvas 的實現原理,方便下文的描述,這裡先看下 Canvas 相關的類圖:

 

 3. 獲取用於繪製的 Context

開發者通過 canvas.getContext("XXX") 來獲取 context 物件,這個 js api 會通過 blink::HTMLCanvasElement::GetCanvasRenderingContext 方法來獲取 context。每種型別的 context 都有對應的 Factory 工廠類,所有這些類都註冊在一個靜態字典中,建立的時候根據 context 型別找到對應的工廠類,然後使用工廠類就可以直接建立 context 物件了。核心邏輯如下:

 js 中的 context 物件對應 C++ 中的 blink::CanvasRenderingContext 物件。不同型別的 js context 分別對應 blink::CanvasRenderingContext 的不同子類,對應關係如下:

 4. 向 Canvas 中繪製內容

js 呼叫 context.drawXXX 方法向 canvas 中繪製內容時,會呼叫到 C++ blink::CanvasRenderingContext 中對應的方法,對於 2d context, 則對應 blink::CanvasRenderingContext2D。它內部定義了所有 2d context 可以使用的 API,這些 API 分佈於三個具有繼承關係的類中:

 

所有的繪製操作都通過 cc::PaintCanvas 記錄到 blink::CanvasResourceProvider 中。 cc::PaintCanvas 有個子類 cc::RecordPaintCanvas,專門用來把 2d 繪製操作記錄到 cc::DisplayItemList 中,它只記錄繪製操作而不會進行真正的繪製。

 

cc 提供了一個 cc::PaintRecorder 類,專門用來錄製繪製操作,相關類圖如下:

 

 5. 完成繪製,提交結果

當所有的 js 繪製指令執行完畢之後,html canvas 在 2d context 下不需要顯式的提交結果(C++內部會自動 flush),這點和 OffscreenCanvas 以及非 2d context 不同,這些模式都需要顯示的提交繪製結果(在某些情況下也可以省略)。

6. 低延遲模式下取出 Canvas 資料

低延遲模式下,canvas 的每次繪製流程開始前都會設定一個標記,表示有新內容繪製了,此時會註冊回撥監聽 blink 執行緒中當前任務結束的回撥,在這個回撥中觸發 Canvas 內容的 Raster 以及提交。

繪製前註冊回撥的流程:

 註冊回撥:

Raster 完成之後, CanvasResource 會通過 blink::CanvasResourceDispatcher::DispatchFrame 合成 CompositorFrame 然後提交。

從 CanvasResource 中取出 Raster 的結果,建立 viz::TransferableResource

 建立 CompositorFrame 並提交資源:

 7. 總結

 

Canvas 從開始繪製到上屏經過以下流程:

 

  • canvas 初始化,獲取 CanvasRenderingContext;
  • js 呼叫繪製 API 進行繪製,繪製的結果被 cc::RecordPaintCanvas 錄製下來,儲存在 blink::CanvasResourceProvider 中的 cc::PaintRecord
  • 在普通 Canvas 模式下提交繪製結果:
    • blink 進入繪製流程,從 blink::Canvas2DLayerBridge 中獲取 cc::TextureLayer;
    • 在提交到 cc compositor 執行緒之前,呼叫 cc::TextureLayer::Update 觸發 cc::PaintRecord 的 Raster,使用 OOP-R 機制將 Raster 任務傳送到 CrGpuMain 執行緒進行 Raster,返回參照 Raster 結果的 gpu::Mailbox
    • 然後用 Raster 的結果 gpu::Mailbox 建立 viz::TransferableResource 並存入 cc::TextureLayer 中進行提交;
    • 將 cc::TextureLayer 和網頁中的其他元素一起提交到 cc compositor 執行緒,在那裡建立 viz::CompositorFrame 然後提交到 viz;
  • 在低延遲 Canvas 模式下提交繪製結果:viz compositor 收到 CompositorFrame 之後等待合適的時機進行上屏;
    • 當有繪製的時候註冊 blink 執行緒任務結束回撥;
    • 當前任務結束之後,觸發 blink::CanvasRenderingContext::DidProcessTask
    • 然後 flush canvas,將 cc::PaintRecord 進行 Raster,使用 OOP-R 機制將 Raster 任務傳送到 CrGpuMain 執行緒進行 Raster,返回參照 Raster 結果的 gpu::Mailbox;
    • 然後在 blink::CanvasResourceDispatcher::DispatchFrame 中建立 viz::CompositorFrame 包裝 Canvas 的內容,並提交到 viz compositor 執行緒進行合成;

8. 參考文獻

https://keyou.github.io/blog/2022/12/01/canvas/