渲染管線之旅|01 從App到硬件各個層級

系列文章原發布在自己搭建的博客上:https://binean.top/

0. 介紹從App到硬件各個層級 (Introduction: the Software stack)

我在這裏發佈了東西已經有一段時間了,我可能會用這個地方來解釋一些關於2011年圖形硬件和軟件的一般觀點。通常你可以找到你電腦中顯卡調用棧的相關描述,但是這些調用關係是如何工作?它們又是爲什麼要這樣呢?這些問題就不那麼容易找到答案了。我會盡量填補空白,而不會對特定的硬件進行具體的描述。我將主要討論在Windows系統上上運行d3d9/10/11的dx11級別GPU硬件,因爲這是我最熟悉的(PC)調用棧——不是API細節。一旦我們真正使用了GPU,所有的本地命令就變得非常重要了。

1 應用程序

應用程序指的是你寫的D3D/OpenGL應用,比如CrossFire, LOL這些遊戲。作爲這些應用的開發者來說,你要做的就是調用D3D/OpenGL提供的api接口,來實現自己想要的渲染效果。出現在應用程序中的的Bug就是需要應用程序開發程序員根據D3D/OpenGL的接口文檔或者來檢查自己程序的實現進行Bug的修復。

2 API runtime

在初接觸編程語言的時候我們就會經常的遇到Runtime這個詞,中文中一般解釋它爲運行時環境。比如說我們用C語言調用了printf的時候,那麼這個printf是從哪裏來的呢?C Runtime! 對比起來,使用D3D/OpenGL API函數那就需要D3D/OpenGL的Runtime,Runtime一般是以動態庫的方式存在,在程序運行的時候加載到應用程序的進程中。D3D/OpenGL應用程序中使用Runtime(d3d, OpenGL)提供的API進行資源創建/狀態設置/繪製等等操作。Runtime 會根據應用程序設置的當前狀態,驗證調用參數的合法性,並進行一致性檢查,管理用戶可見資源。此外,可能還會驗證着色器代碼(Shader code)和各個Stage之間的着色器連接(至少d3d中會做這些工作, 在OpenGL中這部分工作由驅動程序來完成),然後將處理結果全部移交給顯卡驅動程序——更準確地說,是用戶態顯卡驅動程序(UMD, user-mode driver)。

3 用戶態顯卡驅動程序 (UMD)

應用程序開發者有時候可能遇見這樣的“詭異”現象,按照D3D/OpenGL的API使用指南調用的,但是渲染的效果卻和理論上的不一致,或者是在一個廠商的顯卡上渲染出來的是這種效果,換一個顯卡發現渲染出來的效果卻完全不一樣。這些現象大多數都是在這裏發生的。同時,UMD驅動也是CPU和GPU交互的重要地方。
如果你的應用程序因爲API調用而出現了崩潰,問題通常就在這裏——“nvd3dum.dll” (NVidia) or "atiumd.dll” (AMD),其實可以使用調試工具看到crash在了那個dll層面上。這裏只列出了NVidia和AMD,其實還有很多其他的顯卡廠商。就像這些dll名字顯示的,他們的名字中通常都帶有um或者是umd, 這是意味着他們是User Mode Driver。UMD, 應用程序和API Runtime他們都處於相同的Context,和應用程序運行在相同的地址空間,不管是API Runtime或者是UMD,他們都是以dll的形式出現的,換句話說,在程序運行的過程中這些dll纔會加載到應用程序的進程中的,所以他們是處在同一個進程空間的,那麼他們肯定運行在相同的地址空間了。
UMD實現的是由d3d API Runtime調用的底層API(一般稱之爲 DDI), 這些DDI與你表面上看到的API(應用程序調用的)非常相似,但是它更加細化了很多處理,比如內存管理等等。

