著色器 PLS 行動繪圖 Mali GPU 多重採樣抗鋸齒技術 MRT

著色器核心玩轉行動繪圖系統 PLS擴充技術渲染立體光影場景

從2014年到現在,發生許多讓行動繪圖產業產生重大變革的事件,特別是在2016年2月,推出了第一版的Vulkan。在OpenGL服務業界長達20多年之後,從頭打造的Vulkan就是為了取代OpenGL,成為主要的繪圖處理API。而這一新型的繪圖處理API,可望為多個繪圖處理社群重視且認可的多種平台,提供一系列的好處。

一如預期,從OpenGL轉移到Vulkan花了數年的時間。就算目前主要的遊戲引擎預設的API是Vulkan,這些遊戲引擎仍全數支援Android系統的OpenGL ES 3.x。OpenGL ES 3.1將運算力導入行動繪圖,而OpenGL ES 3.2則新增了Android擴充套件(Android Extension Pack),大幅拉近桌上型電腦版本的OpenGL與行動API之間在功能性的落差。Android 6.0或更高的版本可以支援OpenGL ES 3.2(前提是裝置本身可支援此繪圖管線),而這也反映在遊戲開發人員大量使用OpenGL ES的情況。

本文要以今日的視角探討像素本地儲存(Pixel Local Storage, PLS)擴充技術,同時也從當初推出時的角度來審視。將強調它能提供的主要效益、讓開發人員在使用OpenGL ES編寫遊戲程式碼時,可以發揮最大效果。接下來也將介紹一些具有代表性的實例,並提供相關刊物與專題報告的連結。

為何需要像素本地儲存?

節省功耗一直是手遊開發人員心之所繫的考量點。降低遊戲過程使用的功耗,可以節省電池電力的消耗,並讓遊戲體驗更為持久。對於使用OpenGL ES來實作遊戲的開發人員來說,節省功耗的效益是PLS為何如此重要的主要原因。要了解這部分,首先應該了解PLS如何運作。

PLS發揮Mali GPU圖塊架構的優點。基於圖塊的渲染(Tile-Based Rendering)概念,讓Mali GPU得以維持較低的渲染功耗。Mali GPU把螢幕分割成16×16或32×32像素的小區塊,並把它稱為圖塊(Tile)。圖1解釋渲染分為兩個處理階段進行。第一個處理階段建構落入每個圖塊的幾何圖元(Primitives)清單。進行第二個處理階段時,每個著色器核心會逐一地為圖塊執行片段著色,並在圖塊完成後把它們寫回記憶體。

圖1 基於圖塊的渲染資料流

著色過程中由於圖塊的尺寸很小,因此可以將整套的作業資料(色彩、深度與模板)儲存在著色器核心中的內建RAM。RAM的速度很快,並與GPU的著色器核心緊密耦合。如此可以節省寶貴的頻寬,因此節省功耗。

原本Mali GPU的逐像素內建記憶體,通常是用來進行多重採樣抗鋸齒技術(Multisampling Anti-Aliasing)。PLS是一種API的擴充技術,可以把此一記憶體公開給程式設計師,讓他們可以讀/寫自己的逐像素資料。此一資料在Draw Call之間進行保存,並且只要幀緩衝保持有效,它就有效。這種持續的逐像素儲存能力是PLS之所以獨特的關鍵特性,並改變了圖塊架構的GPU繪製圖形的方式。

有了PLS,就可能串連渲染任務,既不清除記憶體,同時大幅降低頻寬的消耗。通常會使用這個記憶體逐步利用多種著色器建立起最後的像素色彩值,最後才使用最終解析著色器,複製到幀緩衝器輸出。

程式設計師對持續的逐像素儲存器的檢視方式是相當有彈性的。PLS允許每個著色器把其PLS的逐像素「檢視」宣告為結構。這樣可以讓開發人員重新解譯資料,並在兩種著色器間改變檢視方式。PLS的逐像素檢視,完全不受當前的幀緩衝格式影響。這意謂最後回沖至主記憶體的物件,仍將遵循當前的幀緩衝格式。

圖2是PLS著色器檢視的實例。左側的布局限定符(Layout Qualifier)是用來具體指定個別PLS變數的資料格式,格式的大小都是32位元。在中間指定的精度與類型,則描述用來讀/寫到這些變數的著色器的類型。從著色器進行讀與寫時,這種類型與布局格式之間,將有個隱含的轉換。

圖2 PLS結構

