<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 剖析虛幻渲染體系(12)- 移動端專題Part 2(GPU架構和機制)

     

     

    12.4 移動渲染技術要點

    筆者這段時間研讀了近年來Siggraph、GDC關于移動端的Papers,查閱了Qualcomm、Arm、PowerVR等移動端GPU廠商和部分移動設備廠商的開發指南,在本章總結一下目前移動端常見的專用渲染技術。

    具體見后面的參考文獻列表。

    12.4.1 Tile-based (Deferred) Rendering

    TBR全稱是Tile-based Rendering,譯為基于分塊的渲染。它是目前移動端GPU架構中應用非常廣泛的一種技術,用來加速渲染,減少帶寬和能耗。

    TBDR全稱是Tile-based Deferred Rendering,是TBR的一種改進版,意為基于分塊的延遲渲染,最早由PowerVR應用于GPU芯片中。它最顯著的不同點在于通過了Early-Z測試的像素不會立即執行像素著色器,而是先標記該像素屬于哪個圖元。當Tile處理完所有圖元(場景中的所有物體),再繪制Tile上所有做了標記的像素。TBDR做到了硬件層級的遮擋像素剔除,減少OverDraw,減少帶寬和內存訪問。

    PowerVR的TBDR在開始渲染之前,會捕獲整個場景,這樣被遮擋的像素在被像素著色器之前就可以被識別和剔除。每個Tile都被光柵化并單獨處理,由于渲染的尺寸很小,使得允許所有數據都保存在非常快的Tile內存中。

    與TB(D)R對應的是用于PC的立即渲染(Immediately Rendering,IMR)模式。IMR、TBR、TBDR架構的對比圖如下:

    IMR、TBR、TBDR架構運行示意圖。其中紅色橢圓表示帶寬高,會引發性能瓶頸。

    對于IMR模式的GPU,若忽略并行處理邏輯,則執行的偽代碼如下所示:

    for draw in renderPass:
        for primitive in draw:
            for vertex in primitive:
                execute_vertex_shader(vertex)
            if primitive not culled:
                for fragment in primitive:
                    execute_fragment_shader(fragment)
    

    IMR的GPU硬件架構如下所示:

    其硬件數據流和內存交互圖如下:

    IMR模式的GPU的優勢在于,頂點著色器和其它幾何體相關著色器的輸出可以保留在GPU內的芯片上。這些著色器的輸出可以存儲在FIFO緩沖區,直到管道中的下一階段準備使用數據,GPU可以使用很少的外部內存帶寬存儲和檢索中間幾何結果。

    IMR模式的GPU的劣勢在于,像素著色在屏幕上跳躍,因為三角形按繪制順序處理,數據流中的任何三角形都可能覆蓋屏幕的任何部分(下圖)。意味著活動工作集是整個framebuffer的大小。例如,考慮一個分辨率為1440p的設備,使用32位每像素(BPP)的顏色,32位每像素的填充深度/模板,將提供30MB的總工作集,若全部存儲在on chip上,數據量過大,因此必須存儲在DRAM的off chip之外。

    IMR的并行渲染示意圖,隨機訪問遍布全屏幕,導致緩存命中率大大降低。

    在處理高分辨率畫面時,放置在內存上的帶寬負載可能非常高,因為每個像素都有多個讀-修改-寫操作。可以通過將最近訪問的framebuffer部分保持在靠近GPU的位置來減輕高帶寬負載。

    TB(D)R的GPU則與IMR GPU不同,它先將屏幕劃分成若干個固定大小的區域,然后再執行著色計算。下面是TBR的執行偽代碼:

    # Pass one
    for draw in renderPass:
        for primitive in draw:
            for vertex in primitive:
                execute_vertex_shader(vertex)
            if primitive not culled:
                append_tile_list(primitive)
    
    # Pass two
    for tile in renderPass:
        for primitive in tile:
            for fragment in primitive:
                execute_fragment_shader(fragment)
    

    TB(D)R GPU的硬件架構如下所示:

    其硬件數據流和內存交互圖如下:

    TB(D)R的優勢在于,Tile只占整個framebuffer的一小部分。 因此,可以將整個顏色、深度和模板的工作集存儲在快速的 on-chip RAM上,與GPU著色器核心緊密耦合。GPU用于深度測試和混合透明像素所需的framebuffer數據無需訪問外部內存即可獲得,通過減少GPU對通用framebuffer操作所需的外部內存訪問數量,可以顯著提高像素密集型內容的能源效率。此外,多數情況存在一個深度和模板緩沖,它們是瞬態的,只需要在著色過程中存在。如果明確告訴GPU驅動程序不需要保存附件(Attachment),那么驅動程序就不會將它們寫回主存。

    以下圖形API可以指示驅動程序丟棄附件:

    OpenGL ES 2.0:glDiscardFramebufferEXT

    OpenGL ES 3.0:glInvalidateFramebuffer

    Vulkan:恰當的渲染通道storeOp

    值得一提的是,由于每個Tile的尺寸通常不會很大,使得GPU計算單元訪問單個Tile內的數據具有良好的鄰域性,能夠提升Cache命中率。

    當然,天下沒有免費的午餐,TB(D)R同樣存在一些劣勢。例如,GPU必須將幾何通道的輸出(每個頂點的變化數據和Tile的中間狀態)存儲到主內存中,著色通道隨后讀取這些數據。因此,需要在與幾何圖形相關的額外帶寬成本和為framebuffer數據節省的帶寬之間取得平衡。同樣重要的是要考慮到一些渲染操作,比如曲面細分,對于TBR來說是不成比例的高消耗。曲面細分等操作被設計來適應IMR模式架構的優勢,因為幾何數據的爆炸可以在on-chip FIFO緩沖區內緩沖,而不是被寫回主存儲器。

    下面以Qualcomm Adreno系列GPU加以說明TB(D)R的架構、運行過程、涉及的渲染技術和優化技巧。

    TB(D)R的渲染不同于IMR模式,繪制過程分為分塊(Binning Pass)渲染(Rendering Pass)解析(Resolve Pass)3個階段。

    分塊(Binning Pass)過程大致如下:

    • 設定每個Bin(也被稱為Tile)的固定大小(2的N次方,長寬通常相同,具體尺寸因GPU廠商而異,如16x16、32x32、64x64),根據Frame Buffer尺寸設置可見數據流。

    • 轉換圖元坐標。注意此階段處理的是索引和頂點數據,某些GPU(如Adreno)會用特殊的簡化過的shader(而非完整的Vertex Shader)來處理坐標,以減少帶寬和能耗。此階段通常只有頂點的位置有效,其它頂點數據(紋理坐標、法線、切線、頂點顏色)都會被忽略。

    • 遍歷所有圖元,標記所有圖元覆蓋到的塊,將可見性數據寫入到被覆蓋的塊數據流中。

    • 將可見性數據流寫回系統顯存中。

    Binning階段的運行示意圖如下:

    渲染(Rendering Pass)過程大致如下:

    • 初始化渲染Pass。
    • 遍歷所有分塊,對每個分塊執行以下操作:
      • 利用分塊的可見性數據流,執行繪制調用。
      • 光柵化圖元。
      • 像素操作(像素著色器、深度模板測試、Alpha測試、混合)。
      • 寫入像素數據(顏色、深度、模板等等)到分塊芯片上的緩沖區(又被稱為On-Chip Memory、GMEM、Tiled Memory)。

    Rendering階段的運行示意圖如下:

    如果GPU上存在多個Tile處理單元,則可以同時處理多個Tile,并且Tile處理單元之間是相互獨立的:

    解析(Resolve Pass)階段過程如下:

    • 如果開啟了MSAA,在GMEM上的解析顏色、深度等數據(求平均值)。可以減少后續步驟GMEM傳輸到系統顯存的數據總量。

    • 將分塊上的所有像素數據(顏色、深度、模板等)寫入到系統顯存中。

    • 如果不是Frame Buffer的最后一個分塊,繼續執行下一個分塊。

    • 如果是Frame Buffer的最后一個分塊,交互緩沖區,執行下一幀的Binning Pass。

    解析階段的運行示意圖如下(注意Tile內的像素包含鋸齒,下方大畫面的是解析完MSAA帶抗鋸齒的像素):

    其中Binning Pass和Rendering Pass通常是分幀處理的,意味著Rendering Pass會落后Binning Pass一幀,以減少Stall,提升吞吐量,提升渲染效率。

    基于TB(D)R GPU架構的優化和技術還有很多,后面會涉及到。

    12.4.2 Hierarchical Tiling

    Hierarchical Tiling譯為層級分塊,是Arm Midgard系列芯片首次使用的分塊技術,顧名思義,它在分層的基礎上實現分塊。

    在這種情況下,使用Hierarchical Tiling允許Midgard使用可變的分塊大小,基于進一步分解分塊的想法(沿著層次結構向下,見下圖),直到分塊的復雜性達到預期的大小(或者達到最小的分塊復雜性)。這種技術使得Midgard只在必要的情況下使用小尺寸分塊,并通過在復雜性較低的場景使用大尺寸分塊來節省資源。

    更具體地說,Arm通常將不同層級的分塊(bin)設為以下的大小:

    • Hierarchy Level 0設為16x16像素;
    • Hierarchy Level 1設為32x32像素;
    • Hierarchy Level 2設為64x64像素;
    • Hierarchy Level 3設為128x128像素;
    • ......

    系統的目標是找出每個圖元覆蓋的分塊,更新分塊的結構信息。

    如果是小面積圖元(如上圖灰色三角形),由于影響到的塊比較少,用低層級的塊,以節省讀取帶寬。

    如果是大面積圖元(如上圖藍色三角形),由于影響到的塊比較多,用高層級的塊,以節省寫入帶寬。

    對于圖元復雜的情況,GPU會采用啟發性策略,以自動決定哪種是最佳的分布。

    至于啟發性策略的具體細節是怎樣的,目前還沒找到相關資料或文獻,如果以后找到了(或有同學提供)再補充。

    12.4.3 Early-Z

    Early-Z是提前深度測試,提供了一種快速遮擋方法,剔除不需要的渲染Pass的對象(屏幕空間的位置不可見的像素)。Adreno GPU可以以4倍的繪制像素填充率剔除被遮擋的像素。

    Early-Z通常發生在Rendering Pass階段的光柵化之后像素著色之前。(下圖)

    Early-Z技術可以將很多無效的像素提前剔除,避免它們進入消耗嚴重的像素著色器。Early-Z剔除的最小單位不是1像素,而是像素塊(pixel quad)。下面是其中的一個運行案例。

    Early-Z運行示意圖。左邊是已經渲染的存儲于深度緩沖的值,全部為1;中間是準備渲染的所有深度值為2的區域;右邊利用Early-Z技術剔除了比深度緩沖較大的像素。

    為了最大化發揮Early-Z技術,渲染引擎(如UE)會在渲染初期利用專門的Pass(如UE的PrePass),渲染出所有不透明物體的深度,發揮TBR架構的Early-Z技術。對支持TBDR架構的GPU,則無需此步。

    但是,以下情況會導致Early-Z失效:

    • 開啟Alpha Test:由于Alpha Test需要在像素著色器后面的Alpha Test階段比較,所以無法在像素著色器之前就決定該像素是否被剔除。
    • 開啟Tex Kill:即在shader代碼中有像素摒棄指令(DX的discard,OpenGL的clip)。
    • 關閉深度測試:Early-Z是建立在深度測試開啟的條件下,如果關閉了深度測試,也就無法啟用Early-Z技術。
    • 開啟Alpha To Coverage:Alpha To Coverage會開啟多采樣,會影響周邊像素,而Early-Z階段無法得知周邊像素是否被裁剪,故無法提前剔除。
    • 以及其它任何導致需要混合后面顏色的操作。

    12.4.4 Transaction Elimination

    Transaction Elimination (TE)是Mali GPU架構的一個關鍵帶寬節約功能,可以顯著節省芯片系統(SoC)上的能源。

    當執行TE時,GPU將當前幀緩沖區與之前渲染的幀進行比較,只對修改過的特定部分進行部分更新,從而大大減少了每幀向外部內存傳輸的數據量。以Tile為粒度進行比較,使用循環冗余檢查(Cyclic Redundancy Check,CRC)簽名來確定Tile是否已被修改(具有相同CRC簽名的Tile被認定是相同的,從而忽略該Tile的數據的傳輸)。

    循環冗余檢查(Cyclic Redundancy Check,CRC)是一種根據網絡數據包、計算機文件、內存數據流等數據產生簡短固定位數校驗碼的一種散列函數,主要用來檢測或校驗數據傳輸或者保存后可能出現的錯誤。這里被Mali GPU用來檢測本幀的Tile數據和之前的Frame Buffer數據是否相同。

    TE技術運行概要。當前幀會每個分塊計算一個CRC鍵值,以便下一幀比較每個分塊是否有數據變更,對于無變更的分塊取消數據傳輸。圖中右下角的綠色分塊和上一幀匹配,不需要傳輸數據到Frame Buffer。對于互動游戲而言,平均可以減少20%以上的帶寬。

    執行TE技術對最終圖像質量沒有影響,可用于GPU支持的所有幀緩存格式的所有應用程序,而無需考慮幀緩存精度要求。另外,需要注意的是,TE發生在Tile寫入數據到系統內存(Frame Buffer)期間(下圖左下方)。

    12.4.5 Forward Pixel Kill

    Forward Pixel Kill (FPK)是Mali-T62X和T678及之后的芯片內置的一種減少OverDraw的技術。

    在支持FPK的GPU中,像素著色的線程即便啟動,也不會不可逆轉地完成。正在進行的計算可以在任何時候終止,如果渲染管線發現后面的線程將把不透明的數據寫入相同的像素位置。因為每個線程都需要有限的時間來完成,所以有一個時間窗口,可以利用它來殺死已經在管道中的像素。實際上,利用管道的深度來模擬對未來的預見效應。

    支持FPK的GPU芯片內都存在FIFO(先進先出)緩沖區(介于Early Z測試和像素著色器之間,見下圖),用來存儲通過了Eearly-Z測試即將進入像素著色計算的Quad。

    舉個具體的例子說明FPK的運行機制,以下圖為例:

    上述圖中新的Quad(位置是10,深度是0)通過了EarlyZ測試,即將進入FPK FIFO緩沖區,結果發現FIFO中已經存在位置為10深度為10,新進來的Quad便會替換掉FIFO隊列的Quad(因為新的深度更靠近屏幕)。換而言之,FPK FIFO中的Quad會被新進的位置相同深度更小(近)的Quad替換掉。

    關于FPK需要補充幾點說明:

    • FPK剔除粒度是Quad(2x2像素塊)。
    • FPK只對不透明物體有效。
    • FPK必須開啟深度測試才能起作用。

    12.4.6 Hidden Surface Removal

    Hidden Surface Removal(HSR)譯為隱藏表面消除,是PowerVR芯片的專用技術,通過HSR技術,可以實現零OverDraw,而與繪制順序無關。

    左下是傳統GPU,不會對被遮擋的像素執行剔除,而右下展示了PowerVR利用HSR可以做到像素級剔除。

    在包含Early-Z測試的架構中,應用程序可以通過從前面到后面提交draw調用來避免一些OverDraw。按照這個順序提交可以建立深度緩沖區,因此遠離相機的被遮擋的像素可以盡早被剔除。然而,這給應用程序帶來了額外的負擔,因為每次相機或場景中的對象移動時,繪制都必須進行排序。它也不能刪除所有的OverDraw,因為逐繪制排序是非常粗糙的。例如,它不能解決由對象交叉引起的OverDraw。它還可以防止應用程序對繪制調用進行排序,以將圖形API狀態更改保持在最小值。

    使用PowerVR的TBDR,無論物體的提交順序如何(不排序),HSR將完全避免OverDraw。HSR能夠做到不依賴順序繪制的像素級剔除,最主要的做法是圖元光柵化后,會先標記該像素屬于哪個圖元(三角形),并存儲于Tag Buffer,待場景所有的圖元處理完畢,才進入片元著色器。HSR階段處于光柵化之后像素著色之前:

    12.4.7 Low Resolution Z pass

    Low Resolution Z pass簡稱LRZ,是Adreno A5X及以上的芯片在TBR執行Early-Z剔除時的優化技術。

    在Binning Pass階段,GPU會構造一個低分辨率的Z緩沖區,以LRZ-Tile(注意不是Bin Tile)為粒度來剔除被遮擋的區域,以提高Binning階段的性能。在測試全分辨率Z緩沖區之前,這個LRZ還可以在Rendering Pass中被用來有效地剔除像素。

    這個特性的優點是減少內存訪問和帶寬,減少渲染圖元,不需要應用程序從前到后繪制,提高幀率。

    但是,以下幾種情況會使LRZ技術失效:

    • 在像素著色器中寫入深度值。
    • 使用了圖形API(Vulkan)的次級命令緩沖區(secondary command buffer)。
    • 需要IMR直接渲染的任何條件。

    12.4.8 FlexRender

    FlexRender是Adreno芯片的獨有技術,是混合了TBR(Binning)和IMR(Direct Rendering)兩種模式的渲染技術,通過在兩種模式之間動態切換來最大化性能。

    FlexRender運行示意圖。Direct Rendering模式下,GPU繞過GMEM直接和系統內存交互;Binning模式下,GPU通過GMEM和系統內存交互。

    驅動程序和GPU分析給定渲染目標的渲染參數并自動選擇模式,比如渲染目標尺寸很小,會主動切換成IMR模式,以減少渲染消耗(TBR存在基礎消耗),如果是執行遮擋剔除,也會切換成IMR模式(哪怕之前處于TBR模式)。

    通常情況下,IMR模式要比TBR模式消耗的能量多:

    GFXBench Manhattan 3.0監控下的Snapdragon SoC在Direct和Binning模式的能耗對比,后者會省20%左右。

    12.4.9 Universal Bandwidth Compression

    Universal Bandwidth Compression (UBWC) 是Adreno A5x及之后的芯片加入的通用帶寬壓縮技術,是一種獨特的預測帶壓縮方案,通過最小化數據帶寬,可提高系統內存的有效吞吐量,實現顯著的節能。

    除了GPU芯片,UBWC技術在應用于Snapdraggon CPU的多個組件上,如顯示、視頻、相機等。壓縮支持YUV和RGB格式,減少內存瓶頸。

    UBWC雖然應用于高通的芯片上,但Google Developer Contributes Universal Bandwidth Compression To Freedreno Driver顯示該技術實際上是由Freedreno開源驅動器提供。UBWC具體用了何種壓縮算法,此文并未提及。

    12.4.10 Arm Frame Buffer Compression

    Arm Frame Buffer Compression (AFBC)專用于Arm設計的GPU中,解決了在移動設備的熱限制下創建越來越復雜的設計的難度。最重要的應用是視頻后處理,在許多使用情況下,GPU需要讀取視頻并在2D或3D場景中使用視頻流作為紋理時應用特效。在這種情況下,AFBC可以降低整個系統級帶寬和傳輸空間協調圖像數據的電力成本高達50%。

    AFBC運行示意圖。

    作為一種無損的壓縮協議和格式,AFBC最小化在SoC的IP塊之間的數據傳輸量。具體低說,AFBC有如下特點:

    • 無損壓縮格式。壓縮格式保留原始圖像精度,壓縮率和其它無損的壓縮標準相媲美。
    • 被Mali GPU完全支持。
    • 減少能量消耗。主要受益于帶寬的減少。
    • SoC設計的區域效率高。AFBC可以在設計中以零面積成本添加。
    • 有界的最壞情況壓縮比。最壞情況(隨機訪問)的效率下降成4x4級別。
    • 支持YUV和RGB格式。YUV壓縮比一般為50%。

    12.4.11 Index-Driven Vertex Shading

    Index-Driven Vertex Shading(IDVS)是Mali GPU內的一種頂點處理優化技術,發生在每個Render Pass的頂點處理階段。

    IDVS的主要特點在于將傳統的頂點著色器拆分為兩個階段:

    • 第一階段是位置著色(Position Shading),發生在各類頂點Culling之前,此階段只轉換頂點位置,而不執行頂點的其它操作。
    • 第二階段是可變著色(Varying Shading),發生在各類頂點Culling之后,只處理通過各類Culling的頂點,執行頂點的位置轉換之外的其它操作。

    IDVS將頂點著色器拆分為位置著色(Position Shading)和可變著色(Varying Shading)兩個階段。

    IDVS技術的優勢在于:

    • Varying Shading大多數情況消耗的性能要比Position Shading大,通過各類頂點Culling階段剔除掉無效的頂點,從而避免進入消耗大的Varying Shading。

    • 通過匹配IDVS技術的頂點屬性布局,可以減少數據讀取量,提升Cache命中率,提升性能,降低功耗。匹配IDVS技術的頂點屬性布局如下:

      • 將頂點的位置單獨成一個數據流,數據流布局如下:

        xyz | xyz | xyz | ...
        
      • 將頂點除位置之外的屬性按照SoA(Structure of Array)布局,例如:

        color,uv,normal | color,uv,normal | color,uv,normal | ...
        

    IDVS頂點數據流拆分優化及交互示意圖。

    12.4.12 Pixel Local Storage

    Pixel Local Storage(PLS)是OpenGL ES的一種數據存取方式,用PLS聲明的數據將保存在GPU的Tile buffer上(下圖)。

    PLS啟用時,渲染管線可以高效地執行顏色操作、混合等。GLSL聲明PLS數據關鍵字有三種,說明如下表:

    關鍵字 作用
    __pixel_localEXT 可讀可寫數據。
    __pixel_local_inEXT 只讀數據。
    __pixel_local_outEXT 只寫數據。

    PLS的應用以延遲渲染為例,則偽代碼如下所示:

    // ------GBuffer生成------
    __pixel_local_outEXT FragData // 只寫數據
    {
        layout(rgba8) highp vec4 Color;
        layout(rg16f) highp vec2 NormalXY;
        layout(rg16f) highp vec2 NormalZ_LightingB;
        layout(rg16f) highp vec2 LightingRG;
    }gbuf;
    
    void main()
    {
        gbuf.Color = CalcDiffuseColor();
        vec3 Normal = CalcNormal();
        gbuf.NormalXY = Normal.xy;
        gbuf.NormalZ_LightingB.x = Normal.Z;
    }
    
    // ------光照累積------
    __pixel_localEXT FragData // 可讀寫數據
    {
        layout(rgba8) highp vec4 Color;
        layout(rg16f) highp vec2 NormalXY;
        layout(rg16f) highp vec2 NormalZ_LightingB;
        layout(rg16f) highp vec2 LightingRG;
    }gbuf;
    
    void main()
    {
        vec3 Lighting = CalcLighting(gbuf.NormalXY, gbuf.NormalZ_LightingB.x);
        gbuf.LightingRG += Lighting.xy;
        gbuf.NormalZ_LightingB.y += Lighting.z;
    }
    
    // ------最終著色------
    __pixel_local_inEXT FragData // 只讀數據
    {
        layout(rgba8) highp vec4 Color;
        layout(rg16f) highp vec2 NormalXY;
        layout(rg16f) highp vec2 NormalZ_LightingB;
        layout(rg16f) highp vec2 LightingRG;
    }gbuf;
    
    out highp vec4 FragColor;
    
    void main()
    {
        FragColor = resolve(gbuf.Color, gbuf.LightingRG, gbuf.NormalZ_LightingB.y);
    }
    

    利用PLS執行延遲渲染的運行示意圖如下(注意右上方小方塊的紅色代表渲染幾何數據階段,綠色代表渲染光照階段):

    除了OpenGL ES,Metal、Vulkan、D3D等圖形API也提供了相應的接口、關鍵字或標記支持GPU Tile上的數據操作。

    以上代碼顯示,延遲著色所需的GBuffer數據一直處于PLS之中,最好解析后返回最終顏色,而不需要將GBuffer寫回系統內存(下圖)。

    PLS能夠提升22%左右的性能:

    UE4還利用PLS實現了高效的粒子軟混合:

    左:粒子一般混合模式;右:粒子軟混合模式。

    Vulkan也有類似的機制,被稱為Subpass,見后面章節。

    12.4.13 subpass

    subpass(子通道)是順應TB(D)R硬件架構的產物,適用于Vulkan、DX12、Metal等現代圖形API,底層原理類似于Pixel Local Storage。

    使用Subpass需滿足以下幾點特殊的要求:

    • 所有subpass必須在同一個Render Pass中。
    • 不需要采樣周邊鄰域像素。(否則會跨Tile訪問數據,無法保持所有數據訪問在同一個Tile內)
    • GPU支持TB(D)R的硬件架構。
    • Vulkan、DX12、Metal等現代圖形API。

    每個RenderPass和Subpass都可以為每個Attachment指定loadOp和storeOp,以便精確控制它們的存取行為:

    subpass的loadOp標記有3種:

    • LOAD_OP_LOAD:從全局內存加載Attachment到Tile。
    • LOAD_OP_CLEAR:清理Tile緩沖區的數據。
    • LOAD_OP_DONT_CARE:不對Tile緩沖區的數據做任何操作,通常用于Tile內的數據會被全部重新,效率高于LOAD_OP_CLEAR。

    以上3個標記執行的效率:LOAD_OP_DONT_CARE > LOAD_OP_CLEAR > LOAD_OP_LOAD。Vulkan使用示例代碼:

    VkAttachmentDescription colorAttachment = {};
    colorAttachment.format = VK_FORMAT_B8G8R8A8_SRGB;
    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
    // 標明loadOp為DONT_CARE.
    colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    

    subpass的storeOp標記有2種:

    • STORE_OP_STORE:將Tile內的數據存儲到全局內存。
    • STORE_OP_DONT_CARE:不對Tile緩沖區的數據做任何存儲操作。

    以上兩個標記的執行效率:STORE_OP_DONT_CARE > STORE_OP_STORE。Vulkan使用示例代碼:

    VkAttachmentDescription colorAttachment = {};
    colorAttachment.format = VK_FORMAT_B8G8R8A8_SRGB;
    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
    // 標明loadOp為DONT_CARE.
    colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    // 標明storeOp為DONT_CARE.
    colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    

    不像OpenGL ES在Shader中有顯式的關鍵字(__pixel_localEXT__pixel_local_inEXT__pixel_local_outEXT)來聲明Tile內變量,Vulkan為了讓Attachment存儲到Tile內,必須使用標記TRANSIENT_ATTACHMENTLAZILY_ALLOCATED

    VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
    imageInfo.flags		= flags;
    imageInfo.imageType	= type;
    imageInfo.format	= format;
    imageInfo.extent	= extent;
    imageInfo.samples	= sampleCount;
    // Image使用TRANSIENT_ATTACHMENT的標記.
    imageInfo.usage		= VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
    
    VmaAllocation memory;
    VmaAllocationCreateInfo memoryInfo{};
    memoryInfo.usage		  = memoryUsage;
    // Image所在的內存使用LAZILY_ALLOCATED的標記.
    memoryInfo.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;
    
    // 創建Image.
    auto result = vmaCreateImage(device.get_memory_allocator(), &imageInfo, memoryInfo, &handle, &memory, nullptr);
    

    使用subpass的loadOp和storeOp進行優化之后,Vulkan的官方測試示例顯示可以減少36%的全局內存讀取、62%的全局內存寫入、7%的片元執行周期:

    另外,使用正確的storeOp和loadOp可以高效地在Tile內解析MSAA數據,具體說明如下:

    • 帶MSAA的Image(或attachment)必須是瞬態的(transient),通過以下標記可在Render Pass結束時獲得解析MSAA后的數據:

      • loadOp = LOAD_OP_CLEAR;
      • storeOp = STORE_OP_DONT_CARE;
      • 使用LAZILY_ALLOCATED的內存標記;
      • 在subpass使用pResolveAttachments標記。
    • 對于深度模板的Attachment,也可以獲得類似的效果:

      • 使用VK_KHR_depth_stencil_resolve標記;
      • Vulkan 1.2及以上的API才支持。

    通過以上方式可以高效地在Tile內解析掉MSAA數據,而不會傳輸MSAA數據到全局內存。此外,需要避免使用vkCmdResolveImage接口解析MSAA:

    上:使用vkCmdResolveImage解析MSAA的錯誤示范;下:使用Tile內解析MSAA的正確示范。

    使用subpass的loadOp和storeOp對MSAA解析進行優化之后,Vulkan的官方測試示例顯示可以減少261%的全局內存讀取、440%的全局內存寫入!!

    優化效果可見一斑!!還等什么,盡管拿起subpass的有利武器對應用程序進行優化吧!!

    更多說明參見Vulkan官方組織KhronosGroup的github:Appropriate use of render pass attachments

    有關UE對Subpass的封裝可參見:10.4.4.2 Subpass渲染

    12.4.14 Adaptive Scalable Texture Compression

    Adaptive Scalable Texture Compression (ASTC)是Arm和AMD共同研發的一種紋理壓縮格式,不同于ETC和ETC2的固定塊尺寸(4x4),ASTC支持可變塊大小的壓縮,從而獲得靈活的更大壓縮率的紋理數據,降低GPU的帶寬和能耗。

    ASTC雖然尚未成為OpenGL的標準格式,只是以擴展的形式存在,但目前已經廣泛地被主流GPU支持,可謂不是標準的的標準擴展。但在Vulkan中,ASTC已經是標準的特性了。具體地說,ASTC支持以下特性:

    • 格式靈活。ASTC可以壓縮1到4個通道之間的數據,包括一個非相關通道,如RGB+A(相關RGB,非相關alpha)。并且塊大小可變,如4x4、5x4、6x5、10X5等。

      Adreno A5X及以上的GPU芯片支持ASTC以下不同塊大小的格式(包含二維和三維):

      • ASTC_4X4

        ASTC_5X4

      • ASTC_5X5

      • ASTC_6X5

      • ASTC_6X6

      • ASTC_8X5

      • ASTC_8X6

      • ASTC_8X8

      • ASTC_10X5

      • ASTC_10X6

      • ASTC_10X8

      • ASTC_10X10

      • ASTC_12X10

      • ASTC_12X12

      • ASTC_3X3X3

      • ASTC_4X3X3

      • ASTC_4X4X3

      • ASTC_4X4X4

      • ASTC_5X4X4

      • ASTC_5X5X4

      • ASTC_5X5X5

      • ASTC_6X5X5

      • ASTC_6X6X5

      • ASTC_6X6X6

    • 靈活的比特率。ASTC在壓縮圖像時提供了廣泛的比特率選擇,在0.89位和8位每texel (bpt)之間。比特率的選擇與顏色格式的選擇無關。而傳統的ETC等格式只能是整數的比特率。

    • 高級格式支持。ASTC可以壓縮圖像在低動態范圍(LDR)、LDR sRGB、高動態范圍(HDR)顏色空間,還可以壓縮3D體積紋理。

    • 改善圖像質量。盡管具有高度的格式靈活性,但在同等比特率下,ASTC在圖像質量上的表現優于幾乎所有傳統的紋理壓縮格式(ETC2、PVRCT和BC等)。

    • 格式矩陣全覆蓋。在ASTC尚未出現之前,傳統的紋理壓縮格式支持的顏色格式和比特率的組合相對較少,如下圖所示:

      以上格式還受圖形API或操作系統限制,因此任何單一平臺的壓縮選擇都非常有限。ASTC的出現解決了上述問題,幾乎實現了所需格式矩陣的完整覆蓋,為內容創建者提供了廣泛的比特率選擇。下圖顯示了可用的格式和比特率:

    ASCT是如何達成上述目標的呢?答案就在于ASTC用了一種特殊的壓縮算法和數據結構。ASTC的算法技術要點和闡述如下:

    • 塊壓縮

      實時圖形的壓縮格式需要能夠快速有效地將隨機樣本轉換為紋理,因此對壓縮技術必須做到以下幾點:

      • 僅給定一個采樣坐標,計算內存中數據的地址。
      • 能夠在不解壓太多周圍數據的前提下解壓隨機采樣。

      所有當代實時壓縮格式(包括ASTC)使用的標準解決方案,是將圖像分割成固定大小的像素塊,然后每個塊被壓縮成固定數量的輸出位。這保證Shader以任意順序快速訪問texels,并具有良好的解壓成本。

      ASTC中的2D Block footprints范圍從4x4 texels到12x12 texels,它們都被壓縮成128位輸出塊。通過將128位除以占用空間中的像素數,便能得到格式比特率,這些比特率范圍從8 bpt(\(128 / (4\cdot4)\))到0.89 bpt(\(128 / (12\cdot12)\))。下面是不同比特率的畫質對比圖:

    • 顏色端點(Color endpoint)

      塊的顏色數據被編碼為兩個顏色端點之間的梯度。每個texel沿著梯度選擇一個位置,然后在解壓期間插值。ASTC支持16色端點編碼方案,稱為端點模式( endpoint mode)。端點模式的選項允許改變以下內容:

      • 顏色通道的數量。 例如:亮度、亮度+alpha、rgb或rgba。

      • 編碼方法。 例如:直接、基數+偏移、基數+比例或量化級別。

      • 數據范圍。 例如:低動態范圍或高動態范圍。

      允許逐塊選擇不同的端點模式和端點顏色BISE量化級別。

    • 顏色分區(Color partition)

      塊內的顏色通常是復雜的,單色漸變通常不能準確地捕捉塊內的所有顏色。例如,躺在綠色草地上的紅球,需要進行兩種顏色的劃分,如下圖所示:

      ASTC允許單個塊最多引用四個顏色梯度,稱為分區。為了解壓,每個texel被分配到一個單獨的分區。

      直接存儲每個texel的分區分配將需要大量的解壓縮硬件來存儲所有塊大小。 相反,ASTC使用分區索引作為seed值,以算法生成一系列模式。壓縮過程為每個塊選擇最佳匹配的模式,然后塊只需要存儲最佳匹配模式的索引。下圖顯示了8 × 8塊大小的2個(圖像頂部)、3個(圖像中間)和4個(圖像底部)分區生成的模式:

      可以在每個塊的基礎上選擇分區的數量和分區索引,并且可以在每個分區上選擇不同的顏色端點模式。

    • 顏色編碼

      ASTC使用漸變來指定每個texel的顏色值。每個壓縮塊存儲漸變的端點顏色,以及每個像素的插值權重。在解壓過程中,每個像素的顏色值是根據每個像素的權重在兩個端點顏色之間插值生成的。下圖顯示了各種texel權重的插值:

      方塊通常包含復雜的顏色分布,例如一個紅色的球放在綠色的草地上。在這些情況下,單一的顏色梯度不能準確地代表所有不同的texel顏色值。 ASTC允許一個塊定義多達四個不同的顏色梯度,稱為分區(partition),并可以將每個texel分配到一個單獨的分區。下圖顯示了分區索引是如何為每個texel指定顏色漸變的(兩個分區,一個用于紅球像素,一個用于綠草像素):

    • 存儲字符表(Storing alphabet)

      盡管每個像素的顏色和權重值理論上是浮點值,但可以直接存儲實際值的位太少了。為了減小存儲大小,必須在壓縮期間對這些值進行量化。例如,如果對0.0到1.0范圍內的每個texel有一個浮點權重,可以選擇量化到5個值:0.0、0.25、0.5、0.75和1.0,再使用整數0-4來表示存儲中的這五個量化值。

      一般情況下,如果選擇量化N層,需要能夠有效地存儲包含N個符號的字符表中的字符。一個N個符號表包含每個字符的log2(N)位信息。如果有一個由5個可能的符號組成的字符表,那么每個字符包含大約2.32位的信息,但是簡單的二進制存儲需要四舍五入到3位,這浪費了22.3%的存儲容量。下圖表顯示了使用簡單的二進制編碼存儲任意N個符號字符表所浪費的位空間百分比:

      上述圖表顯示,對于大多數字符大小,使用整數位每個字符浪費大量的存儲容量。對于壓縮格式來說,效率是至關重要的,因此這是ASTC需要解決的問題。

      一種解決方案是將量化級別四舍五入到2的下一次方,這樣就不用浪費額外的比特了。然而,這種解決方案迫使編碼器消耗了本可以在其它地方使用獲得更大收益的比特位,因此此方案降低了圖像質量,并非最優解決方案。

    • 五元和三元數(Quint and trit)

      一個更有效的解決方案是將三個五元字符組合在一起,而不是將一個五元字符組合成三個位。五個字母中的三個字符有\(5^3=125\)個組合,包含6.97位信息。我們可以以7位的形式存儲這三個quint字符,而存儲浪費僅為0.5%。

      我們也可以用類似的方法構造一個三符號的字母表,稱為三個一組,并將五個一組的三個一組字符組合起來。每個字符組有\(3^5=243\)個組合,包含7.92位信息。我們可以以8位的形式存儲這5個trit字符,而存儲浪費僅為1%。

    • 有界整數序列編碼(Bounded Integer Sequence Encoding)

      ASTC使用的有界整數序列編碼(Bounded Integer Sequence Encoding,BISE)允許使用最多256個符號的任意字符存儲字符序列。每一個字符大小都是用最節省空間的位、元和五元進行編碼的。

      • 包含最多\(2^n-1\)個符號的字母表可以使用每個字符n位進行編碼。
      • 包含最多\(3\cdot(2^n - 1)\)個符號的字母表可以使用每個字符用n位(m)和一個trit (t)進行編碼,并使用方程\((t \cdot 2^n) + m\)重建。
      • 包含最多\(5\cdot(2^n - 1)\)個符號的字母表可以使用每個字符用n位(m)和一個quint (q)進行編碼,并使用方程\((q \cdot 2^n) + m\)重建。

      當序列中的字符數不是3或5的倍數時,必須避免在序列末尾浪費存儲空間,因此在編碼上添加了另一個約束。如果序列中要編碼的最后幾個值為零,則已編碼位串的最后幾個位也必須為零。理想情況下,非零位的數目很容易計算,并且不依賴于先前編碼值的大小。這在壓縮期間很難妥當處理,但也是可能解決的。意味著不需要在位序列結束后存儲任何填充,因為我們可以安全地假設它們是零位。

      有了這個約束,通過對bit、trit和quint的智能打包,BISE使用固定位數對N個符號字母表中的S個字符串進行編碼:

      • S最大值為\(2^N - 1\) ,使用 \(N \cdot S\)位。
      • S最大值為\(3\cdot2^N - 1\) ,使用 \(N\cdot S + \text{ceil}(8S / 5)\)位。
      • S最大值為\(5\cdot2^N - 1\) ,使用 \(N\cdot S + \text{ceil}(7S / 3)\)位。

      壓縮器選擇為所存儲的字母大小產生最小存儲空間的選項。一些使用二進制,一些使用bit和trit,還有一些使用bit和quint。下圖顯示了BISE存儲相對于二進制存儲的效率增益:

    此外,在壓縮過程中,會為每個塊選擇最佳編碼,在計算texel權重值時,除了上述的BISE,還有雙平面權重(Dual-plane weights)算法。

    ASTC免費自由使用,容易集成,被眾多主流系統和硬件支持。支持ASTC需要以下OpenGL擴展:

    GL_AMD_compressed_ATC_texture
    GL_ANDROID_extension_pack_es31a
    

    相比傳統的紋理壓縮格式(ETC、BC、PVRTC等),使用ASTC的壓縮效果非常明顯,畫質更貼近原圖,壓縮率更高:

    左:原始法線貼圖;中:壓縮成ETC的效果;右:壓縮成ASTC的效果。

    由此帶來的直觀收益就是占用更少的內存、帶寬,每幀大約能減少24.4%的帶寬:

    關于ASTC的更多詳情可參看Adaptive Scalable Texture Compression

    12.4.15 big.LITTLE Core

    移動端CPU(注意不是GPU,如Qualcomm Keyo CPU)存在big.LITTLE的組合架構,最早由Arm提出。此架構同時存在big core和little core,big core為了高性能而優化,little core為了能量消耗而優化。

    Qualcomm Keyo CPU的big.LITTLE架構。左邊4個是big core,執行性能高但耗電量較大,右邊4個是little core,執行性能較低但較省電。

    big.LITTLE架構的特點如下:

    • 通過將兩個非常不同的處理器組合在一個SoC中,以應對智能設備在性能方面需求的變化。
    • big.LITTLE軟件自動處理任務分配到適當的CPU核。操作系統直接感知系統中的高性能和高效率核心,并可以根據性能需求將每個任務動態分配到合適的核心。

    理解以及如何使用這種架構的特性對于優化性能和功率效率至關重要,優化得好,將獲得更長的游戲時間和游戲的散熱。

    為了提升big.LITTLE的效率,盡量優先使用little core。假設幀預期時間為16ms (60FPS),開發者可以使用工具(如Snapdragon Profiler)來識別任務,將其移至LITTLE core。例如,一款帶有布料模擬的游戲,在big core上執行需要3毫秒,而在little core執行可能需要10毫秒。只要這個執行時間是可以接受的(本例的幀預算是16ms),應該被移到little core中,減少對big core的利用,提高電力效率。

    移動端SoC制造廠商(如Qualcomm、Arm)通常提供了相關SDK和API給開發者指定任務在哪種類型的CPU核上運行,具體可參看:Controlling Task Execution

    12.4.16 其它技術要點

    除了以上小節涉及的技術要點,實際上移動端芯片或圖形API還存在很多其它技術,比如SIMD、SIMT、Unified shader architecture(統一著色器架構,見下圖)、Scalar architecture(標量著色器架構)、Tripipe(下下圖)等等。更多技術細節可以閱讀筆者的另一篇關于GPU的文章:深入GPU硬件架構及運行機制

    左:分離式著色器處理單元,右:統一著色器處理單元。可見后者的處理器基本處于滿負荷運行,從而減少等待和空載,提升整體運算能力。

    Mali GPU中的Tripipe結構示意圖,包含3個運算單元、1個存取單元和1個紋理單元,擁有128bit帶寬,2倍FP64、4倍FP32、8倍FP16的操運算效率。

    另外,OpenGL ES還有不少擴展可以提升性能,比如針對紋理子區域讀寫操作:

    KHR_partial_update
    EXT_buffer_age
    

    此擴展允許調用者利用Backbuffer的時間指定多個方框繪制幀內容。此技術類似于TE,但不會寫數據到Tile緩沖區。

     

    12.5 移動GPU架構和機制

    本章將闡述移動端GPU的硬件架構和運行機制。

    12.5.1 移動GPU概述

    移動端GPU由于便攜性,需要考慮PPA三個指標,因此設計一款高性能的GPU異常困難,具有高度的挑戰性。目前主要有Qualcomm、Arm、Imagination Tech等GPU制作廠商,他們的代表作分別是Adreno、Mali、PowerVR。移動端的GPU通常集成在SoC之中,和CPU、內存等器件形成有機的硬件架構體系。

    Snapdragon框架圖。包含了了CPU、Adreno GPU、內存等元件,通過Bus、Network等進行數據交互。

    隨著時間推移,移動端硬件隨之發展,越來越多新的圖形API和渲染特性也被遷移到移動端,具體表現在:

    • 主流GPU支持DX12、Vulkan1.2、OpenGL ES 3.2等圖形API,支持VRS、Mesh Shading、Ray Tracing、WaveMath等新的渲染特性。

    • GPU吞吐量和計算能力大幅提升,包含ALU、Texture、Memory等方面:

      Qualcomm Adreno 640 GPU的性能一覽,右側是Xbox One的性能數據。

    • 內存帶寬增加,能耗比提升。

    • 電量節省特性大量涌現。

      • Render Target Compression, FP16 math ops, ASTC, Vulkan Subpasses。
      • UBWC、AFBC、IDVS、PLS等。
    • 移動端Soc被廣泛地應用于VR應用,并帶來了諸多專用優化技術。

    • Compute Shader能力的完善和提升,對OpenCL庫的支持趨于完善。

    • 并行數量越來越多,吞吐量提升。

      CPU和GPU運行示意圖,可知GPU緩存小但擁有數量眾多的線程。

    移動端GPU架構內的相關概念和名詞解析如下:

    概念 全稱 解析
    AMBA Advanced Microcontroller Bus Architecture 高級微控制器總線架構
    AXI AMBA Advanced eXtensible Interface AMBA高級可擴展接口
    APB AMBA Advanced Peripherial Bus AMBA高級外圍總線
    ACE AMBA AXI Coherency Extensions AMBA AXI一致性擴展
    GPU Graphics Processing Unit 圖形處理單元
    VPU Video Processing Unit 視頻處理單元
    DPU Display Processing Unit 顯示處理單元
    ISA Instruction Set Architecture 指令集架構
    SIMD Single Instruction Multiple Data 單指令多數據
    ISP Image Synthesis Processor 合成圖像處理器
    TSP Texture and Shading Processor 紋理和著色處理器

    12.5.2 移動GPU運行機制

    由于每個GPU廠商、每個系列、每代產品的運行機制都可能存在不同,本節就以Mali GPU為例,闡述移動端的GPU運行機制。首先說明一下Arm Mali T880 GPU硬件架構的參數,如下:

    • 16個Shader Core(SC)。

    • Tile尺寸為16x16(內部4x4~32x32)。

      • 可存儲深度模板緩沖,128位像素數據。
      • 每像素擁有16字節,原始位訪問(Raw bit access)。
    • 支持GLES3.2,Vulkan 1.0,CL 1.2,DX 11.2。

    • 4x、8x、16x的MSAA。

    Arm Mali T880 GPU硬件架構示意圖及其功能描述。

    對于Mali GPU,驅動程序通過Job Manager(作業管理器)提交繪制任務,由Job Manager向GPU的繪制硬件創建并提交任務,它們通過內部連接元件交互。

    應用程序、驅動程序、GPU、DPU等各個層級的交互簡化后的示意圖如下:

    應用程序、驅動程序、GPU等交互示意圖。其中eglSwapBuffers表示幀結束,App會屬性繪制命令給驅動程序,驅動程序會編排任務給GPU,GPU繪制完成之后提交結果給DPU。注意它們各個層級之間存在著延時。

    首先考察應用層和驅動層的交互。應用程序在調用圖形API(如OpenGL ES)時,驅動程序會創建對應的資源架構圖:

    GPU內部存在以下幾種Job(作業)類型:

    作業名稱 縮寫 描述
    Vertex Job V 執行一組頂點的頂點著色器。
    Tiler Job T Tiling Unit(分塊單元,固定功能)分拆轉換后的圖元到覆蓋的分塊。
    Fragment Job F 運行在所有Tile的單一渲染目標的工作。
    Job Chain - 作業鏈。

    以下是GPU作業鏈的其中一種情形:

    作業鏈示意圖。其中作業之間存在依賴關系(箭頭所示),只有前序任務完成了,才行執行下一個作業。

    CPU、GPU的交互示意圖如下,其中CPU通過APB提交任務給GPU,GPU內的Job Manager通過AXI存取共享內存,而CPU也可以通過AXI存取共享內存。

    GPU內的Job Manager創建和分配任務示意圖如下:

    Job Mananger運行示意圖。圖中分配了3個頂點作業、1個分塊作業和2個著色作業。其中分塊作業依賴于頂點作業。

    對于Shader Core而言,Mali的結構是Tripipe,是統一著色器架構,可以執行VS或PS:

    更進一步地,頂點作業運行示意圖如下。頂點線程不會寫入tile緩沖區,但會直接訪問主內存,頂點任務包含了4n個頂點。

    片元作業示意圖如下:

    Fragment Work分為Front-End、Tripipe、Back-End三個階段。成功經過光柵化、Early-Z、FPK的像素會由Fragment Thread Creator創建線程(以Quad為單位,即2x2個線程),進入Tripipe著色,然后進入Late-Z、混合,最后寫入Tile內存。

    但是,不是所有移動端GPU的運行機制都跟Mali的一模一樣,比如PowerVR的就會諸多不同點:

    PowerVR Series 7XT架構示意圖。

    PowerVR Series 7XT統一著色器簇組架構圖。

    更多PowerVR的介紹可參見:

    12.5.3 并行、卡頓和延時

    隨著摩爾定律的放緩,現代移動端的SoC朝著多核高并行的方向發展,應用程序能否利用多核性能提升并行效率,很大程序上決定了它的品質和用戶體驗。

    和并行效率相反,卡頓和延時是實時應用(如游戲)的天敵。卡頓意味著幀率低,應用程序運行不夠流暢;延時則意味著操作不能及時響應,降低產品的用戶體驗,甚至會導致用戶嚴重流失。

    無論是在PC端還是移動端,渲染管線需要處理的場景越來越復雜,加上多線程等特性,因此或多或少存在著等待、卡頓(Stall)等現象,由此導致了延時(Latency)。這種現象在TB(D)R盛行的移動端渲染管線中尤為明顯。

    造成卡頓和延時的原因有客觀和主觀。客觀的原因指多線程的協同等待、同步,驅動程序的優化,GPU內部執行機制的良性優化等。而主觀方面是指那些沒有使用符合特定渲染機制的接口、標記、狀態或資源,這類是可以避免和優化的。

    UE存在游戲線程、渲染線程、RHI線程,后面的線程通常會比前面的線程延時一些,它們之間還存在同步和等待,防止前面的線程領先太多時間。

    應用程序、驅動程序、GPU、顯示器之間的延時示意圖,下層會比上層落后一段時間。

    OpenGL的glFinish和glFlush執行示意圖。其中glFlush調用之后,不一定會立即刷新渲染指令到GPU,只有當驅動器的渲染命令緩沖區滿了才會,因此也可能導致延時。

    移動端GPU的TB(D)R較普通的做法是將Binning Pass和Rendering Pass放在不同的幀處理,以提升并行效率,但也會導致延時:

    TBR架構中的Binning、Rendering錯幀處理示意圖。

    以上是完美錯幀處理的情況,如果有以下情形之一,則會打亂TBR的執行節奏,導致更嚴重的Stall和延時:

    • Binning依賴上一幀的數據或資源。

      n+1幀的binning需要依賴n幀的Rendering渲染結果,所以不能和n幀的Rendering Pass并行處理,只能延時到下一幀。

      這種情況可以通過延時使用解決,比如N幀的binning使用N-1幀的Rendering結果。下圖是實時環境立體圖的優化案例:

    • 在提交之后、渲染使用之前,要修改數據。例如:

      • 像素著色器計算并寫入數據到一個幀緩沖對象,使用結果生成位移。
      • 從CPU寫入紋理,使用它進行渲染,然后再次更新紋理,然后渲染下一幀;像素著色器在紋理更新完成之前不會執行。

    Vulkan等現代圖形API存在Subpass機制,Subpass可以并行處理(Overlap),也可以指定數據依賴:

    上:subpass的overlap機制i;下:subpass內部和之間的數據依賴。

    使用Vulkan、Metal、DX12等現代圖形API可以精確指定渲染管線屏障(Barrier)的等待階段,例如下圖使用了默認的PipelineBarrier,會導致Vertex、Fragment處理存在較多的空閑或等待,浪費GPU時間周期:

    通過修改屏障需要等待的源階段和目標階段,可以緩解這類Stall,提升著色器單元的利用率:

    Pipeline Barrier的具體優化示例如下:

    利用Vulkan的Pipeline Barrier優化各個Pass之間的等待階段,可以減少Stall和延時。圖中從28ms下降到22ms。

    PowerVR的TBDR架構,會在本幀所有圖元處理完Binning數據,才開始渲染階段,這也許會導致更嚴重的延時,若是開啟了Alpha Test,深度會寫回HSR階段,從而打斷HSR的正常流程,導致意外的Stall。

    現代GPU(包含移動端)普遍都是統一著色器架構,這種架構的特點是所有Shader Core既可以執行VS又可以執行PS,未來還可能統一GS、MS等等,使得GPU可以統籌和均衡安排VS或PS密集型任務,讓所有Core都盡可能處于滿載狀態,從而減少等待、空閑和延時:

    左:分離式著色器處理單元,右:統一著色器處理單元。可見后者的處理器基本處于滿負荷運行,從而減少等待和空載,提升整體運算能力。

    現代GPU已經將SIMD和SIMT技術運用得爐火純青,以提升整體的并行效率和吞吐量:

    但是,如果GPU指令組之間存在數據依賴關系,則會拉長指令組之間的執行時間:

    左:GPU指令組正常執行,沒有等待的情況;右:GPU指令組被加入了氣泡(bubble),導致了延時。

    上圖氣泡(bubble)的產生是為了解決GPU指令組之間的數據依賴:

    左:下組指令依賴上組數據的寫入,如果不處理就會獲得舊的數據;右:在下組指令插入氣泡,延遲一個時鐘周期以保證獲取最新的數據。

    Shader中的iffor等動態分支循環語句會降低GPU計算單元利用率,拉長它們運行指令的時間:

    訪問內存的指令也會使GPU計算單元產生Stall,延長計算時間:

    不同于CPU的低延時低吞吐率,GPU天生為了高并行和高吞吐率而設計,但與此同時緩存容量小,Cache命中率低,延時較高:

    因此,如果GPU數據結構設計得不好,會極大降低Cache命中率,從而增加計算單元的卡頓和延時。GPU的線程編排器(Thread Schdule)通常會考慮數據關聯性,保持同個線程組的線程在同一個緩存行:

    尺寸為16的Wave運行示意圖。其中虛線表示線程組之間不能跨界存取數據,以提高線程組內部訪問數據的緩存命中率。

    除上述情況之外,如果系統或應用程序使用了雙緩沖、三緩沖、垂直同步等機制,也會引入一定的延時。

    三緩沖執行機制示意圖。

    Android系統渲染模塊使用了多層級封裝和三緩沖機制,使得畫面總是延遲3幀:

    相反,善用Async Compute、Copy Engine、Graphic Pipeline等部位的并行機制,利用RDG的自動處理資源分配和依賴,利用子資源(subresource)和別名資源(aliasied resource)的特性,合并屏障等操作,可以減少Pass之間、Pass內部的等待和延時,提升并行效率。

    Async Compute、Copy Engine、Graphic Pipeline的并行運行案例。

    別名資源運行機制示意圖,其中資源A和D分別在不同時間段占用了同一塊內存區域。在使用RDG時,別名資源可以節省超過50%的已使用資源分配空間,即便它們會給渲染系統添加額外的資源管理復雜性。

    總之,從CPU的App層的邏輯更新、渲染指令生成、圖形API的調用和提交,橫跨驅動層、系統層、GPU內部,到最終的顯示器呈現,都可能存在各種各樣的依賴、等待、卡頓和延時等問題。這就要求我們統攬全局,甄別整條渲染管線的瓶頸,對癥下藥,才能使我們的程序高效、流暢、即時地運行在移動端SoC上。

    UE4官方文檔針對延時給了一些建議和優化措施,詳見Low Latency Frame Syncing

     

    • 未完待續

     

    團隊招員

    博主所在的團隊正在用UE4開發一種全新的沉浸式體驗的產品,急需各路賢士加入,共謀宏圖大業。目前急招以下職位:

    • UE邏輯開發。
    • UE引擎程序。
    • UE圖形渲染。
    • TA(技術向、美術向)。

    要求:

    • 扎實的技術基礎。
    • 高度的技術熱情。
    • 良好的自驅力。
    • 良好的溝通協作能力。
    • 有UE使用經驗或移動端開發經驗更佳。

    有意向或想了解更多的請添加博主微信:81079389(注明博客園求職),或者發簡歷到博主郵箱:81079389#qq.com(#換成@)。

    靜待各路英雄豪杰相會。

     

    特別說明

    • 感謝所有參考文獻的作者,部分圖片來自參考文獻和網絡,侵刪。
    • 本系列文章為筆者原創,只發表在博客園上,歡迎分享本文鏈接,但未經同意,不允許轉載
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目
    • 系列文章,未完待續,完整目錄請戳內容綱目

     

    參考文獻

    posted @ 2021-11-12 22:46  0向往0  閱讀(3430)  評論(2編輯  收藏  舉報
    国产美女a做受大片观看