圖形管線之旅 Part2

原文:《A trip through the Graphics Pipeline 2011》
翻譯:往昔之劍
 
轉載請註明出處
 
還沒那麼快
 
在上一篇,講述了渲染命令在被GPU處理前,經歷的各類階段。簡而言之,比你想像的要複雜。接下來,我將講述提過的命令處理器(command processor),最終都對command buffer作了哪些事情。啥?哪提過這個了——騙你的- -。這篇文章確實是第一次提到命令處理器,可是記住,全部command buffer都通過內存或系統內存來訪問PCIE和本地顯存。咱們將按順序通過管線,所以在咱們到達命令處理器以前,咱們先來聊聊內存。
 
內存系統
 
GPU沒有規則的內存系統,這不一樣於你常見的通用CPU或其它硬件,由於它被設計成多種用途。在常見的機器上 你能發現有兩個本質區別:第一,GPU內存系統帶寬很快,至關快。Core i7 2600K勉強能達到19GB/s的帶寬。GeForce GTX480 的帶寬接近180GB/s——差了一個數量級啊!
第二點,GPU內存系統頻率很慢,至關慢。Nehalem(第一代Core i7)主內存的cache miss大約140個時鐘週期,這是按照時鐘頻率除之內存延遲得來的數據(AnandTech給出的數據)。我剛提到的GeForce GTX 480的內存延遲大約400~800時鐘週期,比Core i7有4倍多的內存延遲。除此以外,Core i7的時鐘頻率是2.93GHz,而GTX 480 shader時鐘頻率表是1.4GHz——就是說,這裏還有兩倍的差距。哇塞,差出一個數量級了!靠,搞笑呢吧,我有點激動。這必定是一種權衡,繼續聽下去。
 
沒錯——GPU在帶寬上大量增長,可是它們要付出大量增長內存延遲的代價(事實證實,這至關耗電,不過已經超出本文討論範圍了)。這種模式上—— GPU的吞吐量受限於延遲。不要一味的等待結果,乾點什麼事情吧!
 
