[ARKit]6-3D與AR/VR應用Debug與優化淺談

蘋果API搬運工發表於2018-03-03

說明

ARKit系列文章目錄

學習ARKit前,需要先學習SceneKit,參考SceneKit系列文章目錄

UI介面/3D模型除錯

介面與模型除錯仍然是使用View Debuger,進入除錯狀態後可看到圖層狀態

WX20180302-092750@2x.png

點選3D圖層,會進入3D模型編輯器,顯示的是實時狀態的模型

WX20180302-092322@2x.png

右側可以調整材質等資訊,點選右下角的按鈕,可以檢視Shader和Action,選中可以檢視我們使用的Shader,再點選問號可檢視引數輸入:

WX20180302-092432@2x.png
WX20180302-092607@2x.png

效能除錯

效能除錯分為實時檢視(用Gauge),與錄製分析(用Instruments)

FPS Gauge

FPS Gauge不僅是實時的,還能顯示負載的分類

WX20180302-092059@2x.png
WX20180302-094045@2x.png

Instruments->SceneKit

一般先使用SceneKit工具分析,可以精確到每幀,但需要先錄製一段;分析問題找到原因後再處理,或使用Metal System Trace再分析Metal程式

WX20180302-091837@2x.png
WX20180302-090733@2x.png
WX20180302-090847@2x.png

例如,蘋果官方的例子中,先用SceneKit工具分析出問題出在Metal程式碼上,再用 Metal System Trace分析出問題出在Shader編譯太慢上.對應的解決方法是:提前編譯Shader.

WX20180302-095213@2x.png
WX20180302-095230@2x.png
WX20180302-095243@2x.png

Metal2高階除錯

本部分內容來自於WWDC2017中的Metal 2 Optimization and Debugging

示例程式的程式碼蘋果貌似沒有全部提供,反正我在蘋果文件庫中只找到了一個示例MetalDeferredLighting.

Metal Frame Debugger(Metal幀偵錯程式)

其實就是對原來的GPU Frame Debugger的增強,使用方法還是和原來一樣,在執行中點選下方工具欄中的照相機圖示捕捉Frame,現在長按也可以彈出選單顯示更多內容了.

WX20180303-105617@2x.png

原有功能:

  • Fully featured frame debugger :全功能的幀偵錯程式
  • Navigate through your workload :瀏覽工作負載
  • Inspect state and resource :檢查狀態和資源
  • Debug graphics and compute :除錯圖形和計算
  • Integrated into Xcode :整合進Xcode

新增了一些功能:

  • Improved Capture Performance :改進效能,提升10倍速度
  • Full Metal 2 Support :全面支援Metal 2
    • Raster order groups :光柵順序組
    • Sampler arrays :取樣器陣列
    • Viewport arrays :視口陣列
    • New pixel formats :新畫素格式
    • New vertex array formats :新頂點陣列格式
    • Argument buffers :引數快取器
  • VR Support :支援VR(macOS上,如SteamVR)
  • Improved Capture Workflow :改進捕捉流程
  • Metal Quick Looks :Metal預覽(也可以用在神經網路CNN中檢視相關資料)
  • Advanced Filtering :高階搜尋過濾
  • Pixel Inspection :畫素檢查
  • Inspect Vertex Attribute Outputs :頂點屬性輸出檢查

下面讓我們結合一個Demo來演示新增功能的使用. 首先,執行一下,這是一個有問題的程式,注意雪花附近出現了異常

WX20180303-114733@2x.png

接著開始除錯,先看紋理本身有沒有異常,在對應位置打斷點,就可以直接預覽GPU上的紋理,非常方便:

WX20180303-114827@2x.png
紋理沒有問題.接著捕捉一幀,看看到底怎麼回事.在相機圖示上長按,選擇Rendering,進入了Metal Frame Debuger中:
WX20180303-114931@2x.png
WX20180303-115050@2x.png

