当我们谈游戏优化时,我们谈些什么
前言
過去幾年里,我經歷過大約幾十場面試,幾乎在每次面試的時候,面試官都會問提一個問題:“你在渲染性能優化方面有什么經驗?”這個時候我就會開始揣測面試官的意圖,試著去回憶他之前提的問題,看看面試官到底想聽什么樣的回答:往往這種嘗試最后都是失敗的,結果就是不知道從何說起。因為沒有具體的情境,最后只能說“整個渲染流程中很多地方都可能出現性能瓶頸,只能case by case的去看,找到項目的具體瓶頸,然后針對性地去解決。”幾乎所有聽到這個回答的面試官都會對我意味深長地一笑,不置可否:一旦看到這種笑容,我就知道糟了。之后的面試反饋中,很多人對我的評價就是“對渲染算法比較熟悉,但是在性能優化方面經驗欠缺”。
總得來說我覺得這不是一個好問題,因為太過寬泛而沒有針對性。我并不想泛泛地說“減少模型數量,減少/合并draw call,縮減貼圖尺寸,壓縮貼圖,使用LOD”,因為這就是所謂“正確但無用的話”:所有游戲不都是這么優化么?此外,對于一個項目來講,模型的面數,貼圖尺寸,LOD的級別這些信息往往是在DEMO階段就已經由TA主導確立的。對于引擎程序員來講,需要你提出優化方案的,通常是在項目的開發過程中產生的新瓶頸(當然你首先需要定位它)。但反過來,我的回答其實也一樣是“正確的廢話”:所有性能優化的流程不都是這樣嗎?
?
所以,當我們談論性能優化的時候,我們究竟在談些什么呢?
我試著理解了這個問題的意圖:如果我們換一種問法,比如“渲染常見的性能瓶頸有哪些?具體可能出現在什么樣的情景下?為什么這些情景會造成對應的性能瓶頸?”會不會是一個更好的問題?所以這篇文章,是在試著回答這個新的問題。不同于以往的文章,優化本身確實是一個比較寬泛的主題,所以本文的組織也相對比較松散,很多內容可能是我想到哪兒寫到哪兒。其中有些概念基于我對硬件的理解,如有錯誤之處,歡迎指正。
說說GPU的架構
核彈廠有一篇關于自家GPU架構和邏輯管線的非常好的文章[1],如果你想要對GPU的結構有一個比較完整系統的認識,請一定不要錯過這篇Life of a Triangle。比較可惜的是,這篇文章只更新到Maxwell這代架構,沒有較新的Pascal架構(GTX10x0系列)和Turing架構(RTX20x0)的技術細節。不過總體來說,現代GPU的設計架構已經趨于穩定,一般只是針對某些單元做優化,或者增加feature,所以文章中的大部分內容在這里仍然是有效的,這是文中的一張圖:
?
這張圖是基于數據的流向,對GPU的硬件單元進行了大致的劃分,實際上GPU中,最核心的部件可以被分成三大塊,我畫了圖來示意他們大致的協作模式:
?
通常來說,GPU會有三個比較重要的部分,分別是控制模塊,計算模塊(圖中的GPC)和輸出模塊(圖中的FBP)。通常來說,GPU架構的設計需要有可伸縮性,這樣通過增加/閹割計算和輸出模塊,就能夠產生性能不同的同架構產品(比如GTX1070和GTX1080的主要區別就在于GPC和FBP的數量),以滿足不同消費水平和應用場景的需求。
控制模塊
控制模塊負責接收和驗證(主要是Host和Front End)來自CPU的經過打包的PushBuffer(經過Driver翻譯的Command Buffer),然后讀取頂點索引(注意是Vertex Indices不是Vertex Attributes,主要由Primitive Distributor負責)分發到下游管線或者讀取Compute Grid的信息(主要由CWD負責,這部分是Compute Pipeline,不作展開)并向下游分發CTA。
Tips:計算管線和圖形管線共享大部分的芯片單元,只在分發控制的單元上各自獨享(PD和CWD)。許多較新的Desktop GPU允許圖形和計算管線并行執行,可以在一些SM壓力輕的圖形計算環節(比如Shadow Map繪制),利用Compute Shader去做一些SM壓力重的工作(比如后處理),讓各個硬件單元的負載更加平衡[2]。
計算模塊
計算模塊是GPU中最核心的部件,Shader的計算就發生在這里。早期的硬件設計上,我們會區分VS,PS等Shader類型,并設計專用的硬件單元去執行對應類型的Shader,但這樣的方法并不利于計算單元滿負荷運轉,所以現在所有的GPU設計都是通用計算單元,為所有Shader類型服務。在NV的顯卡里這個模塊全稱是Graphics Processing Cluster,通常一個GPU會有多個GPC,每個GPC包含一個光柵器(Raster)負責執行光柵化操作,若干個核心的計算模塊,稱之為Texture Process Cluster(TPC),關于TPC,我們進一步分解來看這張大圖[3]:
?
通常來說,一個TPC擁有:
(1)若干個用于貼圖采樣的紋理采樣單元(Texture Units)
(2)一個用于接收上游PD數據的Primitive Engine,PE作為一個固定單元,負責根據PD傳來的頂點索引去取相應的頂點屬性(Vertex Attribute Fetch),執行頂點屬性的插值,頂點剔除等操作
(3)一個負責Shader載入的模塊
(4)若干執行Shader運算的計算單元,也就是流處理器(Streaming Multi-Processor,SM,AMD叫CU)
TPC內最核心的部件就是SM,這里我們再進一步分解SM看這張大圖:
?
一個SM通常擁有一塊專用于緩存Shader指令的L1 Cache,若干線程資源調度器,一個寄存器池,一塊可被Compute Pipeline訪問的共享內存(Shared Memory),一塊專用于貼圖緩存的L1 Cache,若干浮點數運算核心(Core),若干超越函數的計算單元(SFU),若干讀寫單元(Load/Store)。
作為核心計算單元,GPU的設計思路和CPU有很大的不同,就我所知的體現在兩個方面:
(1)GPU擁有較弱的流程控制(Flow-Control)的能力
(2)GPU擁有更大的數據讀寫帶寬,并配合有更多樣的延遲隱藏技術
GPU的執行模型
要詳細解釋這兩點,我們就需要理解GPU的執行模型:GPU的設計是為了滿足大規模并行的計算,為此,它使用的是SIMD(Single Instruction Multiple Data)的執行模式,在內部,若干相同運算的輸入會被打包成一組并行執行,這個組就是GPU的最小執行單元,在NV叫做Warp,每32個thread為一組,在AMD叫做Wavefronts,每64個thread為一組。基于不同的shader階段,被打包執行的對象會有區別,比如VS里,就是32個頂點為一組,PS里,就是8個pixel quad(2*2像素塊)為一組。
那么GPU又如何處理分支呢?我們知道,CPU有一種經典的處理分支的方法,叫做分支預測[4]。CPU會根據一組數據之前的分支結果去預測下一次分支的走向,如果錯誤就會有額外的開銷。GPU沒有這么復雜的流程控制,它的流程控制基于一種叫做“active mask”的技術,簡單來說就是用一個bit mask去判斷當前32個thread的branch狀態,如果是0,則表示只需要執行false的branch,如果bit mask是2^32-1,則表示只需要執行true的branch,否則就需要對某些thread執行true,同時另一些在執行true的同時等待這些thread,反之亦然,這種情形下一個warp的執行時間就是Max(branch(true))+Max(branch(false))。
GPU的內存類型
Desktop GPU的內存類型和CPU比較相似,也是多級緩存的機制,我們能夠接觸到的內存類型包括Register,Shared Memory(本質是L1 Cache的一塊),Texture L1 Cache(本質是L1 Cache的一塊),Instruction Cache(本質是L1 Cache的一塊),L2 Cache,DRAM,各類存儲器的容量在是依次增大的,相應的它們在芯片上的位置也是離核心單元SM越來越遠,同時訪存延遲也是逐級增大的。
關于Desktop的內存類型,包括更多的延遲隱藏技術是一個比較大的話題,這里無法再詳細展開,可以去參考其他文獻[5]。
對于GPU在這幾種內存中的訪存延遲,我從這篇文章[6]找到了一些數據:
?
對于PC端存儲器的速度,可以查看這個網站。
Mobile GPU沒有專用的顯存,而是和CPU共享同一塊系統內存(緩存機制當然也應該是共享的),但它有一塊位于GPU上的專用on chip memory,這里沒有找到Mobile GPU上的延遲數據,如果有相關數據請告訴我。
GPU擁有大量的寄存器(數量遠多于CPU),是為了能夠快速的在warp之間做切換:當某個warp被某些指令阻塞的時候(比如貼圖采樣),warp schedular可以讓其處于休眠狀態,并且把shader core的資源讓出來,喚醒那些未被阻塞的warp。對于CPU來說,context switch的開銷來源于寄存器的恢復和保存(沒那么多寄存器,只能復用),但是對于GPU,每個warp是獨占一份自己的register file的,這樣就可以幾乎無消耗地切換warp。相應的,一個SM里能同時并行多少個warp,就取決于一段shader到底占用了多少register,占用的越多,則能夠并行的warp就越少。
Tips:如果沒有記錯的話,一直到Turing之前的架構,同一個SM內都只能執行一個shader,新的Turing架構似乎是允許SM內執行不同shader。
這里補一張圖簡單說一下Turing架構它和上幾代顯卡在SM上的區別:
?
相較于上幾代的GPU,Turing在SM中增加了專用于光線追蹤的RTCore,以及用于張量計算的TensorCore(后者主要是用于深度學習。在Turing之后,你還可以在做Graphics的同時利用TensorCore去做一些DL的工作,比如DLSS[7]?好像沒什么x用)。下面兩張圖簡單解釋了RTCore前后光線追蹤的基本流程:
?
這個圖看起來很復雜,其實很簡單:對于非Turing架構來說,光線和BVH的遍歷求交、光線和三角形的求交、光線和三角形交點的著色這三件事,都是翻譯成了數千條SM的指令給FP Core執行的。而Turing架構則是把前兩件事作為固定硬件單元集成在了RTCore里,所以RTCore核心功能有兩個:遍歷BVH和光線-三角形快速求交。
輸出模塊
輸出模塊(Framebuffer Partition,FBP)比較簡單,最核心的部件是一個稱之為ROP(Raster Operation)的單元,ROP又包含了兩個子單元,分別是CROP(Color ROP)和ZROP,前者負責Alpha Blend,MSAA Resolve等操作,并把最終的顏色寫到color buffer上,后者則負責進行Stencil/Z Test以及把depth/stencil寫到z buffer上。
Pascal和Turing架構的補充
相較于Maxwell,Pascal架構在圖形方面的feature主要針對VR渲染(Lens Matched Shading,Single Pass Stereo等),因為當時恰好是VR概念大火的一年,具體的技術細節可以參考這篇文章[8]。而Turing架構在圖形方面最大的feature莫過于引入了實時光線追蹤,針對VR和可編程管線部分也有比較進一步的優化,具體可以參見這篇文章[9]。有關光線追蹤的技術,我在之前的兩篇文章[10][11]中有比較詳細的解釋。
在工藝制程和硬件參數方面,這篇文章[12]也給了我們一些參考數據:
?
如果你希望對這些參數包括各類顯卡的參數包括新特性有更深入的了解,也可以去試著讀一讀各代顯卡的技術白皮書[13][14]。如果這些細節還不足以滿足你對硬件的好奇心,那么強烈建議你去核彈廠工作。
Mobile GPU和Desktop GPU的差異
目前市面上主流的Mobile GPU生產商(Qualcomm,ARM,Imagination)的GPU架構都是由Desktop GPU發展而來,因此在硬件架構上同桌面級GPU差異不太大,值得一提是這幾點[15]:
(1)Mobile GPU的芯片面積和功率遠小于Desktop GPU,兩張圖:
?
(2)Mobile GPU位于SoC上,和CPU共享內存,SoC是整體供電的,沒有專用的電源輸出到GPU
(3)移動設備是被動式散熱,整個SoC的供電基于Thermal Throttling機制,當設備功率過高發熱過量時,電源會降低輸送功率防止SoC過熱,這意味著如果CPU負擔過大觸發這一機制,同樣也會使得GPU的性能下降
對于低端Mobile GPU,通常限制性能的是芯片面積,而對高端Mobile GPU,限制性能的則是帶寬和發熱。
回到渲染管線
回到我們一開始說過的那句“無用的廢話”:渲染管線的任何階段都有可能成為性能瓶頸。那么如果你試圖列舉大部分可能的性能瓶頸,就首先需要對整個GPU的渲染管線比較熟悉,并且能夠大致地知道GPU在渲染管線的每個階段,大致都做了哪些事,我們的每一個Graphics API Call在GPU端又對應著什么樣的行為?基于這樣的“翻譯”,我們才能夠理解那些性能瓶頸產生的原因,也能夠理解我們之前說到的那些“泛泛”的優化策略到底為什么能夠解決一些性能上的瓶頸。
說到渲染管線,手機靚號買號平臺就必須介紹當前GPU使用的三種不同的渲染管線:Immediate Mode Rendering,Tile Based Rendering和Tile Based Deferred Rendering[16]。我們用三張圖來詳細描述IMR,TBR和TBDR三種模式的渲染管線:
IMR模式的管線
IMR模式的第一個階段是Vertex Processing,這個環節包括從DRAM/System Memory取Vertex Indices(PD的工作),然后根據Vertex Indices去Vertex Buffer取相應的屬性(PE中VAF的工作),需要注意的是,取Indices/Vertex Attributes的階段都會有L2 Cache在工作,表示如果頂點短時間內被share多次,則可以通過cache命中減少加載時間。加載完頂點數據后,Vertex Shader將會被加載到SM的Instruction Cache,緊接著就是VS在SM的執行。
VS執行完畢后,PE內的固定單元會執行頂點剔除來剔除一些視口外的三角形,背面剔除也在這個階段發生。
接下來,由Raster對三角形進行光柵化,光柵化完畢的像素將會被打包成warp,經過XBAR重新流入SM(可能是同一個SM,也可能是不同的SM)。重新進入SM的每個pixel會根據其重心坐標,使用PE內的固定單元進行屬性插值,從而得到depth,varying attributes等信息。
對于沒有Apha Test的pixel quad,由ZROP對其執行early-Z test。
對于通過early-Z test的像素,在SM內執行pixel shader。
對于開啟Alpha Test的像素,由ZROP對其進行late-Z test,并根據結果決定是否更新FrameBuffer相應位置的顏色和深度值。
若需要更新,則ZROP根據depth test的設置更新z buffer,CROP根據blend的設置去更新color buffer。
注意,IMR的整個流程中,三角形是可以以Stream的形式逐步提交給管線的,先提交的三角形也不需要去等待同一個Render Target上的其他三角形。
有關IMR管線的描述,這篇slides[17]的描述比本文要詳細很多,非常建議仔細閱讀。
IMR是所有Desktop GPU的標配,因為Desktop GPU相較于Mobile GPU,有更多的帶寬用于讀寫,有專用供電接口,也不受限于芯片發熱的問題。IMR架構的好處是設計上會相對來說比較清晰簡明,并且整個管線是連續的,draw call之間不需要互相等待,有利于最大化吞吐量。對于Mobile GPU來說,只有NV的Tegra系列是基于IMR的。
TBR模式的管線
?
TBR架構的GPU會把整個邏輯渲染管線打斷成兩個階段:
第一階段和IMR類似,它負責頂點處理的工作,不同的是在每個三角形執行完他們的VS之后,還會執行一個稱之為Binning Pass[18]的階段,這個階段把framebuffer切分成若干個小塊(Tiles/Bins),根據每個三角形在framebuffer上的空間位置,把它的引用寫到受它影響的那些Tiles里面,同時由VS計算出來的用于光柵化和屬性插值的數據,則寫入另一個數組(我們可以認為圖中Primitive List就是我們說的一個固定長度數組,其長度依賴于framebuffer劃分出的tile的數量,數組的每個元素可以認為是一個linked list,存的是和當前tile相交的所有三角形的指針,而這個指針指向的數據,就是圖中的Vertex Data,里面有VS算出的pos和varying變量等數據)。在Bining Pass階段,Primitive List和Vertex Data的數據會被寫回到System Memory里。
Tips:TBR的管線會等待同一個framebuffer上所有的三角形的第一階段都完成后,才會進入到第二階段,這就表示,你應該盡可能的少切換framebuffer,讓同一個framebuffer的所有三角形全部繪制完畢再去切換
第二階段負責像素著色,這一階段將會以Tile為單位去執行(而非整個framebuffer),每次Raster會從Primitive List里面取出一個tile的三角形列表,然后根據列表對當前tile的所有三角形進行光柵化以及頂點屬性的插值。后面的階段TBR和IMR基本是一致的,唯一區別在于,由于Tile是比較小的,因此每個Tile的color buffer/depth buffer是存儲在一個on chip memory上,所以整個著色包括z test的過程,都是發生在on chip memory上,直到整個tile都處理完畢后,最終結果才會被寫回System Memory。
Tips:TBR的優化實際上是利用緩存的局部性原理。
TBDR模式的管線
?
TBDR和TBR模式基本類似,唯一的區別在于,TBDR模式在執行光柵化之后,不會急著shading,而是會對rasterized sample進行消隱(基于depth buffer和相同位置的其他sample深度去移除被遮擋的sample),這個消隱的過程結束之后,tile上剩下的sample才會被送到PS里面去做shading。
Tips:通常TBR/IMR模式的GPU是基于比較簡單的early-Z reject去防止overdraw,TBDR在這個方面則走得更遠一點。所以對于IMR/TBR模式的GPU來說,對不透明物體的draw call從前到后排序、Pre-Z pass都能夠顯著減少overdraw并提高性能;但對于TBDR模式的GPU來說,這兩個策略都不會提升性能(管線里面做了相同的事),而且還會影響因性能(排序、Pre-Z pass帶來的額外開銷)。
Tips:渲染管線中說的TBDR和我們在引擎的渲染管線中說的TBDR不是一回事,但是這兩者又有很大的關系。
關于這三種模式的區別以及演化,強烈建議配合演示動畫仔細閱讀這篇文章[18]。
什么情景會造成性能瓶頸?
Imagination有兩篇[19][20]關于自家PowerVR系列顯卡的性能優化建議,其中列舉了一些常見的性能優化場景。作為本文的Case Study部分,我會在這兩篇的基礎上結合前面硬件的原理,去解釋其中一些建議的原因。
幾何數據優化
減少頂點數量
這個優化簡單又直接,減少頂點意味著更少的頂點從System Memory/DRAM里讀取到Shader Core(帶寬壓力),同時意味著更少的VS執行(計算壓力),對于Mobile GPU,還意味著更快的Bining Pass(主要是帶寬壓力)。模型的減面、LOD包括normal map去代替高模體現細節都是同類優化。
減少每個頂點數據量
這個也比較直觀,數據量更少意味著VAF階段和Binning Pass階段更少的讀寫開銷。甚至有時候,我們可以在VS里使用一些快速的頂點數據壓縮/解碼方案[21][22](少量的計算開銷換取更少的帶寬開銷)。
避免小三角形
小三角形最直觀的缺點就是:在屏幕上占用的像素非常少,是一種視覺上的浪費。實際上,由于硬件管線中,針對三角形有圖元裝配的環節(Triangle Setup),還有三角形的剔除(Vertex Culling/Triangle Clipping),因此主要是“構造一個三角形的固定開銷”。文章[19]中還提到了一定要避免小于32像素的Triangle,我猜是因為小于32像素的三角形在PS階段,組的Warp可能是不足32pixel的(有待考證)。近幾年提出的GPU Driven Pipeline里面,已經有用Compute Shader去剔除小三角形的優化方法[23]。
優化索引緩沖
這是一個很少會有人提到的優化,原理是:VAF的階段,頂點的數據是根據PD派發的indices patch(長度是幾十個頂點索引)從顯存里面取的,indices patch相當于把一個長的index buffer切分成小段,在每個indices patch內,同一頂點被訪問越多次,memory cache的命中率越高,相應地,帶寬開銷就越小。所以我們可以通過重排index buffer,讓一段indices patch內同一頂點被引用的次數盡可能地多[24]。
Interleaving Attributes vs.Seperate Attributes
通常來說,如果一堆屬性在VS中始終是會被一起使用(比如skinned weight和skinned indices;Normal/Tangent),我們應該把它們放在一起以減少Graphics API bind的次數,如果一堆屬性在不同VS中使用頻率相差很大(比如position非常頻繁,但vertex color很少使用),那么我們應該存儲在不同buffer。這個原理和AoS/SoA的區別一樣,也是盡量提高緩存的利用率(緩存加載的時候的最小單位是Cache Line,通常64/128Bytes,所以要保證每次memory access能load更多有用的數據到cache)。
物件的優化
基于攝像機距離的排序/Z Pre-Pass
這個優化我們之前說過,對于有early-z機制的GPU(IMR/TBR)是有效的,對TBDR無效。
基于材質/RenderState的排序
RenderState是一個比較籠統的稱呼,對于OpenGL這類基于狀態機的Graphics API,像是buffer/texture綁定,framebuffer切換,shader切換,depth/stencil/culling mode/blend mode等都屬于狀態切換,并且有性能開銷。狀態切換中涉及的開銷包括driver端的命令驗證及生成;GPU內部硬件狀態機的重新配置;顯存的讀寫;CPU/GPU之間的同步等。這里有一張圖[25]大致量化了各類狀態切換的開銷:
?
至于每個graphics API call在調用的背后發生了什么,我沒有相關的知識去詳細解釋。這個主題也足夠一篇文章的內容去單獨闡述,如果有這方面的資料或者寫driver的朋友,希望可以解釋一下。
最理想狀況當然是把相同材質/RenderState的物體可以合并為一個batch提交,也就是我們常說的減少draw call[26]。
貼圖的優化
貼圖優化的核心只有一個:Cache Friendly。
減少貼圖尺寸
很多人都覺得減少貼圖尺寸帶來的最大優化是顯存,對于主機/移動平臺以及一些受顯存大小限制的場景或許是對的。但從性能角度分析,減少貼圖尺寸帶來的最大好處是提高緩存命中率:假設把一張1024*1024的貼圖換成一張1*1的貼圖,shader不變,你會發現shader的執行速度變快了,因為對所有需要采樣這個貼圖的shader來說,真正從內存讀取數據只需要一次,而后的每次采樣,都只需要從cache里取那個像素數據即可。換句話說,我們關注的是每條cache line能夠覆蓋多少個像素的PS執行。貼圖尺寸越小,每條cache line覆蓋的被采樣像素就越多。
使用壓縮貼圖
這個思路和頂點壓縮是類似的,即犧牲一些計算量用于即時的數據解壓縮,來換取更少的帶寬消耗。諸如DXT/PVRTC/ASTC都是這樣的思路。同樣的思路還可以用在緊湊的G-Buffer生成,比如CryEngine曾經用Best Fit Normal和YCbCr色彩空間壓縮G-Buffer[27][28]。
合并貼圖到Texture Altas
這個其實是為了減少貼圖的綁定開銷,本質上是減少狀態切換。如果有Bindless Texture[25]的情況下,這個優化就幫助不大。
使用Mipmap
通常我們使用Mipmap是為了防止uv變化比較快的地方(一般是遠處)的貼圖采樣出現閃爍,但究其根源,閃爍是因為我們在對相鄰像素進行著色的時候,采到的texel是不連續的。這其實就意味著cache miss。而Mipmaped texture會按級存儲每一層mip(物理內存上連續),這就意味著當你使用Mipmap去采樣的時候,緩存命中率是更高的,因此性能相比沒有Mipmap的貼圖也會更好。
存儲結果到Buffer還是Texture?
有時候我們會把一些通用計算放在GPU上,結果存在buffer/texture上,理論上,如果能夠選擇的話,盡量把不是圖片類型的數據存儲在buffer上(比如particle的velocity/pos或者skinPallete,最好用buffer存)。這聽起來是一句廢話,理由是:Buffer和Texture在內存中的存儲布局不一樣,Buffer是線性的,Textute是分塊的,在非貼圖數據的訪存模式下,分塊的內存布局往往不利于緩存命中。
Tips:當然,對于移動平臺來說,Cache Friendly還意味著更少的發熱。
Shader的優化
減少分支?
我們已經解釋過GPU是如何實現分支的,所以再回到是否要減少分支這件事,就不應該一味地認為分支總是對性能不好的。應該說,如果分支的結果依賴shader在運行時決定,并且這個結果在warp內差異很大,那么我們應該避免分支,實在無法避免時,盡量提取公共計算部分到分支外。近年來大部分的Deferred Shading框架,都依賴于Material ID去判斷材質類型,并在shading階段依賴動態分支去做不同的著色計算,這是因為材質在屏幕空間上的變化是比較少的(大部分使用標準PBR材質),所以分支帶來的性能問題也不大。
另外,我們經常會用一些Uber Shader來實現不同的材質效果(但又共享很多公共計算)。實現的思路有兩種:用宏定義基于Uber Shader生成不同的Sub Shader和Uber Shader內基于Uniform的分支。前者可能帶來的shader切換的開銷,后者反倒可能更有利于性能(當然,這個也要具體情況具體分析)。
精確指定數據類型
對于ALU來說,它的許多數學運算指令的時長/并發數是依賴于數據位寬的。因此應該盡量使用算法允許的最低精度數據類型來進行計算,比如GLSL中,可以通過highp/mediump/lowp去指定當前shader的數據計算精度。另外,在進行Int/Float的混合計算時,需要額外的指令對Int類型進行轉換,因此,如無必要,盡量不要用Int類數據。
使用向量算法還是標量算法?
過去很多Shader Core的設計是Vector Based,即ALU只會進行向量的加減乘除,對于標量也會規約到向量運算。基于這類設計,就有一些奇技淫巧,去把一個計算盡量向量化[17]。但現在更多的是Scalar Based的Shader Core,所以也就無需太過關注這點,但是,我們還是應該盡量延遲向量和標量之間的運算,比如這個例子:
?
用貼圖緩存中間計算結果?
很多時候,我們會把一些數學上的中間計算結果緩存到一張貼圖里,這些貼圖的數值本身不代表視覺信息,而是純粹的數字。比如Marschner Hair Mode用LUT去存BRDF[29];UE4用LUT去存儲PBR的環境光BRDF[30]。
LUT帶來的性能損耗有兩點:
(1)貼圖本身是數值,所以只能用無損格式,無法壓縮,所以bytes per pixel是比較大的,比一般貼圖占用更多讀取帶寬
(2)對于貼圖的采樣是基于LUT的uv計算的,而相鄰像素算出的uv通常都沒有空間連續性,這就表示每次LUT的采樣幾乎都會導致cache miss,所以這類LUT比一般貼圖的采樣更慢。
結論:盡量使用擬合函數去代替LUT采樣,對于Mobile GPU來說,永遠不要嘗試用LUT去優化一段shader;對于Desktop GPU來說,慎重考慮使用LUT。
結語
這可能是我寫過最累的一篇專欄文章,快寫完的時候發現其實這個主題應該拆成三篇來寫。優化涉及的內容細碎又繁瑣,概念之間彼此相互關聯,而且大部分知識,我只會記下出處和大概內容,要寫下來的時候,往往還需要重新查找引用并且確認細節。另一方面,這個主題相對來說比較硬核,硬件知識枯燥無味,比起圖形算法來說要無趣得多,也沒人愛看。以至于寫到最后,我都開始懷疑到底為什么要寫這個東西。
可能對我來說最大的意義,一是對過往雜亂的知識做一個自我梳理;二是告訴那些面試官,其實我也懂一點優化的(雖然只是嘴炮而已:))。
總結
以上是生活随笔為你收集整理的当我们谈游戏优化时,我们谈些什么的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Unity Gamma校正转为线性空间
- 下一篇: 用Unity开发一款塔防游戏(一):攻击