天天看點

How cc Works 中文譯文

Chromium 的工程師們寫了兩篇技術文章 [How Blink Works] 1 How cc Works

,分别介紹了 Chrome 浏覽器核心内部的兩個重要子產品 Blink 和 cc 内部設計和實作的一些細節。對于想要了解 Chromium 核心内部實作的同學,這兩篇文章提供了不錯的入門指引。在征得作者同意後,我将其翻譯成中文,以饋讀者。

文中部分術語覺得難以翻譯成合适的中文的,我會保留原文。對于一些較為複雜的概念會根據自己的了解給出一些額外的解釋。如果有我了解有誤,沒有正确翻譯的地方,也請讀者留言指出。

另外,How cc Works 的内容比較深,要看明白這篇文章的内容,多少需要對網頁渲染有一定的了解,起碼需要了解光栅化,合成,圖層,分塊這些概念,需要了解軟體合成和 gpu 合成,軟體光栅化和 gpu 光栅化的差別。

Chromium 目前實際支援三種不同的光栅化和合成的組合方式:軟體光栅化 + 軟體合成,軟體光栅化 + gpu 合成,gpu 光栅化 + gpu 合成。在移動平台上,大部分裝置和移動版網頁使用的都是 gpu 光栅化 + gpu 合成的渲染方式,理論上性能也最佳。

tl;dr

cc/

因為曆史原因被稱為 Chrome Compositor,但目前已經不太準确。它既不是唯一的 chrome compositor(當然我們有很多個),甚至可以說不再是合成器。 danakj 建議使用 “content collator” 作為替代名稱。

在 browser 程序,cc 是在 ui/compositor 或 Android 平台相關的代碼中被使用,在 mus utility 程序中,它是在 ui/compositor 中被使用。cc 也通過 Blink/RenderWidget 嵌入到 renderer 程序中。cc 負責從它的 embedder 中擷取繪制輸入,找出它們在螢幕上呈現的位置,根據繪制輸入生成對應的 gpu 紋理,包括光栅化,圖檔解碼,運作圖檔動畫,最終以 compositor frame 的方式将這些紋理轉發給 display compositor。cc 還處理從 browser 程序轉發的輸入事件,在不涉及 Blink 的情況下,可以及時地處理雙指縮放和滾動手勢。

[譯注]

Chromium 的合成器架構在最近和未來的一段時間變化很大,總的說來真正負責合成的部分已經遷移到 viz,目前 display compositor 位于 viz(viz 運作在 viz 程序,也就是以前的 gpu 程序)。cc 從目前的定位來看是作為 viz client 而存在,将不同的繪制内容來源轉換成用于最終合成的輸入,也吻合了上面的 "content collator" 的稱謂。不過如果能夠接受 layer compositor 這種 virtual compositor 的概念的話,cc 目前仍然保有這部分功能,繼續将其稱為合成器也未嘗不可,特别是從頁端的角度來了解合成器,對應的仍然是 cc。

程序/線程架構

cc 可以被 embedders 以單線程或者多線程的方式使用。單線程版本的額外開銷較小。 多線程版本會有延遲開銷,但允許在一個線程繁忙時,另外一個線程可以及時地處理輸入事件和動畫。 通常 browser 使用單線程版本,因為它的主線程任務更簡單負荷更輕量,而 renderer 使用多線程版本,因為它的主線程(Blink)在某些頁面上可能非常繁忙。

單線程和多線程版本都使用 cc::Scheduler 來驅動自己,它決定了何時送出幀。 一個例外(第三種模式僅在這個地方使用)是 Blink layout 測試和 sim 測試,它們不會(總是)使用 Scheduler,而是通過 LayerTreeHost::Composite 直接請求 cc 同步地合成輸出。 這是出于曆史原因,并且也是測試過程需要更直接的控制。

内容繪制資料流動概覽

How cc Works 中文譯文

embedders 調用 cc 的主要接口是通過 LayerTreeHost(使用不同的 LayerTreeSettings 設定項建立)和一棵圖層樹。 圖層是一個包含内容的矩形區域,包括描述該内容應如何在螢幕上呈現的各種屬性。 cc 負責将該内容的繪制表示(也就是 PaintRecord)轉換為光栅化表示(software 位圖或 gpu 紋理)并确定該矩形在螢幕上的顯示位置。