要找到繪製雪花顆粒(particle)的地方,可以在左下角使用高階搜尋,新增條件Forward RenderParticle,搜尋結果中只有一個API滿足要求,點選這個搜尋結果,顯示詳情:

WX20180303-115204@2x.png
WX20180303-115338@2x.png

看看頂點資料是不是有問題,雙擊Vertex Attributes檢視頂點的輸入輸出資料

WX20180303-115403@2x.png
WX20180303-115454@2x.png

資料看起來沒有肉眼可見的異常,估計應該是正常的.再找別的原因. 先回到左側導航欄搜尋結果中,展開當前API呼叫涉及的所有資源,選中Attachments,開啟右下角的畫素檢查器:

WX20180303-115812@2x.png

兩張圖:左邊是渲染目標的色彩圖,右側是渲染目標的深度圖,移動圓形的畫素檢查器Pixel Inspector來檢查畫素級的問題.

WX20180303-115900@2x.png
WX20180303-115938@2x.png

經過查詢,我們發現右側的深度圖上,雪花邊緣附近的深度值不一致,這就是bug所在,正常情況下particle不應該寫入深度值到深度緩衝器depth buffer中.
修復這個bug即可,此處略過...

GPU Shader Profiler

這是整合在Metal Frame Debugger中的Shader分析利器,可以分析出編譯後的Shader哪裡有效能問題.例如:

WX20180303-124248@2x.png

Metal Pipeline Statistics(Metal管線統計)

這個工具也是整合在Metal Frame Debugger中的Shader分析利器.功能如下:

  • Per-shader metrics:每個著色器節奏 編譯器產生的統計資料:

    • Instruction count :指令數
    • Instruction mix :指令組合
    • Register usage :暫存器使用
    • Occupancy :佔用率
  • Compiler remarks :編譯器評價 能避免以下情況出現:

    • Slow math usage:低效的數學運算
    • Register spills:暫存器洩露
    • Stack usage:棧的使用(GPU用棧儲存或讀取會造成負擔,如shader使用了可變陣列)
    • Other optimization opportunities:其他可優化情況

還是通過一個Demo來演示
開啟專案執行,點選相機圖示,進入幀偵錯程式,切換顯示方式,找到Pipeline Statistics介面中:

WX20180303-124539@2x.png
WX20180303-125026@2x.png

中間的上方顯示出編譯器給出的優化建議,我們先處理和棧相關的第2個和第4個,雙擊進入shader中:

WX20180303-131003@2x.png

部分程式碼如下,發現其中的可變陣列會造成影響:

//問題程式碼
float3 v = in.v_view * (scene_z / in.v_view.z);

// Now, we have everything we need to calculate our view-space lighting vectors.
FragOutput output;
output.light = float4(0);
output.albedo = gBuffers.albedo;
output.normal = gBuffers.normal;
output.depth = gBuffers.depth;

float4 lightingFinal[FAIRY_GROUP_SIZE];
for(int i = 0; i < FAIRY_GROUP_SIZE; i++) {
    lightingFinal[i] = float4(0);
    float3 l = (lightData->view_light_position.xyz - v);
    float n_ls = dot(n, n);
    float v_ls = dot(v, v);
    float l_ls = dot(l, l);
    float3 h = (l * rsqrt(l_ls / v_ls) - v);
    float h_ls = dot(h, h);
    float nl = dot(n, l) * rsqrt(n_ls * l_ls);
    float nh = dot(n, h) * rsqrt(n_ls * h_ls);
    float d_atten = sqrt(l_ls);
    float atten = fmax(1.0 - d_atten / lightData->light_color_radius.w, 0.0);
    float diffuse = fmax(nl, 0.0) * atten;
    
    float4 light = gBuffers.light;
    light.rgb += lightData->light_color_radius.xyz * diffuse;
    light.a += pow(fmax(nh, 0.0), 32.0) * step(0.0, nl) * atten * 1.0001;
    lightingFinal[i] = light / FAIRY_GROUP_SIZE;
}

