DirectCompute & DirectX 11 計算著色器編程簡介(翻譯)
譯者注:DirectX一直是Windows上圖形和游戲開發(fā)的核心技術(shù)。DirectX提供了一種在顯卡上運行的程序——著色器(Shader)。在DirectX 11之前,著色器是與具體的渲染步驟綁定的,例如像素著色器,頂點著色器等等。而從DirectX11開始,DirectX增加了一種計算著色器(Compute Shader),它是專門為與圖形無關(guān)的通用計算設(shè)計的。因此DirectX就變成了一個通用GPU計算的平臺。鑒于GPU擁有極其強大的并行運算能力,學(xué)習(xí)使用DirectCompute是很有意義的。而大部分人從未用過DirectX的圖形接口,缺乏使用DirectX的經(jīng)驗。本文是一篇完全從零開始學(xué)習(xí)DirectCompute通用計算技術(shù)的文章,無需圖形編程經(jīng)驗,所以我就把它翻譯過來了。有興趣的兄弟可以看看學(xué)學(xué)。
原文地址:http://openvidia.sourceforge.net/index.php/DirectCompute
本文將介紹DirectCompute程序設(shè)計,旨在為沒有DirectX編程經(jīng)驗的人展示DirectCompute從頭開始進(jìn)行程序設(shè)計的一些概念。本文還將介紹DirectX 11計算著色器(Compute Shader)。希望本文可以幫助大家了解使用DirectCompute進(jìn)行GPU通用計算技術(shù)的相關(guān)知識。
示例代碼介紹
完整示例代碼 該鏈接包含了一個基于控制臺的完整DirectCompute程序所需的最小代碼示例(.cpp)。該程序是控制臺程序,不含任何窗口或圖形代碼。
計算著色器代碼 該鏈接包含了完整的著色器代碼(.hlsl)
示例程序深入展示了以下幾個運行計算著色器所需的以下步驟:
- 初始化設(shè)備和上下文
- 從HLSL文件加載著色器程序并編譯
- 為著色器創(chuàng)建并初始化資源(如緩沖區(qū))
- 設(shè)定著色器狀態(tài),并執(zhí)行
- 取回運算結(jié)果。
下面將逐個討論每一個步驟
設(shè)備管理
基本上,DirectCompute需要通過計算著色器5.0(Compute Shader)編程模型(即CS 5.0)才能完全實現(xiàn)。然而CS 5.0需要DirectX 11硬件才能支持。如果沒有Direct X 11硬件(本文編寫時Direct X 11硬件還非常稀少,僅有Ati HD5000系列顯卡),我們?nèi)匀豢梢赃M(jìn)行DirectCompute編程,一種方法是使用參考硬件模式(軟件模擬),另一種方法是使用落后一點的配置,在DirectX 10硬件上運行可以實現(xiàn)部分計算著色器能力的“計算著色器4.0”(如nVidia G80, G92, GT200系列顯卡)。做法是:
- 使用DirectX 11 API編寫程序(比如調(diào)用ID3D11…)
- 創(chuàng)建DX11設(shè)備,但是創(chuàng)建時指定使用DX10和CS 4.0特性等級。
下面的代碼演示如何多次調(diào)用D3D11CreateDevice…()方法,每次創(chuàng)建一種不同的驅(qū)動類型(軟件模擬的參考型或真正GPU加速的硬件型),以及不同的特性等級(DX10、DX10.1或DX11)
D3D_FEATURE_LEVEL levelsWanted[] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0 }; UINT numLevelsWanted = sizeof( levelsWanted ) / sizeof( levelsWanted[0] ); D3D_DRIVER_TYPE driverTypes[] = { D3D_DRIVER_TYPE_REFERENCE, D3D_DRIVER_TYPE_HARDWARE, }; UINT numDriverTypes = sizeof( driverTypes ) / sizeof( driverTypes[0] ); // 遍歷每一種驅(qū)動類型,先嘗試參考驅(qū)動,然后是硬件驅(qū)動 // 成功創(chuàng)建一種之后就退出循環(huán)。 // 你可以更改以上順序來嘗試各種配置 // 這里我們只需要參考設(shè)備來演示API調(diào)用 for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ ) { D3D_DRIVER_TYPE g_driverType = driverTypes[driverTypeIndex]; UINT createDeviceFlags = NULL; hr = D3D11CreateDevice( NULL, g_driverType, NULL, createDeviceFlags, levelsWanted, numLevelsWanted, D3D11_SDK_VERSION, &g_pD3DDevice, &g_D3DFeatureLevel, &g_pD3DContext ); } |
成功運行后,這段代碼將產(chǎn)生一個設(shè)備指針,一個上下文指針還有一個特性等級的Flag。
注意:為簡單起見以上代碼省略了許多變量聲明的代碼。完整示例代碼需要補上這些代碼。這里的代碼片段僅用來展示程序中發(fā)生的事情。
選擇要用的顯卡
用IDXGIFactory對象即可枚舉系統(tǒng)中安裝的顯卡,如下面代碼所示。首先創(chuàng)建一個IDXGIFactory對象,然后調(diào)用EnumAdapters并傳入一個代表正在枚舉顯卡的整數(shù)。如果不存在,它會返回DXGI_ERROR_NOT_FOUND。
// 獲取所有安裝的顯卡 std::vector<IDXGIAdapter1*> vAdapters; IDXGIFactory1* factory; CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory); IDXGIAdapter1 * pAdapter = 0; UINT i=0; while(factory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND) { vAdapters.push_back(pAdapter); ++i; } |
接下來,在調(diào)用D3DCreateDevice創(chuàng)建設(shè)備的時候從第一個參數(shù)傳入想用的顯卡適配器指針,并且將驅(qū)動類型設(shè)為D3D_DRIVER_TYPE_UNKNOWN。詳細(xì)信息請參見D3D11文檔中D3DCreateDevice函數(shù)的幫助。
g_driverType = D3D_DRIVER_TYPE_UNKNOWN; hr = D3D11CreateDevice( vAdapters[devNum], g_driverType, NULL, createDeviceFlags, levelsWanted, numLevelsWanted, D3D11_SDK_VERSION, &g_pD3DDevice, &g_D3DFeatureLevel, &g_pD3DContext ); |
運行計算著色器
譯注:著色器(Shader)是在顯卡上運行的程序,它并不同于CPU上執(zhí)行的程序,可以用HLSL來編寫(見后文)。
DirectCompute程序中的計算著色器是通過Dispatch函數(shù)執(zhí)行的:
// 現(xiàn)在分派(運行) 計算著色器, 分成16x16個線程組。
g_pD3DContext->Dispatch( 16, 16, 1 );
|
以上語句分派了16x16個線程組。
注意,著色器的輸入通??紤]成“狀態(tài)”。就是說你應(yīng)當(dāng)在分派著色器程序之前設(shè)定狀態(tài),而一旦分派了,“狀態(tài)”決定輸入變量的值。所以著色器分派代碼通常應(yīng)該像這樣:
pd3dImmediateContext->CSSetShader( ... ); pd3dImmediateContext->CSSetConstantBuffers( ...); pd3dImmediateContext->CSSetShaderResources( ...); // CS 輸入 // CS 輸出 pd3dImmediateContext->CSSetUnorderedAccessViews( ...); // 運行 CS pd3dImmediateContext->Dispatch( dimx, dimy, 1 ); |
以上所有常量緩沖(constant buffer),緩沖等可以在著色器程序中看到東東都是在分派線程之前通過調(diào)用CSSet…()設(shè)定的。
與CPU進(jìn)行同步
請注意上面所有的調(diào)用都是異步的。CPU方面總是會立即返回然后才具體執(zhí)行。如果有必要,其后調(diào)用的緩沖區(qū)“映射”操作(詳見下文緩沖區(qū)部分)時CPU的調(diào)用線程才會停下來等待所有異步操作的完成。
事件:基本剖析和同步操作
DirectCompute提供一種基于“查詢”的事件機制API。你可以創(chuàng)建、插入并等待特定狀態(tài)的查詢來判斷著色器(或其他異步調(diào)用)具體在何時執(zhí)行。下面的例子創(chuàng)建了一個查詢,然后通過等待查詢來確保運行到某一點時所有該執(zhí)行的操作都已經(jīng)執(zhí)行了,再分派著色器,最后等待另一個查詢并確認(rèn)著色器程序已執(zhí)行完畢。
創(chuàng)建查詢對象:
D3D11_QUERY_DESC pQueryDesc; pQueryDesc.Query = D3D11_QUERY_EVENT; pQueryDesc.MiscFlags = 0; ID3D11Query *pEventQuery; g_pD3DDevice->CreateQuery( &pQueryDesc, &pEventQuery ); |
然后在一系列調(diào)用中插入“籬笆”,再等待之。如果查詢的信息不存在,GetData()將返回S_FALSE。
g_pD3DContext->End(pEventQuery); // 在 pushbuffer 中插入一個籬笆 while( g_pD3DContext->GetData( pEventQuery, NULL, 0, 0 ) == S_FALSE ) {} // 自旋等待事件結(jié)束 g_pD3DContext->Dispatch(,x,y,1); // 啟動著色器 g_pD3DContext->End(pEventQuery); // 在 pushbuffer 中插入一個籬笆 while( g_pD3DContext->GetData( pEventQuery, NULL, 0, 0 ) == S_FALSE ) {} // 自旋等待事件結(jié)束 |
最后用這條語句釋放查詢對象:
pEventQuery->Release(); |
請小心創(chuàng)建和釋放查詢對象以免弄出太多的查詢來(特別是你處理一幀畫面的時候)。
DirectCompute中的資源
譯注:資源是指可以被GPU或CPU訪問的數(shù)據(jù),是著色器的輸入與輸出。包括緩沖區(qū)和紋理等類型。
DirectX中資源是按照以下步驟創(chuàng)建出來的:
- 首先創(chuàng)建一個資源描述器,用來描述所要創(chuàng)建的資源。資源描述器是一種內(nèi)含許多Flag和所需資源信息的結(jié)構(gòu)體。
- 調(diào)用某種Create系方法,傳入描述器作為參數(shù)并創(chuàng)建資源。
CPU與GPU之間的通訊
gD3DContext->CopyResouce()函數(shù)可以用來讀取或復(fù)制資源。這里復(fù)制是指兩個資源之間的復(fù)制。如果要在CPU和GPU之間(譯注:就是在內(nèi)存和顯存之間)進(jìn)行復(fù)制的話,先要創(chuàng)建一個CPU這邊的“中轉(zhuǎn)”資源。中轉(zhuǎn)資源可以映射到CPU的內(nèi)存指針上,這樣就可以從中轉(zhuǎn)資源中讀取數(shù)據(jù)或者復(fù)制數(shù)據(jù)。之后解除中轉(zhuǎn)資源的映射,再用CopyResource()方法進(jìn)行與GPU之間的復(fù)制。
CPU與GPU之間緩沖區(qū)復(fù)制的性能
CUDA-C語言(CUDA是nVidia的GPU通用計算平臺)可以分配定址(pinned)宿主指針和寫入聯(lián)合(write combined)宿主指針,通過它們可以進(jìn)行性能最佳的GPU數(shù)據(jù)復(fù)制。而在DirectCompute中,緩沖區(qū)的“usage”屬性決定了內(nèi)存分配的類型和訪問時的性能。
- D3D11_USAGE_STAGING 這種usage的資源是系統(tǒng)內(nèi)存,可以直接由GPU進(jìn)行讀寫。但是他們僅能用作復(fù)制操作(CopyResource(), CopySubresourceRegion())的源或目標(biāo),而不能直接在著色器中使用。
- 如果資源創(chuàng)建的時候指定了D3D11_CPU_ACCESS_WRITE flag那么從CPU到GPU復(fù)制的性能最佳。
- 如果用了D3D11_CPU_ACCESS_READ該資源將是一個由CPU緩存的資源,性能較低(但是支持取回操作)
- 如果同時指定,READ比WRITE優(yōu)先。
- D3D11_USAGE_DYNAMIC (僅能用于緩沖區(qū)型資源,不能用于紋理資源)用于快速的CPU->GPU內(nèi)存?zhèn)鬏敗_@種資源不但可以作為復(fù)制和源和目標(biāo),還可以作為紋理(用D3D的術(shù)語說,叫做著色器資源視圖ShaderResourceView)在著色器中讀取。但是著色器不能寫入這種資源。這些資源的版本由驅(qū)動程序來控制,每次你用DISCARD flag映射內(nèi)存的時候,如果這塊內(nèi)存還在被GPU所使用,驅(qū)動程序就會產(chǎn)生一塊新的內(nèi)存來,而不會等GPU的操作結(jié)束。它的意義在于提供一種流的方式將數(shù)據(jù)輸送到GPU。
結(jié)構(gòu)化緩沖區(qū)和亂序訪問視圖
ComputeShader的一個很重要的特性是結(jié)構(gòu)化緩沖區(qū)和亂序訪問視圖。結(jié)構(gòu)化緩沖區(qū)(structured buffer)在計算著色器中可以像數(shù)組一樣訪問。任意線程可以讀寫任意位置(即并行程序的散發(fā)scatter和收集gather動作)。亂序訪問視圖(unordered access view,UAV)是一種將調(diào)用方創(chuàng)建的資源綁定到著色器中的機制,并且允許……亂序訪問。
聲明結(jié)構(gòu)化緩沖區(qū)
我們可以用D3D11_RESOURCE_MISC_BUFFER_STRUCTURED來創(chuàng)建結(jié)構(gòu)化緩沖區(qū)。下面指定的綁定flag表示允許著色器亂序訪問。下邊采用的默認(rèn)usage表示它可以被GPU進(jìn)行讀寫,但需要復(fù)制到中轉(zhuǎn)資源當(dāng)中才能被CPU讀寫。 #p#page_title#e#
// 創(chuàng)建結(jié)構(gòu)化緩沖區(qū) // D3DXVECTOR4 聲明在 D3DX10Math.h 中 // http://msdn.microsoft.com/en-us/library/bb205130(VS.85).aspx D3D11_BUFFER_DESC sbDesc; sbDesc.BindFlags =D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE ; sbDesc.Usage =D3D11_USAGE_DEFAULT; sbDesc.CPUAccessFlags =0; sbDesc.MiscFlags =D3D11_RESOURCE_MISC_BUFFER_STRUCTURED ; sbDesc.StructureByteStride =sizeof(D3DXVECTOR4); sbDesc.ByteWidth =sizeof(D3DXVECTOR4) * w * h; hr = g_pD3DDevice->CreateBuffer( &sbDesc, NULL, &pStructuredBuffer ); |
聲明亂序訪問視圖
下面我們聲明一個亂序訪問視圖。注意需要給他一個結(jié)構(gòu)化緩沖區(qū)的指針。
// 創(chuàng)建一個亂序訪問視圖,指向結(jié)構(gòu)化緩沖區(qū)
D3D11_UNORDERED_ACCESS_VIEW_DESC sbUAVDesc;
sbUAVDesc.Buffer.FirstElement =0;
sbUAVDesc.Buffer.Flags =0;
sbUAVDesc.Buffer.NumElements =w * h;
sbUAVDesc.Format =DXGI_FORMAT_UNKNOWN;
sbUAVDesc.ViewDimension =D3D11_UAV_DIMENSION_BUFFER;
hr = g_pD3DDevice->CreateUnorderedAccessView( pStructuredBuffer, &sbUAVDesc,
&g_pStructuredBufferUAV );
|
之后,在分派著色器線程之前,我們需要激活著色器使用的結(jié)構(gòu)化緩沖:
g_pD3DContext->CSSetUnorderedAccessViews( 0, 1, &g_pStructuredBufferUAV, &initCounts ); |
分派線程之后,如果使用CS 4.x硬件,一定要將其解除綁定。因為CS4.x每條渲染流水線僅支持綁定一個UAV。
// 運行在 D3D10 硬件上的時候: 每條流水線僅能綁定一個UAV
// 設(shè)成NULL就可以解除綁定
ID3D11UnorderedAccessView *pNullUAV = NULL;
g_pD3DContext->CSSetUnorderedAccessViews( 0, 1, &pNullUAV, &initCounts );
|
DirectCompute中的常量緩沖
常量緩沖(constant buffer)是一組計算著色器運行時不能更改的數(shù)據(jù)。用作圖形程序是,常量緩沖可以是視角矩陣或顏色常量。在通用計算程序中,常量緩沖可以存放諸如信號過濾的權(quán)重和圖像處理的說明等數(shù)據(jù)。
如果要使用常量緩沖:
- 創(chuàng)建緩沖區(qū)資源
- 用內(nèi)存映射的方式初始化數(shù)據(jù)(也可以用效果接口)
- 用CSSetConstantBuffers設(shè)定常量緩沖的值
下面代碼創(chuàng)建了一個常量緩沖。注意常量緩沖的尺寸,這里我們知道在HLSL中它是一個四元矢量。
// 創(chuàng)建常量緩沖 // D3DXVECTOR4 聲明在 D3DX10Math.h 中 // http://msdn.microsoft.com/en-us/library/bb205130(VS.85).aspx D3D11_BUFFER_DESC cbDesc; cbDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER ; cbDesc.Usage = D3D11_USAGE_DYNAMIC; // CPU 可寫, 這樣我們可以每幀更新數(shù)據(jù) cbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; cbDesc.MiscFlags = 0; cbDesc.ByteWidth = sizeof(D3DXVECTOR4) ; |
接下來用內(nèi)存映射的方式將數(shù)據(jù)發(fā)送到常量緩沖。通常程序員會在CPU程序里定義和HLSL一樣的結(jié)構(gòu)體,因此會用sizeof取得尺寸,然后將緩沖區(qū)的指針映射到結(jié)構(gòu)體來填充數(shù)據(jù)。
// 必須用 D3D11_MAP_WRITE_DISCARD // http://msdn.microsoft.com/en-us/library/bb205318(VS.85).aspx D3D11_MAPPED_SUBRESOURCE mappedResource; g_pD3DContext->Map( pConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource ); unsigned int *data = (unsigned int *)(mappedResource.pData); for( int i=0 ; i<4; i++ ) data[i] = i; g_pD3DContext->Unmap( pConstantBuffer, 0 ); |
注意計算著色器的輸入變量(在這里就是常量緩沖)是當(dāng)成“狀態(tài)”變量的,因此在分派計算著色器之前需要用CSSet…()函數(shù)設(shè)置狀態(tài)。這樣計算著色器執(zhí)行的時候就能訪問到這些變量。
// 在計算著色器中激活
g_pD3DContext->CSSetShader( g_pComputeShader, NULL, 0 );
g_pD3DContext->CSSetConstantBuffers( 0 ,1, &pConstantBuffer );
|
然后我們可以在著色器中聲明一些變量作為常量緩沖區(qū)變量:
cbuffer consts { uint4 const_color; }; |
最后你就可以在著色器的代碼里使用它們了:
uint4 color = uint4( groupID.x, groupID.y , const_color.x, const_color.z ); |
多個常量緩沖
你還可以定義多組不同的常量緩沖。通常情況下它們的值必須一起更新。只要這么寫就可以做到:
cbuffer consts { uint4 const_color_0; uint4 const_color_1; }; |
甚至還可以這么寫:
cbuffer consts { uint4 const_color_0; }; cbuffer more_consts { uint4 const_color_1; }; |
如果const_color_0每次調(diào)用都要更新,而const_color_1每100次調(diào)用才需要更新的話,這樣寫就很有用。若要分別設(shè)置它們,像剛才一樣創(chuàng)建緩沖區(qū)再映射內(nèi)存就行了。之后在分派計算著色器之前,給每一個常量緩沖指定一個“槽位”數(shù)值。這個槽位數(shù)值決定了在HLSL中的出現(xiàn)數(shù)據(jù)。
g_pD3DContext->CSSetConstantBuffers( 0 ,1, &pConstantBuffer ); g_pD3DContext->CSSetConstantBuffers( 1 ,1, &pVeryConstantBuffer ); g_pD3DContext->CSSetShader( g_pComputeShader, NULL, 0 ); |
最后當(dāng)計算著色器運行的時候,g_pComputeShader所指向的著色器就可以訪問這兩個常量緩沖。
計算著色器(CS)HLSL編程
運行在顯卡上的計算著色器是用HLSL(High Level Shader Language 高級著色器語言)寫成的。在我們的例子中它是以文本形式存在,并且在運行時動態(tài)編譯的。計算著色器是一種單一程序被許多線程并行執(zhí)行的程序。這些線程分成多個“線程組”,在線程組內(nèi)的線程之間可以共享數(shù)據(jù)或互相同步。
線程組是通過Dispatch調(diào)用來創(chuàng)建的,例如Dispatch(16, 16, 1)創(chuàng)建了16x16x1個線程組。而每一個線程組中的線程是在著色器代碼中用這個語法來指定的:
[numthreads( 4, 4, 1)] |
推薦把線程組內(nèi)的線程數(shù)(這里的4,4)用#define定義成常量,這樣你就能在著色器代碼中使用這個數(shù)值。
// 線程組尺寸 #define thread_group_size_x 4 #define thread_group_size_y 4 RWStructuredBuffer<BufferStruct> g_OutBuff; /* 這表示線程組中的線程數(shù),本例中是4x4x1 = 16個線程 */ // 等價于 [numthreads( 4, 4, 1 )] [numthreads( thread_group_size_x, thread_group_size_y, 1 )] void main( uint3 threadIDInGroup : SV_GroupThreadID, uint3 groupID : SV_GroupID, uint groupIndex : SV_GroupIndex, uint3 dispatchThreadID : SV_DispatchThreadID ) { int N_THREAD_GROUPS_X = 16; // 假設(shè)等于 16, 是這么分派的 dispatch(16,16,1) int stride = thread_group_size_x * N_THREAD_GROUPS_X; // 緩沖區(qū)跨度,假設(shè)跨度就是數(shù)據(jù)寬度(沒有邊距) int idx = dispatchThreadID.y * stride + dispatchThreadID.x; float4 color = float4(groupID.x, groupID.y, dispatchThreadID.x, dispatchThreadID.y); g_OutBuff[ idx ].color = color; } |
所有線程都是執(zhí)行這同一個函數(shù)。每個線程都有它自己唯一的組內(nèi)線程ID,而每個線程組又有自己的組ID。通常用這些ID來算出一個數(shù)組中要訪問的位置。這樣你就可以開任意數(shù)目的線程,讓他們并行地訪問數(shù)組中的每一個元素。計算著色器的函數(shù)只能接受下列參數(shù)的任意組合,他們代表特殊的意義:
- uint3 threadIDInGroup : SV_GroupThreadID(組內(nèi)線程ID,三個維度)
- uint3 groupID : SV_GroupID(線程組ID,三個維度)
- uint groupIndex : SV_GroupIndex(線性化的組ID,由三個維度計算而成,像光柵操作那樣)
- uint3 dispatchThreadID : SV_DispatchThreadID(在所有分派線程中的跨組線程ID,三個維度)
后記:這個文章可以讓你了解大量進(jìn)行Compute Shader編程的入門問題。不過它沒有介紹太多HLSL的語法以及計算著色器程序的原子操作、同步等重要內(nèi)容,也沒有介紹寄存器,著色器資源視圖(SRV),二維紋理,可讀寫紋理等內(nèi)容。要想充分利用DirectCompute真是需要學(xué)習(xí)不少東西啊~