cc 通過 PropertyTreeBuilder 将此圖層樹輸入轉換為一組屬性樹,并将圖層樹簡化為可見圖層的有序清單。作為 slimming paint 項目的一部分,Blink 将直接設定屬性樹和圖層清單,而不是通過更原始的圖層樹接口,進而避免渲染流水線在這部分上的耗時。

在送出過程中,cc 将所有來自主線程的資料結構輸入轉換成合成器線程的資料結構。此時,cc 确定每個圖層的哪些部分可見,然後繼續解碼圖檔和光栅化内容。一旦所有内容準備好出現在螢幕上,cc 就會 activates 已送出的樹,然後就可以“繪制”它。

不幸的是,cc 仍然在許多地方使用 “draw” 和 “swap” 的稱謂,盡管事實上它不再是真正的“繪制”和“前後緩沖區交換”。 cc 中的 “Draw” 意味着構造一個包含 quads 和 render passes 的 compositor frame,以便在螢幕上最終繪制。 cc 中的 “Swap” 則意味着通過 CompositorFrameSink 将該幀送出給 display compositor。這些幀被發送到 viz 的 SurfaceAggregator,在那裡所有來自不同的幀生産者的 compositor frame 被聚集在一起。

輸入事件資料流動概覽

cc 的另一個主要輸入是使用者輸入事件,例如滑鼠點選,滑鼠滾輪和觸屏手勢。 輸入事件是從 browser 程序轉發到 renderer 程序中。 由 ui::InputHandlerProxy(實作 cc::InputHandlerClient 接口)負責處理。

其中一些輸入事件在特定時間會被發送到 LayerTreeHostImpl(實作 cc::InputHandler 接口)。 這允許它修改活躍圖層的屬性樹并根據需要滾動或縮放圖層。某些輸入事件無法在合成器線程處理(例如,存在需要同步調用的 Javascript 觸屏或者滾輪事件處理器),是以這些輸入事件将被轉發到 Blink 去處理。 輸入事件的資料流動路徑跟上面的内容繪制的資料流動路徑是剛好相反的。

送出流程

Commit 是一種從主線程推送資料到合成器線程的方式,并且保證了該過程中的資料完整性。 (cc 即使在單線程模式下運作,仍然需要通過 Commit 來推送資料到正确的資料結構)Commit 不是通過發送 ipc,而是通過阻塞主線程并複制資料的方式來完成送出。

How cc Works 中文譯文

主線程可以通過多種方式請求送出。大多數網頁都是通過 requestAnimationFrame 發起請求,最終在 LayerTreeHost 上調用 SetNeedsAnimate。此外,任何對 cc 輸入的變更(例如,圖層屬性比如轉換矩陣,或是對圖層内容的更改),将會觸發在 LayerTreeHost 上調用 SetNeedsAnimate,SetNeedsUpdate 或 SetNeedsCommit。不同的 SetNeeds 函數的作用是,如果确定沒有實際發生變更,cc 可以在不同級别上提前終止送出流程。(例如,如果 requestAnimationFrame 回調沒有改變圖層,那麼就不需要進行送出,甚至不需要更新圖層。)如果不是排程器目前正在處理上一個送出流程的話,上述這些函數都會請求排程器發起新的 BeginMainFrame。

收到主線程請求後的某個時刻,排程器将調用 ScheduledActionBeginMainFrame 對請求進行響應。合成器線程會發送一個 BeginFrameArgs 到主線程啟動 BeginMainFrame。 BeginFrameArgs 包含一個時間戳(用于動畫目的),以及已經在合成器線程上應用的新增滾動增量(主要是處理使用者滾動手勢的結果),而 Blink 還不知道這些新的滾動位置。當 Blink 嵌入 cc 使用時,Blink 會在 BeginMainFrame 的過程中 擷取新的合成器滾動增量,調用 requestAnimationFrame 回調,這些作業構成了 Blink 的渲染過程生命周期的一半。

完成此操作後,cc 将更新所有圖層。如果在此更新流水線中的任何時刻,cc 确定不需要更多的工作(例如,合成器線程滾動更新到 Blink,但 Blink 處理該滾動過程中沒有對頁面進行任何更改),它就可以提前中止送出。(目前,單線程 cc 從不中止任何送出。)一旦 embedder 完成了 BeginMainFrame,并且送出沒有中止,則 ProxyMain 将發送一個附帶互斥鎖的同步 NotifyReadyToCommit 消息給合成器線程,然後阻塞自己等待回應。

