2020年12月9日,由騰訊遊戲學院舉辦的第四屆騰訊遊戲開發者大會(Tencent Game Developers Conference,簡稱TGDC)於線上舉行。來自維塔士上海工作室的技術總監Andy Fong先生,分享了主機遊戲移植到Switch平台的經驗。以下是分享視頻和文字實錄:
我先介紹一下自己,本人是在2007年加入維塔士上海工作室,在遊戲開發編程領域方面已經有超過13年的從業經驗。近年我參與了《最終幻想12:黃道年代》、《黑色洛城》、《星鏈:阿特拉斯之戰》、《生化奇兵:合集》、《幽浮2:典藏合集》等Switch遊戲的開發。 在介紹Switch方面的優化之前,先介紹一下我自己感受到的Switch平台的理念。
我認為它是着重於創新功能,把移動平台跟主機合併在一個遊戲機裏面,同一款遊戲,在移動和主機平台都可以體驗。主機和移動平台,在操作上是沒有太大區別,很容易就可以適應在兩個不同的情況下玩遊戲,存檔也是可以在兩者之間共享的。手柄方面,Switch投入了新的HD Rumble,就是讓振動更有觸感,也可以通過紅外線檢測玩家的手部移動還有形狀。
在多人遊戲方面,Switch允許玩家用兩個手柄在同一台主機去進行多人遊戲,也可以在多台的Switch主機之間連線去進行多人遊戲。這吸引到很多喜歡本地多人遊戲的玩家。
但相對來説,並沒有投入最高性能的硬件到主機上面。如果把PC、Xbox One或其他的主機遊戲移植到Switch的話,內存使用是需要降低的。在遊戲性能方面,如果要保持在30 FPS,也需要做CPU或GPU的優化。
一般來説,移植其他平台遊戲到Switch平台,需要在預生產的階段進行優化計劃,然後到後期的時候實現計劃。
這裏展示了兩張圖,是《生化奇兵合集》在兩個主機平台的截圖,左邊是Switch平台,右邊是Xbox One的。
我們公司在嘗試,Switch平台能夠穩定保持30幀,希望能夠保持畫質和遊戲性。我們爭取保持這兩點,其他遊戲可能會採用別的方法,比方説犧牲畫質或者是遊戲性這樣去達到性能目標,但有些客户,是不容許掉幀的,同時他們也要求畫質達到其他的遊戲平台一樣的水平。
面對這樣的要求,我們需要更多的做代碼、算法的優化,以及更多支持多核心芯。
接下來,我會針對過去項目的經驗,介紹我們採用的優化策略。首先,我會介紹內存優化這一方面的調查、優化,當一款遊戲進行內存優化到一定水平之後,我們會展開CPU和GPU方面的優化工作。
內存優化
如果把遊戲從PC、Xbox One、PS4遊戲平台移植到Switch的話,很容易會觸碰到Switch的內存上限,一旦超過這個上限,遊戲就很容易會崩潰。如果我們要順利開發的話,肯定是需要進行內存優化。
接下來我會介紹分析、優化的工作,這裏展示不同統計的工具,上面是日誌報告的工具,下面是屏幕統計的工具,屏幕統計工具實是通過底層內存的分配器,收集到各個模塊使用到的內存。
在屏幕上展示日誌報告,其實也是收集這樣的信息,但這是通過QA測試或者是一些自動測試。測試之後,把報告產生出來,事後可以讓程序員去分析。
還有一些專用工具,包括引擎或是SDK方面都會提供專用的內存分析工具,可以去查找內存泄露,除了上面的屏幕統計、日誌報告之外,我們會用工具,發現更深入的問題,然後去查找哪裏使用了內存。
通過這些工具,一般來説我們都能夠知道整個遊戲運行的時候,內存使用的分佈,然後從大塊的使用比較多內存的模塊開始,制定開發優化的策略,一步步把內存優化下來。
接下來我會介紹方法,首先,是優化內存分配器的方法。實際上,內存分配器是所有遊戲一般都會使用的系統,它幫助我們從底層去分配內存給不同的系統使用。
內存分配器會有自己的開銷,引擎在管理內存的時候,也有額外的開銷。兩者是有重複的,所以我們是更推薦使用底層虛擬內存分配接口去配合引擎方面或者是第三方內存頁面分配器方面的功能。
兩者結合去控制這方面的開銷,同時在VRAM的管理方面,我們可以推薦CPU跟GPU能夠共享內存,可以達到動態控制VRAM的分配,這樣可以在需要的時候提高VRAM的使用,也就是虛擬內存的使用。
如果CPU的內存需要更多的話,可以動態地去平衡在VRAM的處理方面,我們推薦用一開始分配比較大的內存池,避免後面如果需要分配內存池的話,會因為碎片的問題分配不出來了。
接下來是關於我們對VRAM使用什麼樣的緩衝,我們是不太推薦使用環緩衝還有雙緩衝這樣的結構,我們更推薦使用類似於DX的Write Discard或者是Write No Overwrite。
Write Discard分配一塊大的內存,可以把過去的內存丟棄了,然後重新分配一塊新的Write No Overwrite,在一個大塊的內存裏面一塊一塊小塊的去分配,這樣可以減少分配內存的開銷,加快速度,清除冗餘資源。
如果從其他平台轉移去Switch平台的話,實際上因為其他平台內存比較足夠,所以會有冗餘的資源,包括不必要的立體聲的音頻、渲染緩衝,預加載的關卡。
在Switch上,我們推薦用更激進的一些streaming,就是加載關卡的策略,更嚴謹的選取真正需要的關卡去加載,如果是非必要的話,我們儘量爭取把它去掉或者是減少它,達到一個內存平衡。
常駐貼圖,其他平台會選擇把UI貼圖或者是低層的mipmap貼圖,裏面比較底層的貼圖會保留在內存裏面一直常駐。在Switch上,很多時候是不允許的,我們會選擇性去加載貼圖,只保留當前需要用的貼圖。
關於着色器的二進制文件,在我們用着色器的編譯器去編譯之後,不同的着色器有可能產生一樣的二進制文件。通過調查,我們會爭取找到重複的二進制文件,確保它的唯一性。
我們在打包着色器二進制文件的時候,也希望能夠儘量在遊戲開始的時候就已經把二進制文件打包在一起,然後一起加載。因為如果是分佈的,在不同情況,加載二進制文件也有機會產生更多的額外開銷。
當我們清除掉冗餘的數據之後,剩下的都是真正遊戲需要用的。但這些我們也是爭取需要減少它們,包括貼圖,一般都是遊戲裏面佔用比較大內存的部分,我們會採用ASTC的壓縮格式。
可以看到,最右邊的這張貼圖是ASTC格式,相對於其他格式,能夠比較好的保護原來貼圖的畫質細節,也允許調整不同的參數,對這個貼圖進行壓縮,在保護畫質的同時,可以達到比較好的一個壓縮率。
對於mipmap貼圖,我們會調整最高的mipmap等級,因為在Switch上,屏幕分辨率已經有所降低了,並不需要時刻地保持最高分辨率的mipmap,我們可以限制最高mipmap的使用。
其他的內存塊,包括渲染緩衝,實際上我們可以檢查GBuffer延遲渲染,是一個大的緩衝,很可能裏面是有些效果是不必要的,可以把渲染緩衝給節省掉。
動畫也進行壓縮,實際上很多引擎都支持動畫壓縮,可以使用更激進的壓縮方法,讓這個動畫的數據更加小一點。模型我們會檢查不同的LOD,有可能有一些LOD,實際上是不需要的,我們就直接去把它去掉了。
我們也碰到很多關卡使用內存過大的問題,其中一個方法,我們會拆分大的關卡,再拆分成兩個、三個更小的關卡。只有當你處於特定關卡的時候,才加載那個關卡,這樣也可以大大節省使用的內存。
CPU優化
上面就大致講完了幾個我們會做內存優化的方向,一般來説,做過這些優化之後,遊戲是能夠跑起來了,但是有可能還沒達到30幀,我們會在GPU跟CPU方面進行優化工作。
實際上兩方面的工作都需要,面對不同遊戲,有時候CPU的工作會多一點、有時候GPU會多一點。
接下來我會先分享一些,CPU分析還有優化的方法,這裏就列出了CPU性能的分析工具。
首先我們會選擇採用引擎專用的工具,包括虛幻引擎4裏面的Stat命令,這裏展示的就是UE4裏面的Stat的命令。
列出的參數實際上是羅列了CPU裏面各個模塊使用的時間,我們也會用第三方工具,比方説Frame Pro,它是沿着時間線把不同的函數給羅列出來,不同函數使用時間也能夠很清晰的看出來。
實際上Switch平台,也有提供自己的CPU研究工具,但這個是受到NDA保護,如果有任天堂開發者帳號,可以去了解。
説到CPU的問題,實際上很多時候會聯想到draw call,這樣會產生一個問題:渲染線程上面投入的時間太長。在過去開發的經驗裏面,我們會使用到多線程渲染。
之前我們開發過開放世界的遊戲項目,因為開放世界,看到很寬闊的場景,所以draw call很高,在那個時候我們也分析了其他的核心,實際上其他核心的使用率是不高的,所以我們考慮採用多線程渲染的方法。
首先是先考慮單線程的情況。實際上CPU是負責去產生命令緩衝,就是產生命令投入到一個命令緩衝裏面,最後會提交給GPU去進行渲染,改變到多線程會怎麼樣呢?
我們研究過Switch平台的圖形接口,實際上是能接受多線程渲染的,我們會給各個線程產生獨立的命令緩衝。難點是在於Setup,圖片裏面有提到Setup,就是設置的部分,在我們渲染每一個draw之前,我們都需要通過Setup的步驟。當我們把這些draw分派給所有不同線程的時候,我們要確保每個線程、每個draw call都能有一個正確的設置。
所以你可以看到,我們分攤到多個線程的時候,實際上多了一些Setup,就是設置的工作,最後我們需要確保所有緩衝能夠順序的提交到GPU裏面。
單線程的情況,是一個渲染緩衝,依次提交給GPU,雖然我們多線程有很多的渲染緩衝,我們還是要確保它們按順序的排列,最後依次再提交到GPU那邊。
從單線程到多線程,一個很大的難點就是拆分隊列,在單線程裏面實際上在Setup就是設置這個部分,它可以做到局部的設置,並不需要全部的參數都刷一遍,可以局部的去刷多線程的時候,因為它拆分了所有的隊列之後,它會需要去刷更多的參數。
我們可以看看左邊的例子,在它渲染第二個draw的時候,如果把draw分攤給另外一個線程去工作,在這個draw之前是需要一個Full Setup的。因為它沒有準備好,還需要做設置參數的設定。
再看看右邊例子,實際上在第二個、第三個draw之前,有一個叫Partial Setup就是局部的設定,如果我們把第二個、第三個Draw,把它分派到另外一個線程,是需要做全局的設定。
這些如果只做一個部分設定的話,它很可能還是會漏掉一些設定,這是其中一個困難,我們在開發這個方法的時候,實際上碰到的問題,就是很多狀態都沒有設置對。
但我們發現這個設置本身,也帶來很多額外的開銷,所以後面我們就採用了一個方法,就是我們嘗試爭取用不同的渲染步驟去拆分任務,去到多線程那邊,減少設置。
當我們完成了多線程渲染之後,實際上在準備命令緩衝的時間就從20毫秒減到6毫秒,實際上也是一個很不錯的提升。後邊就進入到另外一個優化,叫圖形腳本的原生化。
實際上圖形腳本,更多是在主線程那邊去執行的,現在越來越多引擎會使用圖形腳本進行遊戲邏輯,包括UE4的藍圖,這些腳本很容易上手,遊戲策劃也可以去使用,但如果是重度使用的話,很可能會對主線程的性能產生壓力,因為它調用的函數會比較多,調用棧站也比較深。
一般來説,我們會採用原生化的方法去把腳本轉換成C++代碼,也有兩種方法,一種是部分原生化,一個是深度原生化。部分原生化的話,我們會自己去檢查,哪一些腳本的性能損耗是比較高的,去選擇性的把局部腳本轉換成C++代碼。
深度原生化的話就是,我們會更多的去實現工具,把圖形腳本轉換成C++代碼,這個會達到更高的提升,當然也會投入更多時間去開發這樣的工具,在過去的項目,我們曾經有經驗能達到10%到20%這樣的提升。
後邊是關於聲音的優化。實際上聲音在現在新時代的遊戲都是很重要的一個方面。
如果是用第三方去播放聲音的話,會用第三方的聲音庫提供的專用工具進行性能檢查。然後在Switch上面使用壓縮方法,就是OPUS還有ADPCM。
OPUS這個格式,有很好的壓縮率,解壓縮速度也很快,但是有一個問題,聲道比較有限,如果聲音聲道比較多的話,會採用ADPCM去播放它們。
另外一些問題,是關於DSP音效的播放,這些效果實際上是需要採用CPU進行運算,我們推薦把這些效果烘焙到聲道上面,讓它去直接播放,節省運算效率,保護關鍵聲音的實例,把次要、低優先級的聲音去掉,或者是把它的頻率給降低,就是讓它不要那麼頻繁地去播放,也可以節省運算時間。
對於距離遠的3D聲音,可以考慮把它放到虛部,放成一個虛部聲音的話,可以減少運算,只有當它靠近比較明顯的時候才開始去計算它們。
好了,上面提到的就是大致我們比較很常用的CPU優化,實際上這裏還有更多的CPU優化,如果以後有機會,我們可以再多交流這方面,接下來我會介紹GPU方面的性能分析、優化方法。
GPU優化
GPU性能的話,一般會跟着色器的效率有關,也會跟每個渲染命令所填充的像素會有一定關係,對於Switch的主機模式以及掌機模式,我們一般會推薦使用1080p的分辨率、掌機會使用720p。這也是考慮到在這兩個模式底下的GPU的性能,這樣去定義的話,確保能在不同的模式都達到30幀的幀率。
接下來談一下分析的工具,一般來説,我們會採用SDK提供的API去獲取GPU裏面不同步驟的渲染時間。把它羅列在屏幕上面,就像這個截圖上面一樣。
也會採用SDK提供的專門工具研究GPU上的問題,如果有任天堂開發者帳號的話可以多瞭解這個工具。
我們對這兩方面收集起來的數據進行對比,確認最終GPU問題在哪裏。有時候我們也會採用個別工具,把某些效果關掉、打開,這樣對比耗用的時間,可以確認某些效果所耗用的時間。
接下來是優化,一般來説,我們會考慮去除分支,對於着色器的程序,會去除一些分支。實際上分支語句無論是對於CPU或者GPU,都是一些難點。因為沒有辦法去判斷程序的流向,着色器的分支會被平坦化。
意思就是説,分支的兩邊都會執行,最後會選擇其中一個結果,但是如果它們工作量不平衡的話,也會影響到在GPU上面執行的並行性。
所以我們儘量爭取去移除掉分支頁面,我們展示了一些方法,第一個是替換寫法,使用特殊語句,比方説叫saturate還有lerp語句,可以很好地去替代原來的分支語句。
也可以使用一種條件編譯,把不同分支的代碼組織成不同的着色器二進制文件,然後在C++那邊去進行判斷。當你可以能夠判斷每一個draw call實際上只用到某一個分支代碼的時候,你就可以這樣去做,在C++那邊就已經判斷出來使用某個特定的着色器二進制文件,執行二進制文件避免你每一個數都需要去判斷,做這個分支的語句,對性能會有相對比較好的提升。
下一個是一個叫圖塊緩衝的優化方法,這個是屬於Tegra GPU的功能,GPU可以接受一些繪製的請求收集起來,再把渲染緩衝劃分成很多的圖塊,一個一個圖塊播放渲染的請求。
這樣的好處就是,當你在一個圖塊上面去渲染的時候,實際上它在訪問一些幀緩衝命中cache的機會會大大提高。當你能夠去從cache裏面獲得數據的時候,能夠大大地加快你的訪問速度。
這個使用場景是什麼呢?就是當你有很多大面積的粒子實際上這個圖展示的粒子效果,這些粒子效果都需要去訪問幀緩衝,一般來説,我們嘗試過把PS2的遊戲移植到Switch。實際上PS2上面VRAM的帶寬還是比較高的,去到Switch上面需要優化圖塊緩衝是不錯的選擇。
在一些情況下,我們會達到五毫秒的提高,但需要注意的是這裏面有收集渲染命令,還有播放的步驟。就是當我們渲染的一些單元使用比較大量的頂點屬性,還有頻繁地切換這些屬性的話,建議還是不要太多去使用,因為可能會在收集跟播放會增加開銷。
接下來是關於美術數據,一般來説我們在所有GPU手段都沒有太大的效果,包括動態分辨率、圖形緩衝、圖塊緩衝,這些都沒有太多的幫助的情況下。
我們會考慮對美術資源進行優化,這裏有一張截圖是《幽浮2》的基地截圖。實際上它展示了很多不同的小房間,在最開始的時候,所有的小房間都是充滿了很高精度的細節。
實際上我們發現當鏡頭拉得比較遠的時候,並不需要對所有房間都能夠提供這麼高的細節的視覺。我們就對每個房間都創建了LOD的模型,實際上就是不同層值、不同精度的模型,可以在拉遠的時候採用相對精度比較低的模型。
實質上在Switch的屏幕上面是不太能看出區別,除了這個模型之外,就是關於粒子效果,我們也是有遊戲會採用粒子的LOD去渲染粒子,也得到不錯的優化。
其他方面,可以選擇不去做投影或者是降低密度,節省GPU的開銷。
接下來想講幫助優化的工具,當遊戲跑到某一個位置突然嚴重的掉幀,會在屏幕展示警告,像warning frame-drop這樣的顯示很容易就被QA或者是開發人員給檢測出來,馬上就開展調查,瞭解為什麼會掉幀了,也會去專門給某些特定敵人或者是技巧去做功能,去觸發敵人。
對於模型的話,我們會採用第三方軟件,比方Simplygon還有Houdini,這樣去製作低模的LOD模型替換高模模型進行自動測試。
我們很多時候都會讓編譯器去自動去跑遊戲,然後收集內存、性能的信息,這樣可以讓開發人員調查問題時,節省QA的測試成本。
這次分享,我想表達的核心目標是,在保護遊戲的畫質、遊戲性的前提下,保證Switch遊戲到穩定幀數,加強玩家體驗。
包括三方面,首先,內存需要優化到位,包括分配器,要時刻關注它額外的開銷,對於冗餘數據,檢查是不是有渲染緩衝可以節省,常駐的貼圖是不是可以變成非常駐。對於大的數據資源,包括貼圖,採用ASTC壓縮技術。對於CPU方面的優化,就是多線程渲染,可以幫助減輕渲染線程的壓力,在主線程對圖形腳本進行原生化,把它轉換成C++代碼。
聲音方面,我們推薦使用OPUS壓縮格式,對於GPU的話對着色器會進行一個去除分支操作,比方説用lerp語句取代原來的分支語句。對於圖塊、圖塊緩衝可以有效優化粒子效果,特別是大面積的粒子效果。
最後,我們會對美術數據進行選擇性優化,包括採用LOD或者是關閉投影效果,達到降低GPU消耗這樣的目的。我們也會採用工具,包括檢測掉幀的工具、自動測試的工具,儘量給開發人員提供有用信息,加快優化工作。