在PLS之前,GLES 3.0仰賴多重渲染目標(MRT)來執行複雜的渲染任務。它可以讓著色器的輸出在單一渲染處理階段中,完成寫入多個的紋理。這些紋理隨後可以當成其它著色器的輸入。MRT在OpenGL中常被用來進行延遲渲染(Deferred Rendering),它可以立即執行整個3D場景的光照運算,而非像正向渲染(Forward Rendering)一般對每個個別的物件進行運算。

延遲渲染的基本概念是,多數較為繁重的渲染作業(也就是光照),會延遲或延後到較後面的階段。這種方法大幅最佳化具有大量光線的場景,並且通常用在電玩主機上。在第一個處理階段(或稱為幾何處理階段)中,會對場景進行一次渲染,以便從物件擷取幾何資訊,然後儲存在名為幾何緩衝(G-buffer)的紋理集合中。 MRT則用來把光照運算的資訊,儲存在多個渲染目標內。在整個場景繪圖完成後,這些資訊會用來運算最後打上光影的影像。通常,一個渲染目標會儲存物件的色彩與表面資訊,而另一個則會儲存表面法線與深度資訊。更多的渲染目標,可以用來儲存如環境光遮蔽等資料。

MRT在行動裝置上碰到的問題是,每個渲染目標都會寫入主記憶體,然後再讀出來以便擷取儲存的資訊。儘管MRT結合幀緩衝提取(Framebuffer Fetch)可能是個替代方案,但PLS還是有些額外的好處,包括:更少的效能陷阱;由於資料格式完全不受色彩附件格式的影響,儲存上更具彈性;以及提供一個更接近著色器程式設計師想要表達的程式設計模型。

基於PLS的行動裝置延遲渲染

使用PLS擴充技術公開的內建記憶體,可在行動裝置上有效率地實作延遲渲染。Mali-T760之後的版本的設計都能提供支援,且每個像素可以允許16bytes的本地儲存。

圖3總結如何分成三個處理階段使用PLS來實作延遲渲染。Jan-Harald Fredriksen的〈Pixel Local Storage on ARM Mali GPUs〉,以及Marius Bjorge針對GPU Pro 5發表的詳盡文章(1)(兩人都是PLS擴充技術原始創作者之一),對於此一實作提供了詳細的解釋。而Hans-Kristian Arntzen較近期發表的文章〈Deferred shading on mobile:An API overview〉,則分享了在行動裝置上進行延遲渲染的綜合分析,並且比較了PLS、幀緩衝提取與Vulkan多處理階段的實作。請留意,在Vulkan的多處理階段概念中,無法直接存取圖塊儲存器。反之,會先向驅動程式提供足夠的資訊,使其足以優化一個渲染的處理階段,以便讓目前的處理階段存取前一個處理階段在同一地點儲存的像素資料,就跟PLS一樣。 在幾何緩衝處理階段中,片段著色器(Fragment Shader)會宣告一個PLS輸出區塊,這個區塊然後會被此一著色器以場景的色彩與法線資訊填滿。請留意,PLS輸出區塊使用的總記憶體為128位元(32位元×4)。

圖3 使用PLS技術的延遲渲染處理階段

擴充限定符(Extension Qualifier)__pixel_local_outEXT意謂儲存器可以寫入,而對於涵蓋同一像素的著色器調用,都是一致的。之後儲存器在另一個宣告__pixel_localEXT或__pixel_local_inEXT為儲存器的著色器調用中被讀取(圖4)。

圖4 在幾何緩衝處理階段中,片段著色器會宣告一個PLS輸出區塊,這個區塊然後會被此一著色器以場景的色彩與法線資訊填滿

在第二個處理階段中,同樣的PLS會被宣告,但這次會使用__pixel_localEXT限定符(Qualifier)來表明儲存器可以被讀取與寫入。著色器會讀取之前處理階段儲存的色彩與法線資料,然後寫入運算後的光照。此時,PLS區塊將包含色彩、法線與累計的光照資訊(圖5)。 

圖5 在幾何緩衝處理第二個處理階段中,著色器會讀取之前處理階段儲存的色彩與法線資料,然後寫入運算後的光照

第三個處理階段只會從PLS區塊讀取光照資訊,並計算出最終的像素色彩值。一旦圖塊充分處理完成,PLS就會被自動丟棄,因此不會對外部的記憶體頻寬帶來任何衝擊。唯一由外部記憶體處理的資料是明確複製到「外」變數的資料,此時PLS資料就會變成無效。

在2014年的遊戲開發人員大會中,Marius Bjorge與Sam Martin針對「手機遊戲繪圖的革命」,向遊戲開發人員社群發表專題演說。如同圖6顯示,他們分享了關於PLS對MRT頻寬消耗的很有趣的比較。我們看到PLS方式與MRT方式相比,消耗的頻寬大約只有1/8,差距可以說相當之大。更少的頻寬消耗意謂更低的功耗,以及遊戲時間更長的電池續航力。如果將發熱情況的改善納入整體考量,消耗頻寬的降低對節省功耗的影響可能會更大。