當排程器準備好送出時,它将調用 ScheduledActionCommit 進行響應。然後,合成器線程上的 ProxyImpl 會完成所有将資料從主線程(主線程此時被阻塞)複制到合成器線程對應資料結構的工作。然後它釋放互斥鎖,以便主線程可以繼續運作。

ProxyImpl 是唯一可以通路主線程和合成器線程上資料結構的類。它隻在主線程被阻塞時通路主線程上的 LayerTreeHost 和 Layers,并通過 DCHECK 斷言進行線程檢查。 ProxyMain 是存在于主線程上的對應物,由 LayerTreeHost 擁有。對于單線程情況,SingleThreadProxy 等同于 ProxyMain 加 ProxyImpl。

圖層

圖層是一個大小為整數的 2d 内容矩形。它還包括一些其它屬性,比如矩陣變換,裁剪和特效,這些屬性共同描述了它如何在螢幕上呈現。

cc 裡面的圖層有兩個不同的類層次結構,一個用于主線程圖層樹(從 cc::Layer 派生),另一個用于合成器線程 pending,active 和 recycle 圖層樹(從 cc::LayerImpl 派生)。 它們基本上是 1:1 對應的,是以會同時存在 SurfaceLayer 和 SurfaceLayerImpl 或者 PictureLayer 和 PictureLayerImpl,是以本節讨論圖層時并不加以強調是哪一種,可自行根據上下文判斷。

在主線程上,圖層是引用計數的。 LayerTreeHost 擁有根圖層,每個圖層以遞歸方式擁有其子圖層。 Blink 的其他一些部分也可能是圖層的提供者(例如,多媒體系統建立 surface 或 video 圖層,插件),這是為什麼主線程的圖層要使用引用計數的原因。而在合成器線程上,圖層是由其父節點使用 unique_ptrs 持有。

屬性樹

有兩種方式可以在 cc 中描述屬性的層次結構。傳統的方式(ui/ 仍然使用這種方式)是以圖層樹的方式。如果父圖層具有矩陣變換(例如,平移,縮放或透視),裁剪或特效(例如,模糊濾鏡,或者 mask,或者半透明),則這些屬性需要遞歸地應用于其子節點。這種組織方式在很多

極端情況

(固定位置圖層,滾動父節點,滾動子節點)會導緻糟糕的性能(需要大面積周遊這棵樹并在所有步驟中計算所有屬性)。

是解決這個問題的一種方式。與上面的方式相反,cc 提供了單獨的屬性樹:矩陣變換樹,裁剪樹,特效樹。然後,每個圖層都有若幹節點 id,分别對應不同屬性樹上的矩陣變換節點,裁剪節點和特效節點。這樣,屬性更新的複雜度就是 O(感興趣的節點)而不是 O(圖層)。當存在屬性樹時,我們也不再需要圖層樹,而是可以使用有序的圖層清單。

PictureLayer

包含繪制内容的圖層。 内容以 cc::PaintRecord 的形式表現。 PictureLayer 決定了對内容進行光栅化時使用的縮放比例。 每個縮放比例由一個 PictureLayerTiling 來表示,PictureLayerTiling 包含了一個特定縮放比例内容的稀疏 2d 平面分塊集合。

分塊集合中的每個分塊就是一個 cc::Tile,它用于關聯所對應矩形區域内的内容,分塊的光栅化由 TileManager 管理。 如果在 DevTools 渲染設定中打開合成圖層邊框,則可以看到分塊的邊框。有許多啟發式方法用于确定分塊的大小,但對于軟體光栅化分塊大約為 256x256 像素,而對于 gpu 光栅化分塊大緻為 viewport width x 1/4 viewport height。

有許多啟發式方法用于确定何時以及如何改變光栅化的縮放比例。 這些方法并不完美,你可以去改進它,不過風險自負。

軟體光栅化的分塊大小一般由螢幕分辨率決定,1080p 的螢幕使用的是 384 x 384 的分塊大小。

PictureImageLayer

PictureLayer 的子類。 專門用于優化一個

