blink 中實(shí)現(xiàn)了2種 canvas,分別是 blink::HTMLCanvasElement 和 blink::OffscreenCanvas ,前者對(duì)應(yīng) html/dom 中的 canvas,后者對(duì)應(yīng) js 中的 OffscrenCanvas。
html canvas 有兩種模式,一種是常規(guī)模式,這種模式下 canvas 的繪制時(shí)機(jī)受 viz/cc 的調(diào)度,和網(wǎng)頁上的其他 dom 繪制的時(shí)機(jī)一致。另一種是低延遲模式 desynchronized = true,此時(shí) canvas 的繪制會(huì)脫離 dom,它會(huì)作為一個(gè)獨(dú)立的 viz client 使用 CanvasResourceDispatcher 來自主向 viz 提交要顯示的畫面(MAC 下還不支持低延遲模式 crbug.com/945835)。
OffscreenCanvas 可以脫離 dom 存在,原理類似 html canvas 的低延遲模式,也是作為一個(gè)獨(dú)立的 viz client 存在,可以自主向 viz 提交要顯示的畫面。不同的是它可以跑在 worker 線程中,從而避免阻塞 blink 線程(線程名 CrRenderMain,cc 的繪制線程),而 html canvas 的低延遲模式只能跑在 blink 線程。
要在 canvas 上繪制內(nèi)容,需要先獲取繪制 context,最常用的就是 2d context,它在 html canvas 和 OffscreenCanvas 下有不同的實(shí)現(xiàn), 分別為 blink::CanvasRenderingContext2D 和 blink::OffscreenCanvasRenderingContext2D,區(qū)別可以理解為后者只支持低延遲渲染模式,而前者不僅支持低延遲渲染模式,同時(shí)支持常規(guī) canvas 渲染模式。
除了 2d context,以下這些 context 在兩種 canvas 中都可以使用:
1. 網(wǎng)頁渲染流程簡介
由于 canvas 是網(wǎng)頁內(nèi)容的一部分,很難在不了解網(wǎng)頁渲染流程的情況下單獨(dú)理解 canvas 的渲染,因此這里先介紹下網(wǎng)頁渲染的一般流程。
網(wǎng)頁的渲染鏈路非常長,由于這里的重點(diǎn)是 canvas,因此只做簡單介紹,不會(huì)過多展開,后續(xù)會(huì)有專門的文章介紹。
下面是網(wǎng)頁渲染的全鏈路流程簡圖 blink-1000:
下面簡單介紹整個(gè)流程:
vsync: 瀏覽器一幀的渲染從 vsync 信號(hào)開始,它會(huì)通知 render 進(jìn)程中的 cc compositor 線程(或者叫 cc impl 線程)開始新的一幀;
BeginFrame: cc compositor 線程緊接著通知 cc render 線程進(jìn)行內(nèi)容的繪制;
DOM: 此時(shí) blink 開始工作,它會(huì)先解析 html 生成 DOM 樹;
Javascript: 此時(shí)如果注冊(cè)有 requestAnimationFrame 回調(diào)或者交互事件回調(diào),則會(huì)在此時(shí)執(zhí)行(樁點(diǎn)1);
Styles + Layout: 然后計(jì)算每個(gè)節(jié)點(diǎn)的樣式以及對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行布局排版;
Paint: 之后開始繪制,不同類別的 DOM 元素采用不同的繪制方法(樁點(diǎn)2),繪制完成之后進(jìn)行合成,最終產(chǎn)出 cc::Layer 樹,然后 blink 通知 cc compositor 線程繪制完成;
Commit: cc compositor 會(huì)從 cc::Layer 樹構(gòu)建自己的 cc::LayerImpl 樹;
Tiles: 然后根據(jù)網(wǎng)頁視口的范圍/頁面的縮放比例將 cc::LayerImpl 進(jìn)行分塊(Tiles);CompositorFrame: 回到 cc compositor 線程,他在分發(fā)完 raster 任務(wù)之后會(huì)根據(jù) cc::LayerImpl 樹構(gòu)建 viz::CompositorFrame 對(duì)象,該對(duì)象表示一幀繪制內(nèi)容(并不一定是整個(gè)網(wǎng)頁,參考后面的canvas低延遲模式介紹),它會(huì)被提交(submit)到 viz compsoitor 線程中進(jìn)行合成;
Raster Tasks: 這些分塊會(huì)被送往 worker 線程進(jìn)行 raster;
Raster: worker 會(huì)把raster任務(wù)序列化到 commandbuffer, 并通知 CrGpuMain 線程進(jìn)行真正的 raster 。
viz Composite: viz compositor 把多個(gè) CF 合成為完整的頁面(樁點(diǎn)3),然后提交到 compositor gpu 線程中;
Display: compositor gpu 調(diào)用 GL 進(jìn)行真正的繪制以及上屏。
我在上面的流程中埋了3個(gè)樁點(diǎn),這三個(gè)樁點(diǎn)就是 canvas 渲染涉及到的三個(gè)重要節(jié)點(diǎn)。下面會(huì)把 canvas 的不同流程插入到這些節(jié)點(diǎn)中去。
2. Canvas 類圖
為了講清楚 canvas 的實(shí)現(xiàn)原理,方便下文的描述,這里先看下 Canvas 相關(guān)的類圖:
3. 獲取用于繪制的 Context
開發(fā)者通過 canvas.getContext("XXX")
來獲取 context 對(duì)象,這個(gè) js api 會(huì)通過 blink::HTMLCanvasElement::GetCanvasRenderingContext 方法來獲取 context。每種類型的 context 都有對(duì)應(yīng)的 Factory 工廠類,所有這些類都注冊(cè)在一個(gè)靜態(tài)字典中,創(chuàng)建的時(shí)候根據(jù) context 類型找到對(duì)應(yīng)的工廠類,然后使用工廠類就可以直接創(chuàng)建 context 對(duì)象了。核心邏輯如下:
js 中的 context 對(duì)象對(duì)應(yīng) C++ 中的 blink::CanvasRenderingContext
對(duì)象。不同類型的 js context 分別對(duì)應(yīng) blink::CanvasRenderingContext
的不同子類,對(duì)應(yīng)關(guān)系如下:
4. 向 Canvas 中繪制內(nèi)容
js 調(diào)用 context.drawXXX
方法向 canvas 中繪制內(nèi)容時(shí),會(huì)調(diào)用到 C++ blink::CanvasRenderingContext
中對(duì)應(yīng)的方法,對(duì)于 2d context, 則對(duì)應(yīng) blink::CanvasRenderingContext2D
。它內(nèi)部定義了所有 2d context 可以使用的 API,這些 API 分布于三個(gè)具有繼承關(guān)系的類中:
所有的繪制操作都通過 cc::PaintCanvas
記錄到 blink::CanvasResourceProvider
中。 cc::PaintCanvas
有個(gè)子類 cc::RecordPaintCanvas
,專門用來把 2d 繪制操作記錄到 cc::DisplayItemList 中,它只記錄繪制操作而不會(huì)進(jìn)行真正的繪制。
cc 提供了一個(gè) cc::PaintRecorder
類,專門用來錄制繪制操作,相關(guān)類圖如下:
5. 完成繪制,提交結(jié)果
當(dāng)所有的 js 繪制指令執(zhí)行完畢之后,html canvas 在 2d context 下不需要顯式的提交結(jié)果(C++內(nèi)部會(huì)自動(dòng) flush),這點(diǎn)和 OffscreenCanvas 以及非 2d context 不同,這些模式都需要顯示的提交繪制結(jié)果(在某些情況下也可以省略)。
6. 低延遲模式下取出 Canvas 數(shù)據(jù)
低延遲模式下,canvas 的每次繪制流程開始前都會(huì)設(shè)置一個(gè)標(biāo)記,表示有新內(nèi)容繪制了,此時(shí)會(huì)注冊(cè)回調(diào)監(jiān)聽 blink 線程中當(dāng)前任務(wù)結(jié)束的回調(diào),在這個(gè)回調(diào)中觸發(fā) Canvas 內(nèi)容的 Raster 以及提交。
繪制前注冊(cè)回調(diào)的流程:
注冊(cè)回調(diào):
Raster 完成之后, CanvasResource
會(huì)通過 blink::CanvasResourceDispatcher::DispatchFrame
合成 CompositorFrame 然后提交。
從 CanvasResource
中取出 Raster 的結(jié)果,創(chuàng)建 viz::TransferableResource
:
創(chuàng)建 CompositorFrame 并提交資源:
7. 總結(jié)
Canvas 從開始繪制到上屏經(jīng)過以下流程:
canvas 初始化,獲取 CanvasRenderingContext
;
js 調(diào)用繪制 API 進(jìn)行繪制,繪制的結(jié)果被 cc::RecordPaintCanvas
錄制下來,保存在 blink::CanvasResourceProvider
中的 cc::PaintRecord
;
在普通 Canvas 模式下提交繪制結(jié)果:
blink 進(jìn)入繪制流程,從 blink::Canvas2DLayerBridge
中獲取 cc::TextureLayer
;
在提交到 cc compositor 線程之前,調(diào)用 cc::TextureLayer::update
觸發(fā) cc::PaintRecord
的 Raster,使用 OOP-R 機(jī)制將 Raster 任務(wù)發(fā)送到 CrGpuMain 線程進(jìn)行 Raster,返回引用 Raster 結(jié)果的 gpu::Mailbox
;
然后用 Raster 的結(jié)果 gpu::Mailbox
創(chuàng)建 viz::TransferableResource
并存入 cc::TextureLayer
中進(jìn)行提交;
將 cc::TextureLayer
和網(wǎng)頁中的其他元素一起提交到 cc compositor 線程,在那里創(chuàng)建 viz::CompositorFrame
然后提交到 viz;
在低延遲 Canvas 模式下提交繪制結(jié)果:viz compositor 收到 CompositorFrame 之后等待合適的時(shí)機(jī)進(jìn)行上屏;
當(dāng)有繪制的時(shí)候注冊(cè) blink 線程任務(wù)結(jié)束回調(diào);
當(dāng)前任務(wù)結(jié)束之后,觸發(fā) blink::CanvasRenderingContext::DidProcessTask
;
然后 flush canvas,將 cc::PaintRecord
進(jìn)行 Raster,使用 OOP-R 機(jī)制將 Raster 任務(wù)發(fā)送到 CrGpuMain 線程進(jìn)行 Raster,返回引用 Raster 結(jié)果的 gpu::Mailbox;
然后在 blink::CanvasResourceDispatcher::DispatchFrame
中創(chuàng)建 viz::CompositorFrame
包裝 Canvas 的內(nèi)容,并提交到 viz compositor 線程進(jìn)行合成;
8. 參考文獻(xiàn)
https://keyou.github.io/blog/2022/12/01/canvas/
查看原文
該文章在 2023/11/27 11:17:38 編輯過