天天看點

LWN 翻譯:Atomic Mode Setting 設計簡介(上)Atomic mode setting design overview, part 1

譯者注

本文翻譯自 Daniel Vetter(Intel,Linux DRM maintainer) 于 2015 年 8 月 5 日在 LWN 上發表的關于 DRM Atomic Mode Setting 的文章。該文章雖然是在五年前發表的,但是它的核心思想至今仍然沒有改變,非常值得一讀。通過閱讀本文,你将了解以下内容:

  • Atomic mode setting 産生的背景
  • Atomic KMS 與谷歌 ADF 相比有哪些優勢
  • TEST_ONLY 模式的正确打開方式
  • KMS state 的使用方法
  • Atomic check 和 commit 的功能
原文連結:https://lwn.net/Articles/653071/

Atomic mode setting design overview, part 1

在過去的幾年時間裡,兩大趨勢促使我們急需一套全新的 kernel Display 驅動接口。一方面,當 GUI 内容發生變化時,人們不再欣賞局部重繪和視窗切割。像 Wayland 這種以 “每一幀都是完美的(every frame is perfect)” 為口号的 Compositor 也随之誕生。另一方面,采用電池供電的手機和平闆電腦,它們有着絢麗的圖形界面,但對功耗卻有着嚴格的限制,這就促使一大批特殊用途的顯示硬體應運而生,以此來輔助更加通用但極其耗電的 GPU,完成螢幕顯示内容的合成工作。将這些趨勢結合在一起,就需要以 “要麼全有要麼全無” 的原子(atomic)方式更新大量顯示硬體狀态(state),以確定每一幀都是完美的,并盡可能地使用那些專門為功耗優化而設計的顯示硬體。

經過幾年的開發,Direct Rendering Manager (DRM,直接渲染管理器) 驅動程式的原子更新(atomic update) ioctl 終于随着 Dave Airlie 的 Pull Request(合入請求)一起合入到了 linux-4.2 主線中。這是一條漫長的道路,許多驅動程式已經轉換為 atomic 驅動,而更多的驅動則還在轉換的過程中,而且 DRM 子系統中的 atomic helper 函數庫和支援代碼已經完善的差不多了。但真正缺少的是對整個 atomic 架構設計的介紹,以及為什麼某些決策和細節要這樣去實作。

本文總共分為上下兩篇,這是第一篇。本篇将首先回顧 kernel mode-setting 支援的曆史,闡述老的接口是如何産生的,以及為什麼它們不再适用了。然後介紹 out-of-tree (即 kernel 主線之外)的解決方案,最後介紹已合入的 atomic display 重新整理接口是長什麼樣子的。在第二篇文章中我們将深入研究這些接口實作的具體細節。

在深入了解所有細節之前,需要先簡單介紹一下如今的 Display 硬體是如何在 DRM 子系統中進行抽象的。首先是

drm_connector

結構體,它表示一個螢幕,無論是內建在主機闆上還是外接的顯示屏。注意,如今的 connector 是可以支援熱插拔的,因為 DisplayPort 支援在一根電纜上對多個裝置進行分支和多路複用,類似于其他外設總線。在另一端是

drm_plane

結構體,它表示一個掃描引擎(scanout engine),該引擎從

drm_framebuffer

所表示的記憶體中讀取像素資料,并将其發送給顯示硬體。

為了讓驅動程式支援所有的硬體特性,尤其是超出每個 object 核心資料結構所能支援的硬體特性,DRM 引入了 property (屬性),這些 property 可以綁定(attach)到任意 DRM object 上。property 的類型有很多種,每種類型接受特定的輸入參數,比如枚舉(例如在 pillarbox 和 letterbox 之間切換縮放模式)或整數範圍(如亮度控制)。

對于一個進階的硬體而言,它的 plane 可以在輸出矩形中自由偏移、縮放以及其它參數的調整,而它的兩端(輸入輸出)則與

drm_crtc

(用于表示 display pipeline)綁定在了一起。請注意 CRTC 的意思是“陰極射線管控制器”(cathode ray tube controller),它隻不過是個曆史遺留的縮略詞而已。多個 plane 可以連接配接到同一個 CRTC 上,為其提供輸入資料,這樣的 display pipeline 再依次連接配接到一個或多個