<img>

元素自己擁有一個獨立圖層的特例。 如果圖檔元素擁有自己獨立的合成圖層并且沒有邊框或填充(也就是說圖層繪制的内容與圖檔本身完全相同),則可以做一些額外的優化。它以固定的縮放比例“光栅化”圖檔,保證縮放此圖檔是高性能的。這種優化隻用于軟體光栅化,gpu 光栅化不需要使用 PictureImageLayer。

TextureLayer

用于插件,canvas,這些圖層内容的光栅化是自己負責而不是通過合成器的光栅化器,另外還有 WebGL。 這裡的 “texture” 是指對 gpu 紋理的引用,但在使用軟體合成的情況下實際上是共享記憶體位圖。

SolidColorLayer

如果已知圖層僅為純色,則無需光栅化該圖層,也不需要配置設定 gpu 記憶體。這是對純色圖層這種簡單圖層的一種優化。

VideoLayer

因為

surfaces for video project

的原因目前已經廢棄。最終會被删除。

顧名思義,原本是用于 video 元素。

SurfaceLayer

一個 surface 圖層擁有一個 surface id,用于表征系統中其它 compositor frames 的流。這是一種間接包含其它 compositor frame 生産者的方式。另見:

surface 文檔

。 實際使用的一個例子是,Blink 通過 SurfaceLayer 嵌入對程序外 iframe 的引用。

SolidColorScrollbarLayer

Android 的滾動條是一個 “純色” 滾動條圖層。它們就是一個簡單的矩形,可以由合成器直接繪制,而無需為它們建立紋理資源。 同時存在純色和滾動條繪制圖層的原因是,在合成器線程上處理滾動可以即時更新滾動條而無需回到主線程處理。 如果不區分處理,即使頁面的滾動很流暢,但滾動條本身的移動則變得不夠平滑。

Painted(Overlay)ScrollbarLayer

桌面版本(非 Android)的滾動條是有繪制内容的滾動條。因為主題樣式代碼不是線程安全的,是以滾動條和滾動軌道在主線程上繪制并光栅化為位圖。然後,這些位圖再在合成器線程上通過生成對應的 quads 進行繪制。ChromeOS 使用 PaintedOverlayScrollbarLayer,這是一個使用 nine-patch 位圖的版本。

HeadsUpDisplayLayer

這個圖層是用于支援 devtools 的

渲染設定

。它用于繪制 FPS 儀表,另外也用于繪制一個表示繪制更新或變更區域的覆寫層。這個圖層比較特殊,因為它的輸入取決于所有其他圖層的 damage 計算,是以必須最後更新。

UIResourceLayer / NinePatchLayer

UIResourceLayer 是與 TextureLayer 等效的軟體位圖版本。它處理位圖上傳紋理并在上下文丢失時根據需要重新建立它們。 NinePatchLayer 是一個派生的 UIResourceLayer 類,它将 UIResource 切割成可伸縮的部分。

樹:commit/activation

圖層樹有四種類型,但在任何給定時間隻存在 2-3 種:

  • 主線程樹(cc::Layers,主線程,始終存在)
  • Pending 樹(cc::LayerImpl,合成器線程,用于光栅化階段,可選)
  • Active 樹(cc::LayerImpl,合成器線程,用于繪制階段,始終存在)
  • Recycle 樹(cc::LayerImpl,合成器線程,與 Pending 樹不會同時存在)

這些被稱為“樹”,因為曆史上它們一直是樹結構,它們存在于 cc/trees/ 目錄下,但它們目前實際上是清單而不是樹(抱歉)。主線程的圖層樹由 LayerTreeHost 擁有。 pending,active 和 recycle LayerImpls 樹都是 LayerTreeHostImpl 擁有的 LayerTreeImpl 執行個體。

Commit 是将圖層樹和屬性從主線程圖層清單推送到 pending 樹的過程。Activation 是将圖層樹和屬性從 pending 樹推送到 active 樹的過程。在上述的兩個過程中,都會建立一個重複的圖層結構(具有相同的圖層 id,圖層類型和屬性)。圖層 id 用于查找每棵樹上的相應圖層。主線程樹上 id 為 5 的圖層資料将推送到 pending 樹上的 id 為 5 的圖層。pending 樹上的該圖層将推送到 active 樹上 id 為 5 的圖層。如果該圖層還不存在,則在推送期間将建立該圖層。類似地,源樹中不再存在的圖層将從目标樹中删除。這一切都是通過樹同步過程完成的。

