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/
查看原文
該文章在 2023/11/27 11:17:38 編輯過