以上是你須要瞭解的關於GPU內存的知識,除了DRAM的趣聞外,後面講的也都很重要:DRAM芯片被組織成2D網格——不管邏輯上仍是物理上。有(水平)行線和(垂直)列線,這些線的每一個交叉點有一個晶體管和一個電容器。若是你想知道如何用這些材料製做內存,能夠訪問( https://tokyo.zxproxy.com/browse.php?u=V%2FmbvKGmGdz9QE4KTynGv27LALUJtfhzT4wC%2FQA%3D&b=14#Operation_principle)。總之,重點是DRAM的地址是被行地址和列地址分開的,DRAM的一次內部讀/寫老是訪問給出行的全部列。訪問內存一行上的全部列比訪問相同數量的多行內存的開銷要小得多。這僅是DRAM的一個小知識,但對後續來講很是重要。注意:之後再看一下這裏。把這裏,包括以前的內容聯繫起來,只讀幾個內存字節,並不能達到內存帶寬的最大值,若是想要內存帶寬飽和,應該一次讀滿一整行DRAM。
 
PCIe 主機接口
 
按照圖形程序員的觀點,這部分硬件沒什麼意思。實際上,這也是GPU的硬件架構。你也得關心它,由於它太慢的話也是瓶頸。因此得找靠譜的人把它弄好,確保沒問題。除此以外,它還能讓GPU讀/寫顯存和大量的寄存器,讓GPU訪問(一部分)主內存。讓人煩惱的是,這些傳輸的延遲比內存延遲更糟糕,由於得從芯片發出信號,到插槽裏,通過主板,而後到CPU上要很長時間。帶寬雖然合適——在16-lane PCIe 2.0接口上 可達8GB/s峯值,大部分是GPU使用的, 而CPU只佔1/3~1/2的帶寬。這個比例是能夠的。不像早期的AGP,是對稱的點對點連接——帶寬是雙向的。AGP有一個快速通道,從CPU到GPU上,可是反向是不行的。
 
最後一部分的內存小知識
 
老實說,咱們如今已經很是很是接近實際看到的3D指令了!你都快聞到它了。但還有一件事情咱們須要解決。由於如今咱們有兩種內存——(本地的)顯存和映射的系統內存。一個是往北走一天的路程,另外一個是往南走沿着PCIe高速公路一週的旅程,選哪一個?簡單的解決方案:只增長一條額外的地址線,來告訴你走哪條路。這就簡單了,頗有效並已經使用很長時間了。或者你多是在統一的內存架構上,好比某些遊戲主機(不包括PC)。那種狀況的話,就不用選了,只有內存是你要去的地方。若是你想更好一點,你就添加一個MMU(memory management unit內存管理單元),它提供給你徹底虛擬化的地址空間,並容許你搞一些很好的tricks,好比頻繁地訪問顯存中的紋理(這很快),其餘部分在系統內存裏,以及大部分徹底沒映射的——就跟憑空變出來同樣,一般,讀一個磁盤花50年的話, 這絕不誇張,訪問內存就比如是一天,這就是一個硬件的讀取花費時間,這至關的快。操蛋的磁盤!我跑題了……
 
當你顯存不夠用的時候,MMU還能整理顯存地址空間上的碎片,而不用實際拷貝。好東西啊,它讓多個進程共享同一GPU更簡單了。使用一個MMU確定是能夠的,但我不肯定是否須要它,儘管它至關好用(誰來幫幫我? 若是我搞懂了,我會更新這篇文章,可是如今我壓根不懂)。總之,MMU/虛擬內存並非你實際添加上去的(不像在架構裏的緩存和一致性存儲器),並不針對於某個特別的階段——我會在別的地方提到它,先把它放着。
 
還有個DMA引擎,能夠拷貝內存而不牽扯到咱們重要的3D硬件/shader內核。一般,它至少能夠在系統內存和顯存之間拷貝(雙向的)。它經常進行顯存複製(若是你須要整理顯存IPIan的話,這就頗有用)。它一般不能進行系統內存的拷貝,由於這是GPU,而不是內存拷貝單元——在CPU上執行系統內存拷貝,不用雙向的通過PCIe。
 
我畫了個圖,展現更多的細節——如今,你的GPU有多個內存控制器,每一個控制多個內存條,它們都得爭取到帶寬:)
 
好,來列個清單。CPU端有一個預置的command buffer,有了PCIE主機接口,CPU能夠通知咱們和寫寄存器。咱們得把邏輯轉變成地址載入,而後返回數據——若是它是從系統內存通過PCIe的,假如咱們想要獲取顯存中的command buffer。KMD會設置一個DMA傳輸,不管是CPU仍是GPU上的shader內核都不用管它。而後經過內存系統能夠拿到顯存中的拷貝數據,這就是咱們設置的全部過程,最後來看一下command buffer。
 
終於到了命令處理器
 
在開始討論命令處理器以前,已經作了不少準備工做,用一個詞歸納它,那就是「緩衝」。
 
如上所述,內存通道是高帶寬而且高延遲的。對於大多數GPU管線後續位而言,解決辦法是運行多個獨立的線程。但若是這樣作的話,咱們只有一個命令處理器,得考慮一下command buffer的順序(由於command buffer包含了狀態改變和執行渲染須要的正確隊列)。因此咱們接下來應該作的事情是:添加一個足夠大的緩衝區向前預取來避免間斷。
 
在該緩衝區中,命令處理器能到達實際的命令處理前端——基本上就是個知道如何解析指令的狀態機(按照硬件規範格式)。一些指令處理2D渲染操做——除非把命令處理器單獨分爲2D的,3D前端纔不用管它。無論怎樣,如今的GPU仍藏有檢測2D的硬件功能,就像是淘汰掉的VGA芯片的某個地方依然支持文本模式,4-bit/像素的位平面模式,平滑滾動之類的同樣。沒用顯微鏡就能發現這些淘汰掉的東西說明運氣不錯。總之,這些東西還存在,可是之後我就再也不講它們了:)而後是實際處理3D/shader管線裏一些圖元(primitive)的指令了。我會在接下來的部分講它們。還有一些指令在3D/shader管線裏因爲各類緣由(和各類管線設置)不參與渲染,後面都會講。
 