因為 Layer(Impl) 的配置設定是昂貴的并且大多數圖層樹結構不會每一幀都發生變化,是以我們激活 pending 樹後,原來的 pending 樹就變成 “recycle 樹”。除了作為最後一個 pending 樹的緩存外,此樹沒有其它用途。這避免了從主線程推送資料到 pending 樹時額外的圖層配置設定和屬性指派的工作。這僅僅是一種優化。

pending 樹存在的原因是,如果單個 Javascript 調用過程中的網頁内容有多處更改(例如,html canvas 上畫了一條線,并且移動了一個 div,還将某些元素背景顔色更改為藍色),這些都必須以原子方式一次性完整地呈現給使用者。 Commit 會對這些更改進行快照并将它們推送到 pending 樹中,以便 Blink 可以繼續更新主線程圖層樹以供将來送出。送出後,這些更改需要重新光栅化,并且必須先完成所有光栅化,然後才能向使用者呈現新的内容而不是一個部分分塊區域的更新。pending 樹是用于等待所有異步光栅化工作完成的暫存區域。當 pending 樹用于完成下一幀所需的所有光栅化工作時,active 樹仍然可以被動畫和滾動更新,保證使用者得到即時的響應。

單線程版本的 cc 沒有 pending 樹,主線程送出的資料直接推送到 active 樹。(此模式下不使用 recycle 樹。)這是一種避免額外工作和拷貝的優化。為了解決原子性呈現的問題,active 樹會強制可見區域内的所有分塊都光栅化完畢後才能進行繪制。不過,鑒于這是 cc 的單線程版本,不需要處理合成器線程動畫或滾動,由合成器本身發起的重繪請求其實很少。

光栅化器和分塊管理

TileManager 負責光栅化所有的分塊。每個 PictureLayer 都提供了一組用于光栅化的分塊,其中每個分塊都是在特定縮放比例下的繪制内容的子矩形。

TileManager 找到在 active 樹上目前繪制所有需要的分塊,和所有需要激活 pending 樹的分塊,另外還有靠近 viewport 但暫時還不可見相對低優先級的分塊,以及需要解碼的屏外圖檔。

目前 cc 有三種不同的光栅化器:

  • 軟體光栅化器:光栅化器生成軟體位圖
  • gpu 光栅化器:通過 command buffer 發送 gl 指令生成 gpu 紋理
  • oop 光栅化器:通過 command buffer 發送 2d 繪制指令生成 gpu 紋理

TileManager 根據用于送出 compositor frames 的

LayerTreeFrameSink

是否具有 context provider,來選擇軟體光栅化或者是 gpu/oop 光栅化。它始終處于其中一種模式。切換模式需要釋放所有的配置設定資源然後重新光栅化。GPU 光栅化器目前已經逐漸棄用,最終将在所有情況下由 OOP(程序外)光栅化器替代。切換模式的一個常見原因是 gpu 程序崩潰太頻繁,Chrome 将不得不将光栅化和合成都從 gpu 切換到軟體的模式。

一旦 TileManager 确定了下一個要完成的工作集,它就會生成一個包含任務依賴關系的 TaskGraph,然後跨多個 worker 線程進行任務排程。TaskGraphs 不會動态更新,而是整個 graph 會被重新排程。一旦開始運作的任務是無法被取消的。但是已經進入排程還未開始運作的任務,如果新送出的 graph 不包含這些任務,這些任務會被自動取消。

圖檔解碼

圖檔解碼在 TileManager 中有很多需要特殊處理的地方,因為它們是光栅化中耗時最長的部分,特别是相對于性能較好的 gpu 光栅來說。 在 task graph 裡面,每個圖檔的解碼都有自己獨立的解碼任務。 軟體光栅化與 gpu 光栅化使用不同的解碼緩存。 SoftwareImageDecodeCache 管了解碼,縮放和顔色校正,而 GpuImageDecodeCache 進一步需要将解碼後的位圖上傳到 gpu 程序生成紋理,使用 gpu discardable memory 來緩存紋理。