drm_connector

上,進而最終在螢幕上顯示真正的畫面。CRTC 除了是整個鍊路的核心對象外,還負責跟蹤其他參數的設定,如 display pipeline 的顯示模式(如重新整理率和分辨率)以及背景色(通常指沒有可見圖層區域的顔色)。

舊世界 —— 一堆的 ioctl 和 property

關于 mode-setting 的曆史已經有更好更詳細的文章了,是以我這裡隻重點介紹最近幾年發生的變化。大約在 7 年前,kernel 顯示驅動合入第一筆 KMS(kernel mode setting)patch,預示着 kernel 顯示驅動新時代的到來。當然,fbdev 一直都存在,隻是這個子系統從來沒有獲得對顯存管理真正意義上的支援,在顯示和渲染之間沒有一個明确的劃分,并且從來沒有解決更多的問題。

最初的 KMS ioctl 指令是按照 X 的 user-space mode-setting 協定 XRandR 模組化的,也就是說這些 ioctl 能很好的在螢幕上單獨設定模式,并将單個 primary framebuffer object(表示驅動程式特有的記憶體緩沖區)與該顯示連接配接起來。這都是在旋轉桌面立方體成為新潮流時設計出來的,那時每個人都想使用 3D 渲染引擎來合成桌面顯示内容 —— 老式的視訊疊加層(video overlay plane)徹底失去了人們的青睐(因為那樣的桌面根本就無法動彈),是以根本不受支援。

當然,有個特例:光标支援。但它完全是一個獨立的 ioctl,甚至都沒有使用 KMS framebuffer object。無論是 primary framebuffer 更新操作還是 vertical blanking 事件,光标都沒法與這些操作同步更新 —— X 無需也無法使用該 ioctl。如果不能與 VBLANK 保持同步(即 Vsync),螢幕重繪将與光标更新産生競争,最終出現難看的撕裂(tearing effect)效果。于是後來添加了對 primary plane 的非阻塞更新(non-blocking update),以此來實作基于 Vsync 同步且無撕裂的更新操作。

當然,再後來智能手機和平闆電腦出現了,于是再也不能用電源來顯示了。突然間,overlay plane 又成為了新寵,因為它們在一些細分應用場景下更加省電,比如視訊播放場景。KMS 通過新增 plane object 和一組新的 ioctl 指令來實作對 overlay plane 的支援。

但是,就像光标更新一樣,plane 的更新不能與其它任何操作保持同步,無論是 plane 更新還是 Vsync,都還是因為 X 做不了更多的事情,也不關心 upstream graphics。結果就是,對于相同硬體的不同執行個體,有三個不同的 ioctl 接口。Planes,無論是 Primary、Cursor 還是 Overlay,都接收一個 framebuffer object,然後以某種方式将其合成到一起,并将其發送給 display pipeline (在 KMS 中由 CRTC object 表示),後者再發送給 connector 和 panel。要統一 plane 接口其實相當簡單:primary plane 和 cursor plane 必須導出給 userspace,以避免給原來的使用者空間代碼造成混淆。

但是,還是沒有統一的 ioctl 能搞定這一切。例如,隻有 primary plane 支援使用精确的 completion 事件來實作 non-blocking 更新,而且還沒有什麼接口能一次性更新多個 plane —— 使用者空間必須進行多次 ioctl() 調用,并希望所有更新操作都能在同一幀裡完成。很明顯,對于像 Wayland 這樣想要保證 “每幀像素的更新都是完美的” 現代 Compositor 來說,這是不可能做到的。

發生的另一件事則是向所有的 KMS object 添加 property 支援,這樣就可以将硬體的其它特性輕松地導出給 userspace 了,如 plane 之間的合成方式、背景色的設定、旋轉或直接輸出。當然,這仍然是采用單獨的 ioctl 指令來完成的。同樣,因為 X 在引入這個的時候做的不夠好,是以無法實作同步更新。這意味着破壞性的更新會很容易發生,舉個例子,當旋轉角度的值已經更新了,但 plane 的 buffer 卻還沒有更新到與該角度對應的畫面内容。如果可能的話 —— 我們希望某些參數隻能一起或以特定的順序進行更新,比如在将 primary plane 切換到 memory-bandwidth-demanding 模式之前先關閉其它的 plane。