for(int i = 0; i < FAIRY_GROUP_SIZE; i++) {
    output.light += lightingFinal[i];
}

return output;
複製程式碼

修改程式碼,移除陣列相關程式碼,直接計算光線最終值:

//改後程式碼,移除lightingFinal陣列相關程式碼
float3 v = in.v_view * (scene_z / in.v_view.z);

// Now, we have everything we need to calculate our view-space lighting vectors.
FragOutput output;
output.light = float4(0);
output.albedo = gBuffers.albedo;
output.normal = gBuffers.normal;
output.depth = gBuffers.depth;

for(int i = 0; i < FAIRY_GROUP_SIZE; i++) {
    float3 l = (lightData->view_light_position.xyz - v);
    float n_ls = dot(n, n);
    float v_ls = dot(v, v);
    float l_ls = dot(l, l);
    float3 h = (l * rsqrt(l_ls / v_ls) - v);
    float h_ls = dot(h, h);
    float nl = dot(n, l) * rsqrt(n_ls * l_ls);
    float nh = dot(n, h) * rsqrt(n_ls * h_ls);
    float d_atten = sqrt(l_ls);
    float atten = fmax(1.0 - d_atten / lightData->light_color_radius.w, 0.0);
    float diffuse = fmax(nl, 0.0) * atten;
    
    float4 light = gBuffers.light;
    light.rgb += lightData->light_color_radius.xyz * diffuse;
    light.a += pow(fmax(nh, 0.0), 32.0) * step(0.0, nl) * atten * 1.0001;
    output.light += light / FAIRY_GROUP_SIZE;
}

return output;
複製程式碼

點選按鈕,重新載入Shader,各項數值已減小:

WX20180303-132648@2x.png
WX20180303-133515@2x.png

GPU Counter Profiling(GPU計數分析)

這個工具也是整合在Metal Frame Debugger中的,但並不針對於Shader.功能有圖形化列表展示瓶頸分析:

還是分析一個Demo,執行捕捉,進入幀偵錯程式,選中GPU展示GPU Counter Profiling介面:

WX20180303-135226@2x.png

先看左側圖形化列表,雙指放大,選中啟動時的最高峰:

WX20180303-135351@2x.png
WX20180303-140032@2x.png

我們發現問題出在Fragment Shader TimePixels Stored上面,先展開第一個Fragment Shader Time進行分析:

WX20180303-140147@2x.png

發現FS Stall Time很高,說明等待的時間非常長,這一般是由於從記憶體中讀取圖片或資料造成的.向下滾動檢視紋理快取的情況:

WX20180303-140258@2x.png

看到Texture Cache Miss Rate很高,也就說明紋理命中率只有不到40%,所以需要不斷從記憶體中讀取紋理,造成效能問題.這也解釋了為什麼前面Pixels Stored也很高.

具體哪裡出現了問題,還需要看右側的瓶頸分析資料表:

WX20180303-141425@2x.png

提示紋理可能沒有mipmaps,點選右側箭頭:

WX20180303-141620@2x.png

原來是載入了一個256M的高度地圖,造成了快取被大量佔用,快取命中率低,不斷從記憶體讀取圖片,GPU不斷等待.

原因找到!!

最後

Metal相關的除錯技巧也適用於蘋果的機器學習框架(基於Metal)中.學習相關技巧,受益很多.

由於水平所限,本文第二部分的高階除錯基本是照搬蘋果WWDC2017的演講,具體在自己的專案中使用時,因為不同平臺(macOS/iOS/tvOS),不同技術(SceneKit/SpriteKit,Metal/OpenGLES)還是會有一些不同的限制. 當然,最大限制還是在於自己是否足夠了解圖形學的相關知識,對此我深感力不從心,需要學習更多相關基礎.

相關文章