cc 還處理 Chrome 中動畫 gifs 的所有動畫。 當 gifs 動畫時,它們會生成一個新的 pending 樹(由合成器線程直接生成而不是主線程送出),包含一些需要重新光栅化的區域,然後重新光栅化該 gif 覆寫的分塊。

Raster Buffer Providers

除軟體與硬體光栅化模式外,Chrome 還可以在軟體與硬體顯示合成模式下運作。 Chrome從 不将軟體合成與硬體光栅化混合,但光栅化模式 x 合成模式的其他三種組合都是有效的。

合成模式影響 cc 提供的 RasterBufferProvider 的選擇,它管理在光栅化工作線程上的光栅化過程和資源管理:

  • BitmapRasterBufferProvider:用于軟體合成,使用軟體位圖作為光栅化的目标緩沖區
  • OneCopyRasterBufferProvider:用于 gpu 合成,光栅化的軟體位圖使用(跨程序)共享記憶體,然後在 gpu 程序中上傳成紋理
  • ZeroCopyRasterBufferProvider:用于 gpu 合成,光栅化的軟體位圖使用 GpuMemoryBuffer(例如IOSurface),可直接被 display compositor 使用
  • GpuRasterBufferProvider:用于 gpu 合成,通過 command buffer 發送 gl(用于 gpu 光栅化器)或者 paint 指令(用于 oop 光栅化器),使用 gpu 直接光栅化到 gpu 紋理

需要注意的是,由于需要在光栅化過程中鎖定上下文,gpu 和 oop 光栅化目前不支援多線程并發運作,不過圖檔解碼仍然可以在其他線程上并發進行。這個單線程限制是通過加鎖解決的,而不是線程親和性。

GpuMemoryBuffer 是一種平台相關的特殊緩沖區的封裝,比如 IOSurface,它們即可以被 cpu 也可以被 gpu 直接通路,在 Chrome 裡面一般是 cpu 寫(光栅化),gpu 讀。

動畫

animation/ 目錄實作了一個動畫架構(由 LayerTreeHost(Impl) 通過 cc::MutatorHost 接口使用)。該架構支援基于關鍵幀的矩陣變換清單,半透明和其它 filter 效果清單動畫,這些動畫直接操作屬性樹中對應 TransformNode/EffectNode 上的值(由ElementId辨別)。

動畫由一個動畫執行個體表示,動畫執行個體具有一個(将來或許更多)KeyframeEffects,每個 KeyframeEffects 都有多個 KeyframeModel。動畫管理動畫的播放狀态,開始時間等,KeyframeEffect 表示動畫的目标元素,而每個 KeyframeModel 描述動畫作用于元素上的特定屬性(例如矩陣變換/透明度/filter 等)。動畫的來源可以是 embedder(例如,一個矩陣變換的 Blink 動畫),或者它可以是來自 cc 本身(例如,用于平滑滾動的滾動動畫)。

LayerTreeHostImpl 向 AnimationHost 通知添加新元素和删除舊元素,進而引發作用于這些元素的動畫的狀态更新。它調用 NeedsTickAnimations 來了解是否應該繼續排程動畫,并且每幀的 TickAnimations 調用會更新動畫時間戳,狀态,産生動畫事件,以及根據動畫更新屬性樹節點的實際輸出值。

cc/paint/

此目錄存放用于描述繪制内容的類。它們與 Skia 裡對應的資料結構非常相似,但在所有情況下都是可變的,内省的和可序列化的。它們還需要處理 Skia 不需要考慮的安全問題(例如

TOCTOU

問題,一塊用于序列化繪制内容供 gpu 程序通路的共享記憶體可能會被一個已經溢出的惡意 renderer 所改寫)。

PaintRecord(又名PaintOpBuffer)是 SkPicture 的等價物,用于存儲 PaintOps。 PaintRecord 可以由 raster buffer provider 光栅化為位圖或 gpu 紋理(使用軟體或 gpu 光栅化器),也可以被序列化(使用 oop 光栅化器)。

PaintCanvas 是記錄繪圖指令的抽象類。 它可以由 SkiaPaintCanvas 實作(使用 SkCanvas 完成繪制)或 PaintRecordCanvas 實作(将繪制指令存儲到 PaintRecord)。

排程

