本篇博客给读者介绍关于计算着色,在directx11中,微软引入了计算着色器(也称为直接计算),它基于可编程着色器,并利用GPU来执行高速通用计算。这个想法是使用一个写在HLSL中的着色器来制作一些图形。不同于我们编写的通常的着色器,计算着色器提供了某种形式的内存共享和线程同步,这有助于改进我们使用该工具所能做的事情。计算着色器的执行即使它可以访问图形资源,它也不附加到图形管道的任何阶段,当我们分派一个计算着色器调用时,我们所做的是生成一些GPU线程,它们运行一些我们编写的着色程序代码。
我们需要理解的主要问题是,不同于像素和顶点着色,计算着色器不受输入/输出数据的约束;在执行代码和处理数据的线程之间
没有隐式映射。每个线程都可以从任何内存位置读取,也可以在任何地方写入。这是计算着色器的主要问题。它们提供了一种方法,
可以将GPU用作通用计算的大型矢量处理器。
在这里我们以一个简单的事例给读者介绍一下,假设从一个图像读取数据,并输出另一个图像,处理图像并不是唯一可以使用
计算着色器的方法,但从更简单的角度看,决定这样做。同样,由于线程和数据之间没有隐式关联,没有人强迫我们每个线程处理
一个像素;我们可以处理几百个。对于本篇博客,我们将使用简单的方法,我们将处理每个线程的像素,但是记住,这不是强制的,
我们开始吧。
下面开始实现上述功能,首先进行初始化操作,cpp实际上只是创建了一个DXApplication(简单命名的应用程序)实例,并调用如下方法。
if( FAILED( InitWindow( hInstance, nCmdShow ) ) ) return 0; if(!application.initialize(g_hWnd, width, height)) return 0; // Main message loop MSG msg = {0}; while( WM_QUIT != msg.message ) { if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); } else { application.render(); } } return ( int )msg.wParam;
继续介绍,我不会给出初始化或渲染,因为它们非常简单。前者创建DX11设备,加载纹理等,而后者则使用两种纹理来呈现一个大的quad。唯一有趣的是在初始化的地方,我们调用了两个方法,createInputBuffer和createOutputBuffer,以创建需要为计算着色器工作的缓冲区,并在它加载和运行计算着色器(runComputeShader)之后立即运行。我们将详细介绍这三个函数,因为它们是本博客的核心。
// ... last lines of DXApplication::initialize() // We then load a texture as a source of data for our compute shader if(!loadFullScreenQuad()) return false; if(!loadTexture( L"data/fiesta.bmp", &m_srcTexture )) return false; if(!createInputBuffer()) return false; if(!createOutputBuffer()) return false; if(!runComputeShader( L"data/Desaturate.hlsl")) return false; return true; }
运行程序,按F1和F2,我们在两个计算着色器之间切换。这在代码调用runComputeShader(L“数据/ Desaturate.hlsl”)和runComputeShader(L“data / circl . hlsl”)上分别在F1和F2键上完成。runComputeShader函数从HLSL文件加载计算着色器,并分派线程组。
在继续讲解之前,最好先谈谈GPU的工作原理,与CPU不同的是,gpu由多个称为流处理器的处理器组成,每个处理器都可以用来运行线程并执行我们的着色器的代码。流处理器被打包成块,称为SIMDs,它们有本地数据共享、缓存、纹理缓存和获取/解码单元。
这实际上是一种简化,像GeForce 6600这样的旧的视频卡,曾经有顶点、片段和组合单元(而且它们无法运行计算着色器!)无论如何,我们将简化GPU的视图。
继续介绍,在GPU上,我们有几个SIMDs(例如,在ATI HD6970上有24个单元),每一个都可以用来运行一组线程,如果不深入了解细节,我们需要知道的是,可以编写我们的计算着色器,可以创建具有一些共享内存的线程组,并且能够并发运行。这些线程能够从一些缓冲区读取数据并将其写入输出缓冲区。因此,除了像素着色器之外,设置计算着色器的两个主要内容是线程之间的共享内存和在输出缓冲区中写入的可能性。
从线程的角度考虑计算着色器是很重要的,而不是“像素”或“顶点”。处理数据的线程,我们可以为每个线程计算4个像素,或者我们可以做物理
运算来移动刚体,计算着色器允许我们使用GPU作为一个强大的矢量并行处理器!
现在我们知道,我们可以在GPU上生成线程,我们必须在组中组织这些线程。这如何转化为代码?我们指定直接在shader代码内生成的线程数。这是用以下语法完成的:
[numthreads(X, Y, Z)] void ComputeShaderEntryPoint( /* compute shader parameters */ ) { // ... Compue shader code }
程序代码中有X,Y,Z,其中X、Y和Z代表该组织的每条轴的大小。这意味着,如果我们指定X = 8,Y = 8,Z = 1,我们得到8 * 8 * 1 = 64个线程。但是为什么我们不能只指定一个数字,比如64呢?为什么要指定每个轴的大小?实际上,唯一的原因是有一种方便的方法来访问在矩阵(如图像)上工作的线程。实际上,我们可以从以下系统中获得当前线程id:
uint3 groupID:SV_GroupID
-在每个维度的调度范围内的分组索引
uint3 groupThreadID:SV_GroupThreadID
-每个维度的组内线程的索引
使用uint groupIndex:SV_GroupIndex
-组内的顺序索引,从左上角开始,一直到右下角
uint3 dispatchThreadID:SV_DispatchThreadID
-整个调度中的全局线程索引
我们需要这些值来区分哪些数据可以访问哪个线程。我们来看看,例如,我们如何编写一个简单的减饱和度计算着色器。
[numthreads(32, 16, 1)] void CSMain( uint3 dispatchThreadID : SV_DispatchThreadID ) { float3 pixel = readPixel(dispatchThreadID.x, dispatchThreadID.y); pixel.rgb = pixel.r * 0.3 + pixel.g * 0.59 + pixel.b * 0.11; writeToPixel(dispatchThreadID.x, dispatchThreadID.y, pixel); }
因此, 这个计算着色器有32 * 16 * 1个线程组(例如,每个组最大线程数为768,在cs_4_x中为最大Z = 1,在cs_5_0中为最大Z = 64)。唯一一个函数的参数是全局线程ID,恰好是,由于我们操作一个图像,像素的X和y的函数readPixel和writePixel,在这里我们想只关注逻辑,我们调用readPixel,它将从着色器提供的图像中读取相应的像素,然后我们取消了像素并将结果保存到可变像素本身,我们不利用函数writePixel将我们的新值输出到输出图像。
/** * Run a compute shader loaded by file */ bool DXApplication::runComputeShader( LPCWSTR shaderFilename ) { // Some service variables ID3D11UnorderedAccessView* ppUAViewNULL[1] = { NULL }; ID3D11ShaderResourceView* ppSRVNULL[2] = { NULL, NULL }; // We load and compile the shader. If we fail, we bail out here. if(!loadComputeShader( shaderFilename, &m_computeShader )) return false; // We now set up the shader and run it m_pImmediateContext->CSSetShader( m_computeShader, NULL, 0 ); m_pImmediateContext->CSSetShaderResources( 0, 1, &m_srcDataGPUBufferView ); m_pImmediateContext->CSSetUnorderedAccessViews( 0, 1, &m_destDataGPUBufferView, NULL ); m_pImmediateContext->Dispatch( 32, 21, 1 ); m_pImmediateContext->CSSetShader( NULL, NULL, 0 ); m_pImmediateContext->CSSetUnorderedAccessViews( 0, 1, ppUAViewNULL, NULL ); m_pImmediateContext->CSSetShaderResources( 0, 2, ppSRVNULL ); ...
在 这里没有什么复杂的,忽略我们用来清除输入和输出缓冲区的变量,第一个有趣的是加载着色器的行,自从我们每次点击F1或F2的时候,我们都运行这个着色器,我们每次都重新加载它并重新编译它。
这样,一旦着色器被加载,我们就运行它。方法CSSetShader设置我们的计算着色器。CSSetShaderResources和CSSetUnorderedAccessViews用于设置输入和输出缓冲区。我们将在后面详细介绍这些内容;这里重要的是m_srcDataGPUBufferView是计算着色器的输入缓冲区,它包含我们的输入图像数据,而m_destDataGPUBufferView是一个输出缓冲区,它将包含我们的输出图像数据。
这里最重要的是调度,这是我们对DX11进行着色的地方,也是我们指定创建组的位置,由于我们已经决定每个像素运行一个线程,因此需要分派足够的组来覆盖整个图像;picutre维度是1024x336x1,我们为每个组生成32x16x1线程,因此我们需要32x16x1组来完美地覆盖图像。在最后一行中,我们重新设置了DX11状态。
至此,我们已经了解了如何指定线程和线程组。这是我们需要知道的关于计算着色器的一半。另外一半是如何为计算着色器提供数据到GPU工作。
接下来继续介绍,计算着色器以两种方式接收输入数据:字节地址缓冲区(原始缓冲区)和结构化缓冲区。在本博客中,我们将使用结构化的缓冲区,但是像往常一样,记住缓存正在做什么是很重要的;这是阵列结构与结构阵列的典型问题。
我们的结构化缓冲区,定义为计算着色器,看起来是这样的:
struct Pixel { int colour; }; StructuredBuffer<Pixel> Buffer0 : register(t0);
给读者解释一下,这个结构包含一个元素,因此我们可以很容易地使用原始缓冲区,但是我们可能想要指定r、g、b、a和4个浮点数,在这种情况下,将它们封装到结构中是很有用的。
为了计算方便,我已经决定使用int来编码颜色,只是为了在着色器内部进行比特移位操作。显然,这并不是GPU的最佳用途,它更喜欢使用浮点和向量,但我对使用这些新操作很有兴趣,它们与shaders 4和5一起使用。再说一次,这不是GPU最好的用途,我们只是为了方便。
现在,要使用DX11创建结构化缓冲区,我们使用以下代码:
/** * Once we have the texture data in RAM we create a GPU buffer to feed the * compute shader. */ bool DXApplication::createInputBuffer() { if(m_srcDataGPUBuffer) m_srcDataGPUBuffer->Release(); m_srcDataGPUBuffer = NULL; if(m_srcTextureData) { // First we create a buffer in GPU memory D3D11_BUFFER_DESC descGPUBuffer; ZeroMemory( &descGPUBuffer, sizeof(descGPUBuffer) ); descGPUBuffer.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; descGPUBuffer.ByteWidth = m_textureDataSize; descGPUBuffer.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; descGPUBuffer.StructureByteStride = 4; // We assume the data is in the // RGBA format, 8 bits per chan D3D11_SUBRESOURCE_DATA InitData; InitData.pSysMem = m_srcTextureData; if(FAILED(m_pd3dDevice->CreateBuffer( &descGPUBuffer, &InitData, &m_srcDataGPUBuffer ))) return false; // Now we create a view on the resource. DX11 requires you to send the data // to shaders using a "shader view" D3D11_BUFFER_DESC descBuf; ZeroMemory( &descBuf, sizeof(descBuf) ); m_srcDataGPUBuffer->GetDesc( &descBuf ); D3D11_SHADER_RESOURCE_VIEW_DESC descView; ZeroMemory( &descView, sizeof(descView) ); descView.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; descView.BufferEx.FirstElement = 0; descView.Format = DXGI_FORMAT_UNKNOWN; descView.BufferEx.NumElements=descBuf.ByteWidth/descBuf.StructureByteStride; if(FAILED(m_pd3dDevice->CreateShaderResourceView( m_srcDataGPUBuffer, &descView, &m_srcDataGPUBufferView ))) return false; return true; } else return false; }
注意,这个函数假定我们已经从磁盘加载了图像,并将所有的像素保存到m_srcTextureData中。
我们所做的只是创建一个无序访问资源,它可以绑定到着色器。我们还指定了结构化的缓冲标志,并提供了两个元素之间的跨越(这是4个字节,每个通道一个)。
如果在没有问题的情况下创建缓冲区,我们也会创建一个shader视图,它是向着色器提供数据的DX11方法。
与我们创建输出缓冲区的方式非常相似:
/** * We know the compute shader will output on a buffer which is * as big as the texture. Therefore we need to create a * GPU buffer and an unordered resource view. */ bool DXApplication::createOutputBuffer() { // The compute shader will need to output to some buffer so here // we create a GPU buffer for that. D3D11_BUFFER_DESC descGPUBuffer; ZeroMemory( &descGPUBuffer, sizeof(descGPUBuffer) ); descGPUBuffer.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; descGPUBuffer.ByteWidth = m_textureDataSize; descGPUBuffer.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; descGPUBuffer.StructureByteStride = 4; // We assume the output data is // in the RGBA format, 8 bits per channel if(FAILED(m_pd3dDevice->CreateBuffer( &descGPUBuffer, NULL, &m_destDataGPUBuffer ))) return false; // The view we need for the output is an unordered access view. // This is to allow the compute shader to write anywhere in the buffer. D3D11_BUFFER_DESC descBuf; ZeroMemory( &descBuf, sizeof(descBuf) ); m_destDataGPUBuffer->GetDesc( &descBuf ); D3D11_UNORDERED_ACCESS_VIEW_DESC descView; ZeroMemory( &descView, sizeof(descView) ); descView.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; descView.Buffer.FirstElement = 0; // Format must be must be DXGI_FORMAT_UNKNOWN, when creating // a View of a Structured Buffer descView.Format = DXGI_FORMAT_UNKNOWN; descView.Buffer.NumElements = descBuf.ByteWidth / descBuf.StructureByteStride; if(FAILED(m_pd3dDevice->CreateUnorderedAccessView( m_destDataGPUBuffer, &descView, &m_destDataGPUBufferView ))) return false; return true; }
非常类似于shader视图,在本例中是unordere访问视图。
最后一件值得展示的东西是对饱和度效果的完全着色。请记住,这一切都是最理想的,主要是为了实验!
struct Pixel { int colour; }; StructuredBuffer<Pixel> Buffer0 : register(t0); RWStructuredBuffer<Pixel> BufferOut : register(u0); float3 readPixel(int x, int y) { float3 output; uint index = (x + y * 1024); output.x = (float)(((Buffer0[index].colour ) & 0x000000ff) ) / 255.0f; output.y = (float)(((Buffer0[index].colour ) & 0x0000ff00) >> 8 ) / 255.0f; output.z = (float)(((Buffer0[index].colour ) & 0x00ff0000) >> 16) / 255.0f; return output; } void writeToPixel(int x, int y, float3 colour) { uint index = (x + y * 1024); int ired = (int)(clamp(colour.r,0,1) * 255); int igreen = (int)(clamp(colour.g,0,1) * 255) << 8; int iblue = (int)(clamp(colour.b,0,1) * 255) << 16; BufferOut[index].colour = ired + igreen + iblue; } [numthreads(32, 16, 1)] void CSMain( uint3 dispatchThreadID : SV_DispatchThreadID ) { float3 pixel = readPixel(dispatchThreadID.x, dispatchThreadID.y); pixel.rgb = pixel.r * 0.3 + pixel.g * 0.59 + pixel.b * 0.11; writeToPixel(dispatchThreadID.x, dispatchThreadID.y, pixel); }
提供的源代码有更多的功能,主要是读取纹理并将结果返回到屏幕上。由于这与计算着色无关,我在这里没有包含它,但是在代码中有一些注释,
以便您了解正在发生的事情。不过,这都是非常简单和线性的。
源代码用VS2010编译
链接:http://pan.baidu.com/s/1sl0Tc2x 密码:1kr6