圖6 PLS對MRT在延遲渲染中的頻寬消耗

更少的頻寬消耗同時也意謂更高的效能,因為花在資料傳輸的時間會更少。根據在GitHub上分享、並專注於內建記憶體管理的一份基準研究顯示,把資料留在內建記憶體可以提升40%的效能。

半透明

接下來,將要更深入地研究透過PLS可以達成的先進著色技術。自從PLS的發展於2013年美國計算機圖形學頂級年度會議(SIGGRAPH 2013)中公開與大眾分享的那一刻起,就開始突顯它對實作先進渲染,以及對在行動裝置上進行後處理效果的重要性。在那之前,類似半透明的效果需要好幾個處理階段,結果造成記憶體刷新以及從主記憶體回讀資料。對於在行動裝置上運用許多複雜的渲染技術,頻寬消耗當時是個實際的阻礙。

現在就來看看如何使用PLS實作如此先進的效果。半透明是當光線輕拂某個物質產生的效果,它介於完全不透明與完全透明之間。蠟燭裡的蠟與樹葉,是半透明物質的常見實例。為了逼真地渲染這些物質,需要考慮次表面散射(Subsurface Scattering, SSS)技術。這是一種光線傳送的機制,可以解釋穿透一個半透明物件表面的光線,如何靠與該物質進行互動並在不同的點離開表面,產生散射情況。比方說,當光線通過耳朵時,都能輕易地認識出這種效果。

在行動裝置上進行粗略的SSS實作,必須考慮到當光線通過一個物質時,它會如何衰減。衰減會受好幾個因素影響,包括物件不同的厚度、觀看的方向,以及光線的特性。實際上,人們必須在光線達到渲染點前,判定它已在物件裡行進了多遠。運算過程會從相機(t)觀察到的物件厚度,而非光線(s)實際行進的距離,是個頗為公允的近似值(圖7)。

圖7 人們必須在光線達到渲染點前,判定它已在物件裡行進了多遠,運算過程會從相機(t)觀察到的物件厚度,而非光線(s)實際行進的距離,是個頗為公允的近似值

若依照這種方式,以視空間(View-Space)內物件最大與最小深度間的差異,當成估算到的厚度。這將分成兩個處理階段進行。第一個處理階段將決定視空間內最靠近的物件,以及它的最小深度。藉由在模板緩衝器中分別寫入0與>=1的ID值,以區分不透明與半透明的物件,以配置以下PLS區塊。請留意整個結構的總體規模為128位元(圖8)。

圖8 行動裝置上進行粗略的SSS實作,第一個處理階段將決定視空間內最靠近的物件,以及它的最小深度

在第一個處理階段中,片段著色器寫入物質的特性,並把最小與最大的深度都設置為進入深度。由於光照變數在第二個處理階段中將用來累計光照,因此先予以清除(圖9)。

圖9 行動裝置上進行粗略的SSS實作,在第一個處理階段中,片段著色器寫入物質的特性,並把最小與最大的深度都設置為進入深度

在第二個處理階段中,使用同樣的PLS區塊,而著色器則找到之前判定最靠近物件的最大深度。這一次場景(參考圖3)沒有進行深度測試就進行渲染,但卻有進行設置目的要讓每個物件的ID平等的模板測試。由於最靠近物件的ID儲存在模板緩衝器內,只有那個同樣的物件會有通過的片段(圖10)。

圖10 行動裝置上進行粗略的SSS實作,在第二個處理階段中,使用同樣的PLS區塊,而著色器會找到之前判定最靠近物件的最大深度

圖11中的左圖,顯示半透明物件最後合成厚度的渲染。一旦運算出視空間內物質的特性與物件的厚度,並儲存在PLS區塊中,就能對所有半透明的幾何體執行最終的著色處理階段(圖11中間的圖)。此一處理階段的基本概念是,厚度會衰減光線的透射力。厚度越大,光線透射力的強度則越小。對於厚度極薄的物件,應該會看到很高的強度。在Arm OpenGL ES SDK for Android可以找到這個處理階段的詳細解釋,以及開放原始碼。

圖11 圖片顯示三種運算後的實例

在此一半透明實例中,兩道光線朝著相反的方向移動,但都會穿透立方體。現在可以透過改變一些參數來更改這個半透明實例,並看到其產生的結果(圖11中的右圖)。

無關順序的半透明渲染

通常,針對所有具備若干透明度的幾何體,都會使用alpha合成進行渲染。每個半透明的幾何體會遮蔽在它前面的幾何體,並依據它的alpha值增加本身的色彩。幾何體混合的順序有相關的意義。例如,Arm Mali GPU最佳實務指引推薦首先以由前到後的渲染順序來渲染所有不透明的網格(Mesh),隨後為了確保混合可以正確運作,需要在不透明的幾何體上面,以由後到前的渲染順序渲染所有透明的網格。

依據半透明幾何體的數量與複雜程度,定序可能會花很多的時間,而且並不一定會產出正確的結果。因此也有人實作像是稱為無關順序的半透明渲染(Order Independent Transparency, OIT)替代方式。OIT在光柵化之後,會逐像素地對幾何體進行排序。要準確估算出最後色彩的精確解決方案,需要將所有的片段進行排序,而這對於複雜場景可能構成瓶頸。對行動裝置更友善的近似解決方案,則在品質、效能與記憶體頻寬間取得平衡,其中包括多圖層alpha混合(Multi-Layer Alpha Blending, MLAB)、適應性透明(Adaptive Transparency)與深度剝離(Depth Peeling)等技術。

MLAB是種在單一渲染處理階段中,於有限記憶體情況下運作的即時近似OIT解決方案。換句話說,它與PLS能完美搭配。它在運作時會把光線透射率、深度及累計的色彩,儲存在固定數量的圖層中。在可能的情況下,新的片段會依順序插入,且必要時會進行合併。如此一來,alpha混合中嚴格的定序需求,可以略為放鬆。針對一共只有兩個圖層的最單純情況,混合的圖層可以在PLS結構中進行累計,並且最終把它們解析成單一的輸出色彩。2014年SIGGRAPH會議中有一場簡報比較了OIT的不同方式,並建議MLAB的PLS實作在色彩與alpha方面使用RGBA8,深度則使用32位元浮點(表1)。

深度值是用來判定第一個片段的0是在第二個片段1的前面或後面。然後會相對地套用上/下運算子。以圖12的虛擬碼顯示作業會在著色器內實作。圖13顯示使用這裡描述針對雙層次的MLAB方式產出的合成影像,以及參考影像。儘管這是最單純的MLAB方式,並且能針對常見的alpha混合進行改良,但在左下球面還是觀察到明顯的假影。

圖12 虛擬碼顯示作業會在著色器內實作
圖13 針對雙層次的MLAB方式產出的合成影像以及參考影像

著色器幀緩衝提取(Frame Buffer Fetch, FBF)

在本文最後,筆者想強調將Mali快速的內建記憶體公開給開發人員的另外兩個擴充技術,它們也因此提供可顯著節省頻寬的選項。第一個擴充技術是Arm幀緩衝提取,它讓片段著色器可以讀取之前片段的色彩。它可以讓片段著色器將現有的幀緩衝資料讀取成輸入資料,並與實作像是可程式化混合,以及用固定管線功能混合不可能實作的其它所有作業等使用場景,都具有相關性。此擴充技術在OpenGL ES 2.0版或更新的版本都得到支援,並提供單一幀緩衝讀回,但與EXT_shader_framebuffer_fetch相比,它具有效率更高的近似MSAA路徑。

由於這種擴充技術透過全新內建的gl_LastFragColorARM(mediump vec4)來運作,因此使用起來很直接。圖14為實例展現可程式化的混合。

圖14 以幀緩衝提取運算進行可程式化的混合

第二個擴充技術是Arm幀緩衝提取深度與模板(Arm Frame Buffer Fetch Depth and Stencil),它可以讓片段著色器從幀緩衝讀取目前的深度與模板值。
使用起來透過兩個全新唯讀的內建變數,也相當直接:供存取模板緩衝的lowp int gl_LastFragStencilARM,以及供存取深度緩衝的gl_LastFragDepthARM。以深度來說,精度取決於GL_FRAGMENT_PRECISION_HIGH完成定義(Highp Float)與否(Mediump Float)。這些變數分別包含像素的深度與模板緩衝的現行值,而現行的片段也將送至該像素。這種擴充技術可以促成的使用場景,包括像是可程式化的深度與模板測試、調變陰影與軟粒子,以及在單一渲染處理階段建構變異數陰影貼圖的能力。這種擴充技術同時也提供應用項目一種非常方便的方法,可以利用深度與相機矩陣重建螢幕中任何像素的3D位置。

