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:
下面簡單介紹整個流程:
我在上面的流程中埋了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 從開始繪製到上屏經過以下流程:
CanvasRenderingContext
;cc::RecordPaintCanvas
錄製下來,儲存在 blink::CanvasResourceProvider
中的 cc::PaintRecord
;blink::Canvas2DLayerBridge
中獲取 cc::TextureLayer
;cc::TextureLayer::Update
觸發 cc::PaintRecord
的 Raster,使用 OOP-R 機制將 Raster 任務傳送到 CrGpuMain 執行緒進行 Raster,返回參照 Raster 結果的 gpu::Mailbox
;gpu::Mailbox
建立 viz::TransferableResource
並存入 cc::TextureLayer
中進行提交;cc::TextureLayer
和網頁中的其他元素一起提交到 cc compositor 執行緒,在那裡建立 viz::CompositorFrame
然後提交到 viz;blink::CanvasRenderingContext::DidProcessTask
;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/