一切都亂糟糟的,急需解決。

Android Atomic Display Framework (原子顯示架構)

在 Upstream Graphics 之外,特别是在 Android 領域,情況甚至更糟。每個 GPU 廠商都有自己的 kernel-space、user-space API,所有的驅動程式都在重複造相同的輪子,隻是方式略有不同而已。谷歌對這一現狀很不滿意,于是建立了 Atomic Display Framework (ADF)原子顯示架構。它借鑒了 upstream kernel mode-setting 的支援,但與此同時它又是一個全新的子系統。我并不打算在本文中講解 ADF 的總體設計,因為它并不實用。相比之下,我更願意聊聊 ADF 在 Upstream Graphics 中都有哪些使用上的不足:

  • ADF 對于整個顯示裝置而言隻支援一個更新隊列,這非常适合 Android 的 SurfaceFlinger,後者隻有一個繪制循環體,對于大多數情況下隻有一個螢幕的手機和平闆電腦來說,這完全夠用了。但是,如果您有多個螢幕,而它們通常以略微不同的重新整理率運作,那麼一個更新隊列是遠遠不夠的。你要麼在幀率快的螢幕上延遲送顯,要麼在幀率慢的螢幕上直接丢棄某幾幀,這兩種方法都無法實作絕對流暢的動畫。upstream kernel 中的 primary plane 非阻塞更新已經完全解耦了,像 Wayland 這樣的 Compositor 已經支援了每個顯示輸出(per-output)擁有一個獨立的繪制循環體,這對 ADF 而言是一個重大的功能缺失。
  • ADF 使用驅動程式特有的資料結構來描述 atomic update,這對 Android 來說沒什麼問題,因為它在 Hardware Composer 接口下實作了一個 GPU 所特有的 userspace 驅動程式(類似于 xf86-video-intel 這樣的 X 裝置驅動程式)。但 upstream 還希望支援通用的使用者空間 Compositor,如 Wayland 或 xf86-video-modesetting X 驅動程式。ADF 有一個用于螢幕重新整理的通用接口,但它隻适用于簡單的啟動界面重新整理。當然,我們總會面臨某些 feature 隻适用于某個特定的驅動程式,但是通過對驅動程式的 property 進行标準化處理,upstream DRM 已經具備了在通用 user-space 代碼中支援任意功能的基礎架構。
  • ADF 的原子特性隻适用于 plane 的更新,而不适用于重新配置整個輸對外連結路。同樣,如果您隻關心單個螢幕的使用場景,這也不是什麼問題,但是在現代 GPU 上,當使用多個輸出時,會有很多共享資源。通過循環周遊所有輸出進行重新配置的幼稚方法很容易導緻硬體不支援的狀态發生,比如因為驅動程式用光了臨時配置的顯示時鐘發生器。是以對于 upstream 而言,對整個 pipeline 的所有輸出進行原子更新是必須要支援的。更重要的是,在冒着黑屏的風險去修改某些參數之前,需要有一些手段來檢測該操作是否真正可行。
  • ADF 是一個獨立的中間層子系統。如果你的目标是在那 90% 的驅動裡提高技術水準,ADF 确實能做的很好。但如果你的驅動恰巧在那不被支援的 10% 裡,那就很痛苦了。盡管在一些 upstream DRM legacy 子系統中出現過一些糟糕的情況,但通過将所有接口經由 ioctl() 導出給驅動程式的回調接口 (譯者注:原文為 hook,鈎子函數,本文統稱為回調接口),并為所有常用 case 提供一個龐大的 helper 庫,mode-setting 已經形成了一套良好的體系結構,這肯定是要保留下來的。
  • ADF 還有一套全新的 user-space ABI 和驅動程式接口,也就是說它所有的驅動都是和主線分隔開的。從向後相容性和維護性角度來看,這是不可取的。