圖15的實例(2)展現使用深度讀回來渲染軟粒子。正常來說,這樣會需要兩個渲染處理階段:第一個階段把背景幾何體的深度值寫出到深度紋理;第二個階段會一邊渲染粒子,同時從深度紋理讀取資料以進行混合。此擴充技術可以讓這一切在單一的處理階段中全部完成。 

圖15 擴充技術可以讓片段著色器從幀緩衝讀取目前的深度與模板值

圖15擴充技術的規格提到,若要從gl_LastFragDepthARM與gl_LastFragStencilARM進行讀取,必須等到要送至現行像素的所有之前片段都處理完成才行。為了達成最佳效能,它也因此建議從這兩個內建的變數之一進行讀取時,要盡可能在片段著色器執行的最後階段再進行。

PLS與FBF擴充技術注意事項

如同以上所述,PLS與FBF擴充技術可以為OpenGL ES開發人員帶來不少好處。然而,準備使用這些擴充技術時,仍有必須留意的一些事項。首先是並非所有的GPU廠商都支援PLS。事實上,Arm的Mali GPU便因對此種擴充技術提供支援,是最受市場歡迎的GPU。

對於想要以單一編碼基底鎖定所有Android裝置的開發人員,這種只有部分廠商支援的情況,讓PLS的採用顯得較為困難。這種挑戰也是為何繪圖社群開始尋找一種所有廠商都支援的API,而這也把大家導向Vulkan。Vulkan的渲染處理階段提供一個表達類似於PLS語意的機會,但卻可以用跨平台的方式進行,儘管它在形式上較為受限,且較為含蓄。在Vulkan環境中,當一個渲染處理階段包含多個次處理階段(Subpass),並利用次處理階段的負載功能性在不同處理階段間傳送逐像素資料,GPU會擁有足夠的資訊,可以把這些資料留在內建的儲存器裡,並避免因為儲存/載入來回往返記憶體。這點稱為「次處理階段融合」(Subpass Fusion),而所有的Mali Vulkan驅動程式都提供支援。不過,GPU使用DRAM或是內建儲存器,並非明確地由應用程式控制,因此這種特性在不同廠商之間不盡相同。儘管如此,就目前的情況來看,在Vulkan的環境裡無法像在OpenGL ES擴充技術一樣,直接存取圖塊儲存器。

此外,OpenGL ES與Vulkan使用內建記憶體的前提是每個像素只能使用來自相對應的輸入像素資訊。兩者都不允許開發人員從任意的像素位置進行取樣,更不用說是之前的內容。這也為在行動裝置實作高度優化的新效果,敞開了大門。

最後,儘管所有的繪圖API設計用意都是要儘量發揮底層硬體的潛力,但它們並不保證可以實作高效能、低功耗的行動App。要達到上述目標,沒有捷徑。唯有耐心地進行系統層級分析作業,才能得知GPU是否停頓,或是輸入端不夠快而閒置,而抵銷了圖塊記憶體擴充技術的優勢。這是最終指引瓶頸在哪裡的東西,並協助使用者做出以理想的方式使用Runtime資源的決定。針對Arm Mali GPU,Arm Mobile Studio工具套件提供數個系統層級分析與監控工具的取用,例如簡化效能分析器(Streamline Performance Analyzer)、效能計數器(Performance Counters)、效能顧問(Performance Advisor )與Mali離線編譯器(Mali Offline Compiler)。這些資源結合下列的Mali GPU最佳實務,有助於繪圖開發人員用以發揮Arm Mali GPU最大潛力的工具。

擴充技術有效降低功耗

閱覽本文的OpenGL ES開發人員,對於如何使用PLS與FBF擴充技術在行動裝置上實作高效率的繪圖作業,已經有了想法。不過,在閱讀這些不同實例時,更重要的是要瞭解PLS之後的概念。PLS是繪圖界首次把內建記憶體資源公開給開發人員的技術,伴隨而來的是利用持續性的記憶體打造著色器管線的可能性。原本行動裝置不可能觸及的使用場景,瞬間成為可能,而這主要是受惠於頻寬的驟降。對於行動平台,功耗是珍貴的資源。由於允許使用內建記憶體來儲存與讀取資料,從主記憶體把資料搬來搬去浪費的功耗,會變得更低,這意謂電池的續航力會更長。最後,藉由把資料留在內建的記憶體中,可以大幅度降低資料傳送使用到的Runtime資源,並且提升效能。在規畫使用OpenGL ES的下一個應用項目時,請記住如果使用PLS,就可以免費享受所有的這些效益。

(本文作者為Arm首席軟體工程師,中文審稿者為Arm首席應用工程師胡岱勛)

本站使用cookie及相關技術分析來改善使用者體驗。瞭解更多

我知道了!