接下來是改變狀態的指令。做爲一個程序員,你能夠認爲它們只是改變了一些變量。可是GPU是一個巨大的並行計算器,在並行系統裏你不能只改變一個全局變量並想讓它正確工做——若是你不能保證全部東西都是一成不變的,最後就會出bug。有幾種常見的辦法,基本上全部的芯片都針對不一樣類型的狀態使用不一樣的方法:
  • 當改變一個狀態的時候,你得讓全部涉及到的工做都結束掉(即flush部分管線)。在過去,顯卡芯片都是這麼處理狀態改變的——這很簡單,而且批次少,三角面少,管線簡短的時候開銷不大。隨着批次和三角面數的增長,這種開銷也增長了。這種辦法限用於改變頻率不高的(只刷新整個管線的一部分影響不大)或者只實現開銷大/難度大的特殊需求。
  • 你可讓硬件單元徹底的無狀態。只傳遞狀態改變指令給指定的階段,而後週期性的把這個階段追加到當前狀態。這些狀態不會保存在哪裏——但老是存在,若是管線的其它階段想要知道這些狀態位是能夠的,由於已經當參數傳進來了(而後傳遞給下一階段)。若是你的狀態只改變少數位,那就不划算了。要是改變所有的紋理採樣狀態設置,那還行。
  • 有時只存一份狀態的拷貝,每次階段上都要改變一大堆的東西,都得刷新它。但要是存兩份拷貝(或四份)那就好多了,這樣前端的狀態設置就能夠提早了。要是你有足夠的寄存器(插槽)來位每一個狀態存儲兩個副本,一些激活的工做用插槽0,你能夠安全的修改插槽1而不用中止或干擾到工做的運行。如今你不要發送整個狀態到管線了——只有一個指令,選擇使用插槽0仍是1。固然,若是插槽0和1正在使用,又趕上一個狀態要改變的時候,你仍是得等,可是你能夠提早操做一步。這個技術不止用兩個插槽。
  • 對於採樣器或者紋理資源視圖(Shader Resource View)的狀態,你能夠在同一時間大量的設置,不過你也不會這麼作。你不會僅僅由於你要跟蹤兩條憑空的狀態集,就爲2*128的紋理保留狀態空間。對於這種狀況,你可使用一種寄存器重命名方案——擁有128個實際紋理描述的內存池。若是在一個shader裏真的須要128個紋理,那改變狀態將很是的慢。但大多狀況下,一個應用程序用不到20個紋理,你有至關多的空間來保障多個版本。
這些並不全面——但重點是在你的應用程序裏改變一個變量看似簡單(甚至UMD/KMD和command buffer也是)實際上可能須要背後大量的硬件支持來保證性能。
 
同步
 
指令的最後一部分是處理CPU/GPU和GPU/GPU同步。一般,這些形式都是「若是事件X發生,則執行Y」。我將先講「執行Y」的部分,它多是GPU告訴CPU如今該作什麼的推送通知(「CPU啊,我正要進入顯示設備0的垂直空白間隙VBI,因此你要是不想無效的翻緩衝,如今就趕忙幹活吧!」),或者也多是GPU只記錄發生了什麼,CPU能夠之後來詢問它(「說吧,GPU,你最近處理了哪一個command buffer片斷?」—「等我查一下啊,序列號是303。」)前者經過中斷來實現,只用在頻率不高的,優先級高的事件,由於中斷開銷很大。在這以後,須要每次觸發事件時,從command buffer將值寫入到CPU可見的GPU寄存器。
 
好比你有16個寄存器,將寄存器0賦值爲currentCommandBufferSeqId。給每次要提交到GPU的command buffer分配一個序列號(這步在KMD中完成),而後在每一個command buffer的開始部分,添加標記「若是到達這裏,就寫入register 0」。瞧,如今GPU知道正在處理哪一個command buffer了,咱們知道命令處理器會按序列嚴格執行完全部的指令,因此若是第一個指令command303被執行,那就是直到序列號爲302的command buffer都已經完成了,它們如今能夠被KMD從新利用、釋放、更改,或者想怎麼處理均可以。
 