當然,這些問題并不那麼容易解決。接下來的内容以及下一篇文章将更詳細地進行讨論,并闡述這些問題在已合入的 Atomic 支援中是如何被解決的。

一個真正實用的驅動程式通用接口

由于 DRM 已經支援了 property,很明顯我們可以重用這部分作為 user-space ABI 的通用傳輸通道,使用者空間隻需要提供一個(object_id、property_id、value)三元組的清單即可。這一下子就實作了驅動程式的可擴充性 —— 使用者空間對于不了解的 property 就不會去修改它。隻要驅動程式在初始化時将所有 property 設定為合理的值(比如将 rotation property 設定為 “unrotated”),那麼當加入新 feature 後,老的使用者空間通用代碼将依然能夠正常工作。

當然,這個計劃還是有一些差距的 —— 必須為已存在的中繼資料(metadata)添加 property,并使用一個特殊的 flag 來将其導出給那些隻支援 atomic 的 user-space 程式。目前已經建立了一種新的 property type,它接受 KMS objects 作為 property 的 value 來設定顯示鍊路。還有一些其他的代碼需要調整,比如擴充 blob property 以便适用于 atomic update。

允許 部分參數更新(partial update)也解決了 kernel 内部向後相容的問題:所有老的 KMS ioctl 指令都已支援部分參數更新,如果隻允許 全部參數更新(full update),則意味着驅動程式需要同時支援 legacy 接口和 atomic 接口。有了部分參數 atomic update 支援,legacy 驅動的回調接口就可以直接使用 atomic helper 庫函數來實作了。

最後,還有将硬體限制和驅動程式限制導出給使用者空間的問題。這種情況很多,而且每次标準化一個新的 property 時,它就會變得更加複雜。試圖在接口中明确地描述限制條件很快就被認為是不現實的,核心唯一能做的事情就是拒絕不可能實作的狀态請求(甚至隻是轉換,因為有時這些都有可能成為限制條件)。但這需要驅動程式特有的使用者空間代碼 (譯者注:想想 Android HWComposer),導緻為實作通用接口而付出的努力全都白費了。

相反,Atomic ioctl 支援

DRM_MODE_ATOMIC_TEST_ONLY

flag,可以不真正向硬體送出更新。這樣,通用使用者空間就可以使用一些探測方法逐漸建構它想要配置的狀态,并每次測試該更新操作是否仍然有效,直到找到最大化配置參數。例如,Compositor 在使用硬體 plane 将給定的 client buffer 合成到螢幕上時,可以按照優先順序逐個添加 plane,最後使用 OpenGL 來處理剩餘的 client buffer。一旦一切準備就緒,它就可以将真正的更新操作塞入(queue)隊列裡,并且确信該操作是能被正确執行的(當然,前提是驅動程式沒有 bug)。這樣,使用者空間的通用代碼就可以使用具有各種限制的硬體,而無需在接口中顯式地描述這些限制。當然,不可能每次做出的判斷都是完美的,但在大多數情況下,這已經足夠好了。

這些問題很快就解決了,大約自三年前第一筆 RFC patch 出現以來,除了一些小的修補,上遊 atomic user-space ABI 一直沒有發生過變化。而花了幾年時間才解決的一個大問題則是 DRM Core 到 vendor driver 的接口(core-to-driver)。僅僅将相同的三元素組合(property)以清單的形式傳遞給驅動程式是最簡單的做法,而且也實作了概念驗證(proof-of-concept)。但是,對于 kernel 使用者來說,這意味着一個脆弱而冗長的接口,對于最終的 atomic 驅動的支援而言,将有太多這樣的回調接口來處理每個 legacy ioctl 指令和 legacy user(如 fbdev emulator)。簡單粗暴地傳遞解析後的資料結構(就像老的回調接口一樣)也不見得是個好方案,因為有時我們會面臨驅動程式私有擴充(driver-private extensions)和部分參數更新的需求。

目前已 merged 的解決方案十分簡單粗暴。首先,每種 KMS object 類型都有一個通用的 state 結構體,你可以為其配置設定 property。例如,plane 的 state 結構體如下:

struct drm_plane_state {
	struct drm_plane *plane;
	struct drm_crtc *crtc;
	struct drm_framebuffer *fb;

	/* Signed dest location allows it to be partially off screen */
	int32_t crtc_x, crtc_y;
	uint32_t crtc_w, crtc_h;

	/* ... */

	struct drm_atomic_state *state;
};
           

每個 state 結構體都有一個與之對應的 object 的指針,緊随其後的是該 object 所對應的 KMS state 在 kernel 中解析後的參數。對于 plane 而言,它是一個指向 plane 所綁定的 CRTC 指針(表示一個 display pipeline),以及一個指向它應該掃描的 framebuffer 指針、plane 在顯示視窗上的位置,以及為了友善閱讀而省略的其他内容。最後還有一個指向

drm_atomic_state

結構體的反向指針,它用來跟蹤每次更新操作時每個 object 的不同狀态,進而允許在對象級别上進行部分參數更新。

當更新最終被送出(commit)時,state 指針将存儲在每個 object 中,這裡對 plane 而言則是 plane->state。當送出 state 時,plane->state->state 反向指針也會被清空,因為一旦送出,state 的資料結構就由驅動程式保管,而不再由更新操作時的結構體來維護。

object 中的部分參數更新是通過複制(duplicate)已有 state 來實作的。驅動程式私有擴充(driver-private extensions)的支援是通過将

drm_plane_state

嵌入到它們自己特有的 state 結構體中來實作的。也就是說需要有一個

->atomic_duplicate_state()

回調接口,它具有像

drm_atomic_helper_plane_duplicate_state()

這樣的預設實作。由于 state 結構體指向的某些 object 會帶引用計數(如 framebuffer),是以還需要有一個

->atomic_destroy_state()

回調接口用來銷毀所有内容。所有的回調接口都有一個預設的實作,即使有些暫時還排不上用場 —— 通過這種方式,将一些 property 從 driver 特定的資料結構轉移到 DRM Core 結構中,就很容易對它們的處理過程進行标準化操作。

這樣,那些隻實作了通用 property 的通用代碼和驅動程式就不必直接處理原始 property ID 和 value 了,因為所有的解析都已在核心架構代碼所實作的 atomic ioctl() 中完成了。對于特殊的驅動程式,我們還有

->atomic_set_property()

->atomic_get_property()

回調接口,它們同樣是對 state 結構體進行操作,并用于解析其他新增的 property。

除了這些用來處理 per-object state 的功能函數與回調接口外,最主要的 atomic 驅動接口其實非常簡單,就兩個回調接口:

  • ->atomic_check()

    需要確定本次 atomic update 操作是能被正确執行的。它隻會從傳入的

    drm_atomic_state

    中檢視并寫入 object 所對應的 state 結構體。一方面,這是為了確定

    TEST_ONLY

    模式不會突然改變已經穩定的硬體或軟體狀态。另一個方面則是為了確定并發更新操作不會意外地出現互相踩踏 —— duplicate state 操作也會在内部持有相關的鎖。本系列文章的第 2 部分将更詳細地讨論持鎖。
  • ->atomic_commit()

    會将已準備就緒且經

    ->atomic_check()

    檢查通過的 state 進行送出,并鼓勵驅動程式将派生出來的 state(如顯示 clock 參數)存儲在其 state object 的私有資料結構中,以避免在 check 和 commit 回調函數中出現重複的代碼邏輯。注意,為了驅動程式的健壯性,在 duplicate state object 時應該清空它的派生 state,以確定總是計算得到正确的值。commit 函數隻允許因記憶體不足或硬體故障而傳回失敗,除此之外的其它任何異常(比如 GPU 缺少共享資源)都必須在前期的 check 回調接口中被提前捕獲。

下一篇我将讨論關于異步更新和鎖操作接口的更多細節。當然,驅動程式不必完全由自己來實作主要的 atomic 回調接口,因為這實作起來一點也不輕松 —— 我們還将介紹一個大型的 helper 庫。

下一篇:LWN 翻譯:Atomic Mode Setting 設計簡介(下)

文章彙總: DRM (Direct Rendering Manager) 學習簡介

繼續閱讀