cc 的工作由 cc::Scheduler 來驅動。這是 Chrome 衆多排程器之一,其它還包括 Blink 排程器,viz::DisplayScheduler,浏覽器 UI 任務排程器和 gpu 排程器。

cc::Scheduler 由 ProxyImpl(或SingleThreadProxy)擁有。它接收各種輸入(可見性,幀開始消息,重繪請求,準備繪制,準備激活等)。這些輸入驅動 cc::SchedulerStateMachine,然後決定了 SchedulerClient(LayerTreeHostImpl)要采取的操作,例如 “Commit” 或 “ActivateSyncTree” 或 “PrepareTiles”。這些操作的觸發依賴于目前狀态,它們通常是流水線裡面耗時較長的部分,我們需要小心避免頻繁調用。

cc::Scheduler 代碼區分不同的 begin frames,display compositor 發送的 begin frames 是 BeginImplFrame(即 cc 應該生成 compositor frame),發送給它的 embedder 的 begin frame 是 BeginMainFrame(即,cc 應該告訴 Blink 運作 requestAnimationFrame 并産生一個送出,或者在浏覽器中 cc 應該告訴 ui 做類似的事情)。 BeginImplFrame 由 viz::BeginFrameSource 驅動,後者又是由 display compositor 驅動。

在一次低延遲和光栅化足夠快的流水線更新中,一般排程流程是 BeginImplFrame - > BeginMainFrame - > Commit - > ReadyToActivate - > Activate - > ReadyToDraw - > Draw。

但是,如果光栅化速度很慢,cc 可以在激活之前就可能發送第二個 BeginMainFrame,并且它将在 NotifyReadyToCommit 中阻塞,直到激活完成,因為如果目前的 pending 樹還沒有被激活,SchedulingStateMachine 将阻止啟動送出。這允許主線程并行地去生成下一幀,而不是以延遲為代價而處于空閑狀态。在光栅化速度不夠快時,一個可能的排程排序是:

BeginImplFrame1 - > BeginMainFrame1 - > Commit1 - >(slow raster) - > BeginImplFrame2 - > BeginMainFrame2 - > ReadyToActivate1 - > Activate1 - > Commit2 - > ReadyToDraw1 - > Draw1。

cc::Scheduler 維持一個期望其 embedder 響應的截止時間。如果主線程響應緩慢,則排程器可以在不等待送出的情況下進行繪制。如果發生這種情況,則認為排程器處于高延遲模式。如果未來的幀生成再次開始變快,則排程器可以嘗試跳過 BeginMainFrame 以“趕上”并重新進入低延遲模式。高延遲模式通過增加延遲來加大流水線的吞吐量。排程器通過儲存曆史時間戳來區分不同的模式,并試圖用啟發式的方法進行調整。

Compositor frames, render passes, quads

cc 的輸出是 compositor frame。compositor frame 由中繼資料(裝置縮放比例,顔色空間,大小)和一組有序的 render passes 組成。一個 render pass 包含一組有序的 quads,quads 包含對資源的引用(例如 gpu 紋理)以及有關如何繪制這些資源的資訊(大小,縮放比例,紋理坐标等)。一個 quad 是繪制到螢幕上的一個矩形,可以通過開啟

合成圖層邊框可視化

來檢視這個矩形。圖層本身通過覆寫 AppendQuads 函數來生成 quads。它将生成一組 quads,剛好覆寫(不重疊或相交)圖層的可見區域。

有各種類型的 quads 大緻對應于不同的圖層類型(ContentDrawQuad,TextureDrawQuad,SolidColorDrawQuad)。由于一個圖層可以生成許多 quads(即 PictureLayerImpl),這些 quads 的部分資訊是相同的,我們使用 SharedQuadState 來優化這些共享資訊的記憶體占用。RenderSurfaceImpls 跟 render passes 是 1:1 的對應關系,它的 AppendQuads 的邏輯跟圖層生成 quads 大緻相同 ,用于生成 RenderPassDrawQuads。

How cc Works 中文譯文

render pass 的存在是為了實作合成特效(請參閱:特效樹)。有些特效需要先合成到 render surface 後才能實作。另外也可能是先合成再實作這些特效會更簡單(因為特效可以直接在單個 render pass 産生的紋理上應用,而不是應用在一棵圖層子樹産生的任意的 quads 集合上)。render passes 的常見例子是:masks,filters(例如模糊),裁剪旋轉後的圖層或一棵内容子樹的半透明效果。