關於「若是事件X發生」中的「事件X」是什麼,咱們來舉一個例子,好比「若是你到達這裏了」——大概就是這個意思。再好比,「若是在command buffer裏,shader讀取完全部渲染批次的貼圖以前」(這時候就代表回收再利用texture/render target的內存是安全的),「若是全部激活的render target/UAV已經處理完了」(這表示實際上能夠將它們做爲紋理安全使用了),「若是到目前爲止全部的操做都已經完成」,之類的等等。
 
這些操做一般被叫作「fences」,順表說一下,有不少種方法從狀態寄存器中取出寫入的值,但我以爲最靠譜的方法是使用一個順序計數器(可能借用了其它知識)。沒錯,這裏有些概念我沒講,由於我以爲你應該都懂。我之後可能會詳細說明:)
 
已經講了一半了——如今能夠從GPU返回狀態到CPU了,容許咱們在驅動程序裏作適當的內存管理(如今能夠知道,在何時能夠實際安全的複用vertex buffer,command buffer, texture和其它資源了)。但這還沒完——還漏了一個難點。要是咱們須要純粹的在GPU端同步呢?咱們回到剛纔render target的例子上,在實際的渲染完成以前,不能使用它做爲紋理(而且在其餘步驟發生的時候——曾經用過的紋理單元也有不少細節)。解決辦法是「等待」指令:「一直等到寄存器M有值N」。這能夠是等於,小於,或者更復雜的比較操做——簡單起見,只討論等於的狀況。在提交渲染批次以前,「等待」能夠容許咱們同步render target。還能夠容許咱們構建一個flush GPU的操做:「若是掛起的工做完成了,設置寄存器0爲++seqId」/「一直等到寄存器0有值seqId」。GPU/GPU同步就所有搞定了——在DX11的compute shader指令裏,有一種更細粒度的同步,這是GPU端惟一的同步機制。關於 規則渲染,你不須要了解太多。
 
順便說一下,若是你能夠寫CPU端的寄存器,你還能夠用另外一種方法——提交一個局部comand buffer,包含上一個的特殊值,而後讓 CPU端替代GPU端改變寄存器。這種方法能夠用來實現D3D11風格的多線程渲染,你能夠提交一個包含vertex/index buffer引用的渲染批次,CPU仍然要加鎖(有可能會正被另外一個線程寫入)。你僅須要在實際渲染調用以前發送一個等待指令,隨後一旦vertex/index buffer解鎖,CPU就能夠改變寄存器內容了。若是GPU沒收到這個指令,那麼等待指令就是一個空操做;若是收到了,就花費一些(命令處理器)時間處理。乾的漂亮吧?實際上, 若是你在提交指令以後更改command buffer,即便沒有CPU可寫的狀態寄存器,也能夠實現這種方法,只要有一個command buffer「跳轉」指令。細節留給讀者思考:)
 
固然,你沒必要須要這種設置寄存器/等待寄存器模型;對於GPU/GPU同步,你僅須要一個「render target barrier」指令,來確保render target能夠安全使用,還須要一個「flush全部東東」的指令。可是我更喜歡這種設置寄存器風格的模型,由於能夠一石二鳥(反饋給CPU正在使用的資源,和GPU自同步)。
 
這裏,我畫了一張圖。有點複雜,我說一下細節。基本想法是這樣:命令處理器開始部分有一個先入先出隊列(FIFO),跟着是指令解碼邏輯,由2D單元、3D前端(常規3D渲染)或者shader單元(compute shader)等多種塊來直接執行操做,還有一個塊處理同步/等待指令(包含我說過得公開可見寄存器),和一個處理command buffer跳轉/調用指令的單元(改變了當前的預取地址,轉向FIFO)。全部分派工做的單元都須要發送給咱們完成事件,因此咱們知道什麼時候紋理再也不被使用了,以及能夠再利用它們的內存。
 
結束語
 
下一步,才正真接觸渲染工做。最後還剩3個部分關於GPU,咱們開始看一下頂點數據!(三角形還沒被光柵化呢。還須要一些時間)
 
事實上,在這個階段,管線已經出現了分支;若是咱們運行compute shader,下一步將是compute shader階段。可是咱們先不講它,由於compute shader是後面的部分!先講常規渲染。
相關文章
相關標籤/搜索