在UMD模塊中會處理類似於着色器編譯的事情,現在的應用程序通常將比較複雜絢麗的特效都用shader來編寫,其實這些shader就是在UMD中進行編譯的。
對於Direct3D來說,d3d Runtime向umd傳遞一個預先驗證的shader token數據流——D3D Runtime會檢查shader的語法,並且會檢查shader的編寫是遵循D3D的規範的(類型的使用時正確的,使用的C#, T#, S# 和U#都不超過可用範圍)。shader token中還使用到很多高級優化, 比如各種循環優化、死循環消除、持續傳播、預測IFS等。使用shader token對於driver來說大有裨益。但是,它還應用了一系列較低級別的優化(如寄存器分配和循環展開),這些比較低級別的優化驅動程序一般都寧願自己來做。概括的講,驅動程序通常會將shader token立即轉化爲中間表示(IR),然後再進行編譯;着色硬件與編譯的d3d字節碼非常接近。不需要額外工作一般就可以獲得比較好的結果(而且HLSL編譯器已經完成了一些高產量和高成本的優化,這是非常有幫助的)。

這裏有個有意思的事情:如果你的應用是一款非常著名的遊戲,那麼NV/AMD的程序員們很可能已經看過你的Shader,他們爲了讓自己的顯卡能更加流暢的運行你的應用程序,他們很可能會手動的替換你的Shader,這樣做的前提是他們的Shader渲染的結果最好和你的Shader渲染的結果一樣,不然這就鬧笑話了。這裏大家可以想到的就是各種跑分的軟件了,很多廠商都會對跑分軟件進行優化,爲的就是達到很高的跑分,雖然這對於用戶來講並不是什麼好事,但是高的跑分通常可以給顯卡廠商帶來更高的收益,因爲這直接反應的就是他們的硬件性能。

更有趣的是:一些API狀態可能最終會被編譯到着色器中。舉個例子,紋理採樣器中可能沒有實現某個相對奇特(或很少使用)的功能,比如說紋理邊界,但在着色器中通常使用額外的代碼進行模擬(或者根本不支持)。這意味着,對於不同的API狀態組合,同一個shader有時會有多個版本的編譯結果。

很多創建/編譯Shader的工作都是由驅動程序啓動的時候完成的,這也是爲什麼你打開遊戲的時候通常會等待比較長時間的原因。只有在實際需要時纔會執行需要的那些Shader(某些應用程序創建了很多的Shader, 但是有很多都是使用的垃圾!)。Graphics程序員知道的另外一面——如果你想確保某個東西是真的被創建的(而不是僅僅保留內存),你需要發出一個虛擬的繪製調用,使用它來“預熱它”。這雖然看起來很挫,但自從1999年第一次開始使用3D硬件以來,情況就是這樣的——也就是說,到目前爲止,這幾乎是生活中的一個事實,所以要習慣它。

還有內存管理之類的事情。UMD將獲得諸如紋理創建命令之類的東西,並需要爲它們提供空間。實際上,umd只是從kmd(內核模式驅動程序)中分配了一些更大的內存塊;實際上,映射和取消映射頁面(以及管理umd可以看到的那些顯存,以及gpu可以訪問哪些系統內存)是內核模式的特權,不能由umd完成。

但是UMD可以做一些像旋轉紋理(除非GPU可以在硬件中這樣做,通常使用的是2d傳輸單元而不是真正的3D管道)和系統內存和(映射的)顯存等之間的調度傳輸。最重要的是,一旦kmd分配並移交命令緩衝區,它還可以編寫命令緩衝區(一般稱之爲“dma buffer”——我將交替使用這兩個名稱)。命令緩衝區包含命令, 所有的狀態更改和繪圖操作都將由UMD轉換爲硬件能夠理解的命令。還有很多你不需要手動觸發的東西,比如上傳紋理和材質到顯存中。

一般來說,驅動程序將儘可能多地把實際的處理放到UMD中;UMD是用戶模式代碼,因此在其中運行的任何東西都不需要任何昂貴的內核模式轉換,它可以自由地分配內存,將工作分配給多個線程,因爲它只是一個常規的DLL(即使它是由API加載的,而不是直接通過你的應用程序)。這對驅動程序開發也有好處——如果UMD崩潰,應用程序也會崩潰,但不是整個系統,如果是KMD奔潰了那麼面臨的就是直接的藍屏了;UMD可以在系統運行時被替換(它只是一個DLL!);它可以使用常規調試器進行調試;所以它不僅效率高,而且調試和使用都方便。

前面我們已經說過UMD只是一個DLL。雖然這個dll是依靠D3D的runtime調用,而且可以直接和KMd進行交互,但它仍然是一個常規的dll,並且在調用它的進程的地址空間中運行。

我一直在說的這個“GPU”,他是所用應用共享的資源。然而,我們有多個應用程序試圖訪問它(並假裝它們是唯一一個這樣做的應用程序)。這不僅僅是自動的;在過去,解決方案是一次只給一個應用程序3D,而當這個應用程序處於活動狀態時,其他所有應用程序都無法訪問。但是,如果你想讓你的窗口系統使用GPU進行渲染,這並不能真正解決問題。這就是爲什麼您需要一些組件來仲裁對GPU的訪問並分配時間片等等。

4 進入調度器

調度器是一個系統組件,我在這裏談論的是圖形調度程序,而不是CPU或IO調度程序。這和你想象的完全一樣,它通過在不同的應用程序之間對3D管道進行時間切片來仲裁對它的訪問。上下文切換至少會導致GPU的一些狀態切換(它爲命令緩衝區生成額外的命令),並且可能還會交換顯存中或內存中的一些資源。當然,在任何給定的時間,只有一個進程能夠真正地向3D管道提交命令。
您經常會發現控制檯程序員抱怨PC上的3d API的多層次、易操作性由此帶來的性能成本。但問題是,PC上的3D API驅動程序確實比控制檯遊戲有更復雜的問題要解決——例如,它們確實需要跟蹤完整的當前狀態,因爲有人可能隨時從它們下面拉出隱藏的畫面!他們還圍繞着壞掉的應用程序工作,並試圖在背後解決性能問題;這是一個沒有人滿意的非常惱人的做法,當然包括驅動程序作者自己。但事實是,業務場景在這理顯得更爲重要;人們希望運行的東西繼續運行(並且運行得很順利)。你只是喊着“這是錯誤的”,不會贏得任何朋友。不管怎麼說,在運行流程上走進下一站:內核模式驅動!

5 用戶態顯卡驅動程序(The kernel-mode driver, KMD)

KMD是實際處理硬件的部分。系統中一次可能有多個UMD實例在運行,但KMD卻只有一個,如果KMD崩潰了,那麼Boom, 就直接的藍屏了。
即使有多個應用程序在爭奪它,GPU的內存也只有一個。有人需要調用快照並實際分配(和映射)物理內存。同樣,有人必須在啓動時初始化GPU,設置顯示模式(並從顯示器中獲取模式信息),管理硬件鼠標光標,這是有硬件處理的,而且真的只有一個!對硬件看門狗定時器進行編程,以便GPU能在一定時間內無響應、響應中斷等情況下(一般講這種狀態稱之爲hang)重置。這些就是KMD所做的。
對我們來說最重要的是,kmd管理實際的命令緩衝區。這個命令緩衝區就是硬件實際消耗的那個。UMD產生的命令緩衝並不是真正的緩衝區——事實上,它們只是GPU可尋址內存的隨機切片。它們實際發生的情況是,UMD完成它們,將它們提交給調度程序,然後等待該進程啓動,然後將UMD命令緩衝區傳遞給kmd。然後,kmd將對命令緩衝區的調用寫入主命令緩衝區,根據GPU命令處理器是否可以從主內存中讀取,它可能還需要首先將其DMA到視頻內存中。主命令緩衝區通常是一個相當小的環緩衝區(Ring buffer)——在那裏唯一能被寫入的東西就是系統/初始化命令和對“真實的”豐富的3D命令緩衝區的調用。
Ring buffer仍然只是內存中的一個緩衝區。顯卡知道Ring buffer的位置——通常有一個讀指針,它是GPU在主命令緩衝區中的位置,還有一個寫指針,它是KMD寫入緩衝區的距離(或者更準確地說,它告訴GPU它已經寫入的命令的數量)。

6 總線

當然,CMD不會直接進入顯卡,除非它集成在CPU芯片上!,因爲它需要先通過總線——通常是現在的PCI Express。DMA傳輸等採用相同的路徑。這不需要很長時間,但這是我們命令流進入的的另一個階段。

7 命令解析器

這是GPU的前端——實際讀取KMD寫入的命令的部分。我將在下一期文章中從這裏繼續,因爲這篇文章已經足夠長了。

8 順帶說說OpenGL

OpenGL與我剛纔描述的非常相似,只是API和UMD層之間沒有那麼明顯的區別。與d3d不同,glsl的shader編譯不由API處理,而是由驅動程序完成。這帶來的一個不幸副作用是,GLSL前端和3D硬件供應商一樣多,也就是說每個顯卡的廠商都要實現他們自己的GLSL的前端編譯。雖然它們都基於相同的規範,但都有自己的缺陷和特性。這也意味着驅動程序必須在看到着色器時自己進行所有優化——包括昂貴的優化。對於這個問題,d3d字節碼格式確實是一個更乾淨的解決方案——只有一個編譯器(所以不同供應商之間沒有稍微不兼容的方言!)它允許進行比通常情況下更昂貴的數據流分析。

9 遺漏

本片文章只是一個概述,忽略了很多微妙之處。例如,不僅有一個調度程序,還有多個實現(驅動程序可以選擇);還有關於如何處理CPU和GPU之間的同步的整個問題到目前爲止還沒有解釋過。後續的文章中慢慢的將這些都補上了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章