在 compositor frame 内,render passes 和 render passes 中的 quads 是有序的。render passes 清單是一個展平後的清單,用于表示 render passes 的依賴關系樹。如果 render pass 1 依賴于 render pass 9(因為它包含一個 RenderPassDrawQuad,引用了 9 的輸出),那麼在清單上 9 将出現在 1 之前。是以,root render pass 始終位于清單的最後。在單個 render pass 中,quads 按底部到頂部的順序排列(

畫家算法

)。

通常,quads 不被認為是存在于 3d 空間中(即使通過三維矩陣變換進行變換),它們仍然按順序繪制,一個 quad 繪制到在它之前繪制的 quad 之上。但是有一種模式,其中一組 quads 可以在一個 3d 上下文中(由 css transform-style:preserve-3d 引起)。這時我們使用 BSP 樹在同一個 3d 上下文中對它們進行排序和相交檢查。

術語表

參見:

cc/README.md

其它資源

相關的示範稿,視訊和設計文檔:

https://www.chromium.org/developers/design-documents/chromium-graphics

找不到合适歸類的各種雜項

Damage

Chrome 在整個系統的不同子產品裡都有各種不同的 invalidation 的概念。 “Paint invalidation” 是需要在 Blink 中重新繪制的部分 document(DOM 節點)。 “Raster invalidation” 是已發生變更并需要重新光栅化的圖層的一部分(可能是由于 paint invalidation,也可能是合成失效,例如圖層第一次被光栅化或者紋理被丢棄然後再次需要時) 。最後,damage 是 “draw invalidation” 的另一種表述方式。表示螢幕的一部分需要重新繪制。

有兩種類型的 damage:invalidation damage 和 expose damage。Invalidation damage 是由于 raster invalidation 造成的,其中紋理的一部分内容已經改變并且螢幕需要更新顯示。Expose damage 是指圖層消失,第一次被添加或者圖層的層疊順序發生變化。在這些情況下雖然沒有發生重新光栅化,但仍需要更新螢幕。

cc 通過 DamageTracker 來計算 damage,并将其與 CompositorFrame 一起發送。需要 damage 的一個原因是 display compositor 可用于局部緩沖區交換優化(此時僅僅更新螢幕的一部分),這種方式可以減少了耗電。另一個原因是當使用硬體覆寫層時,display compositor 可以知道隻有覆寫層被更新而不必重新合成場景的其餘部分。

Mask 圖層

Mask 圖層是用于實作

蒙版效果

的圖層。他們不在圖層樹裡面,沒有父親節點。它們由應用蒙版的圖層所擁有。 它們可以是任何類型的圖層子類(例如 PictureLayer 或 SolidColorLayer)。 任何時候周遊圖層,它們都是需要考慮的特殊情況,因為它們不是普通父/子樹結構的一部分。雖然它們在光栅化和分塊管理方面與其他圖層一樣,但是它們的 AppendQuads 調用是通過額外 RenderSurfaceImpl, 而不是頂層的圖層周遊,這是因為它們是用于繪制特效而不是繪制自身的内容。

"Impl" 字尾

cc 使用 “impl” 字尾... 但是涵義與 Chrome 的其它子產品或軟體工程常用的普遍涵義并不一樣。 在 cc 中,“impl”表示該類在合成器線程上使用,而不是在主線程上使用。

這樣做的曆史原因是,cc 在設計時,我們在主線程上有 Layer,我們需要一個運作在合成器線程上等價的類。jamesr@ 咨詢了 nduca@,他提出了一個非常合乎邏輯的論點,即合成器線程上的東西是合成器内部的,并且實際上是主線程版本的實作,是以我們就從 Layer 得到了 LayerImpl。參見:

https://bugs.webkit.org/show_bug.cgi?id=55013#c5

然後,如果你需要一個LayerImpls 樹,LayerTreeImpl 就出現了,而擁有和管理這些樹的對象就被命名為 LayerTreeHostImpl。突然之間,“impl 線程” 就演變成所有 “impl 類” 存在的線程。如果你将光栅化移動到合成器線程,那麼就會很詭異地被稱為 “impl-side painting”。