本文章標題來源於來源於AMD在4C上的一個演講: Compute Shaders: Optimize your engine using compute [3]
Compute Shader是在GPU上運行的程序。雖然是老生常談了,但是我們還是要先介紹一下GPU。 眾所周知,CPU和GPU是兩種不同的架構,那麼他們之間的區別是什麼?
CPU有很強大算術邏輯單元,減少操作延遲;巨大的cache,為了降低內存訪問的延遲;複雜的控制器,使用分支預測來減少分支延遲,使用數據轉發減少數據延遲。
我們可以這樣說:CPU擅長邏輯控制和串列的運算。[1]
GPU有小的cache,用來促進吞吐量;簡單的控制,沒有分支預測和數據轉發;高效節能的ALU,很多延遲很長的ALU,但是為了高吞吐量被重度管線化;需要開啟大量的線程才能降低延遲。
相應地,我們可以這樣說:GPU適用於計算密集型和易於並發的程序。[1][2]
可以看出,CPU和GPU各有自己的擅長,那麼我們可以將二者結合起來,使用CPU做串列,而使用GPU做並行。這種技術就叫做GPGPU,也就是利用GPU進行通用計算的技術(General Purpose Computing on GPU)[1]。
但是,我們知道,通常來講,GPU是用來執行圖形渲染的。那麼,為了執行通用計算,NV推出了CUDA,Khronos推出了OpenCL,Microsoft推出了DirectCompute,也就是後來的Compute Shader,然後,各種圖形API也相繼推出了CS。[25]
DX雖然從10開始支持Compute Shader/Direct Compute,但是限制比較大。DX11的Compute Shader擁有更強大的功能(當然肯定還有DX12)[6]。所以我們一般在Unity中使用CS,還是要求shader target4.5(也就是shader model 5)[19]。
OpenGL從4.3開始支持CS(但是MacOSX不支持4.3)。ES從3.1開始支持CS[5]。
Metal和Vulkan都支持CS[4][7]。
另外PS4和Xbox one(DX11.2)也支持CS[19]。
我們通過幾張圖,來簡單對比一下計算管線與傳統圖形管線有什麼不同。
我們可以看到,計算管線變得很簡單[3]。
(關於GPU Rendering Pipeline,可以參考這張圖[14] https://github.com/ecidevilin/Blogs/blob/master/IntroTo3DGPWithDX/Tessellation/pic/pipeline.jpg)
從硬體端來看:
上圖是圖形管線在硬體端的工作流程[3]。
上圖是計算管線在硬體端的工作流程[3]。
通過對比,我們可以看出: Compute Shader可以在不通過渲染管線的情況下,利用GPU完成一些與圖形渲染不直接相關的工作,從而降低硬體的overhead。
這就是Compute Shader的優勢。
上文中介紹了,目前有很多圖形API支持CS,但是各種API的shading language語法和API各不相同。
Unity的ShaderLab採用了跟HLSL接近的API,方便我們編寫shader。
如果我們在Unity裡面新建一個CS,便是如下的代碼(稍作修改)。
// test.compute #pragma kernel FillWithRed // 1
RWTexture2D<float4> res; // 2
[numthreads(8,8,1)] // 3 void FillWithRed (uint3 dtid : SV_DispatchThreadID) // 4 { res[dtid.xy] = float4(1,0,0,1); // 5 }
這是一個簡單的Compute Shader示例,將一個RT填充成紅色。
如何執行這樣一個CS代碼?在C#裏,調用如下代碼。
public void Dispatch(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ);
在CPU端,我們可以通過這個介面,將CS dispatch出去。Dispatch就相當於Drawcall,但是沒有draw。 其中kernelIndex可以通過ComputeShader.FindKernel來獲取。而threadGroupsXYZ代表線程組的數量。 那麼什麼又是線程組?
在CS裡面,線程可以分為三個維度[2]。
上圖中,最右邊的表示單個線程,最左邊的表示一個dispatch,而圖中間的,表示一個Thread Group。
thread group是指將多個線程組合成為一個group,在這個group裡面,每個線程有自己的相對位置。group內,還可以使用共享變數,相互通信。
將numthreads這個attribute聲明在kernel函數的前面,就表示一個thread group中有多少個thread。
如圖所示一個Dispatch中有3x2x3個thread groups,而一個group中有4x4x2個thread。
這樣做的好處一個是可以利用gpu的warp/wavefront/EU-thread[2][3]。
另外,舉個例子,現在很多圖像壓縮演算法都是基於block的,而thread group(OpenGL裏叫做local size)可以為圖像數據的一個block的大小(例如8x8),group數量可以是圖像的尺寸除以塊的尺寸。每個塊被當作一個單獨的work group來處理,並且group內可以共享一些信息[5]。
更進一步的,我們可以看下圖[6]。
上半圖代表了一個5x3x2的dispatch,圖中的坐標代表一個thread group。
接著,將2,1,0的thread group打開,我們可以看到下半圖。這張圖代表了一個10x8x3的thread group,圖中的坐標代表了一個thread。
如圖所示,我們可以根據這些坐標算出GroupThreadID,GroupID,DispatchThreadID和GroupIndex。
這些id一般是用來作為索引來獲取Buffer、Texture或者thread group shared memory裏的數據。
例如上面舉的例子,GroupThreadID就是圖像的block內的坐標,GroupID是圖像按塊劃分的坐標(圖像的尺寸除以塊的尺寸),而DispatchThreadID是像素的坐標。
CS可以使用一些常規的類型,標量、向量、矩陣、紋理、數組等。
除此之外,為了更靈活的使用CS,還推出了StructuredBuffer,簡稱SBuffer。
GPU Side CPU Side
*StructuredBuffer ComputeBuffer
RWTexture*D RenderTexture|
(SBuffer在fs裏也可以使用,在其他shader裏也可能可以使用。)
StructuredBuffer還包括 RWStructuredBuffer RWStructuredBuffer with counter (RW)ByteAddressBuffer AppendStructuredBuffer * ConsumeStructuredBuffer
StructuredBuffer除了可以包含各種內置的類型之外,還可以包含自定義的struct。
使用groupshared可以將一個變數標記為組內共享(又叫TGSM[2])。
使用這種變數,就可以在thread group內進行通訊。
例如,我們可以在forward+/Deferred管線裏使用compute shader對點光源進行剔除。這個是在戰地3中使用的技術[16][21]。
當我們在不同線程訪問同一個資源的時候,我們需要使用barrier來進行阻塞和同步。
分為以下兩種。
GroupMemoryBarrier DeviceMemoryBarrier AllMemoryBarrier DeviceMemoryBarrierWithGroupSync GroupMemoryBarrierWithGroupSync AllMemoryBarrierWithGroupSync
GroupMemoryBarrier是等待對groupshared變數的訪問。
DeviceMemoryBarrier是等待對texture或buffer的訪問。
AllMemoryBarrier是以上兩者的和。
*WithGroupSync版本是需要同步到當前指令
原子操作,不會被線程調度機制打斷。
InterlockedAdd InterlockedAnd InterlockedCompareExchange InterlockedCompareStore InterlockedExchange InterlockedMax InterlockedMin InterlockedOr InterlockedXor
但是隻能用於int/uint
例如可以用於計算灰度直方圖,用於TonemappingAuto Exposure等效果[19]。
雖然Unity幫我們做了跨平臺的工作,但是我們仍然需要面對一些平臺差異。
另外,在使用CS的時候,我們還需要知道一些性能優化點。
還有一些在渲染管線中適用的tips這裡沒有列舉出來。
那麼介紹過CS之後,我們看看,目前都有哪些應用。
圖為用CS實現的GPU粒子系統,這個功能中使用CS計算粒子的運動軌跡[10]。
圖為布料模擬,使用了CS進行布料粒子的受力運動計算、碰撞檢測和反饋,以及約束計算。類似的還有頭髮模擬和海水模擬[11]。
圖為一個簡單的去色的圖像處理[12],將rgb與(0.299,0.587,0.114)進行dot,獲得灰度值[24]。類似的還有eye adaptation, color grading等等[3]。
Unity的PPS2中使用的histogram就是一個很好的例子,幾乎用到了CS的所有feature[23]。
圖為ASTC演算法壓縮過的圖像(4x4 6x6 8x8)[13]。 上面提到過,我們可以使用CS來實現基於Block的紋理壓縮演算法。
曲面細分[15]:默認管線中的Tessellation比較受限,雖然可以使用Displacement mapping來提升它的效果,但是仍然不夠動態。
我們配合CS一起使用,我們可以配合一些邏輯更自由更動態的生成細分頂點[14][3]。
戰地3中,使用的是Deffered shading pipeline,通過cs對點光源、探照燈等光源進行剔除[16]。
圖片來源,知乎大V MaxwellGeng實現的GPU Occlusiong Culling,他使用了Hiz的方法,對cluster進行遮擋剔除[17]。
而這種思想就是GPUDRP。
圖為刺客信條大革命,在這部遊戲中使用了GPUDRP技術,並在Siggraph 2015: Advances in Real-Time Rendering in Games course中發表[18]。
「Simple, but not easy」是我對Compute Shader的認識,也是對本文的總結。
ES從3.1開始支持CS,也就是說,在手機上的支持率並不是很高。
另外,手機算力還是很低。GTX 1050 Ti的算力是1.9k~2.9k Gflops(floating point operations per second),有768個core。華為P20的Mali-G72 MP12的算力是300+ Gflops,只有12個core [28]。
所以,CS在手機上的使用,是困難的。
但是,我認為它是有巨大潛力的,隨著手機硬體的高速發展,我相信,用不了多久,Compute Shader的使用就可以在手機上普及。
推薦閱讀: