DirectX 11 学习笔记-07

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Zealot_Alie/article/details/82289832

Computer Shader 

使用GPU进行非图形应用被称为general purpose GPU (GPGPU) programming。并非所有的算法都适用于GPU计算,由于GPU的高并行构架,我们需要大量的数据元素使用相似操作并能对它们进行并行操作,向对像素片段着色器所做的那样。粒子系统也可以作为GPU计算的粒子,当粒子之间可以独立计算时,使用GPU进行并行计算。 

对GPGPU编程,通常来说我们需要获取GPU的计算结果并返回到CPU中。这需要拷贝显存中的数据到系统内存。在01中我们讨论了这方面的性能问题,通常来说这个拷贝操作是需要较多时间的,但如果和计算所花费的时间相比较能获得较大的提升,那拷贝带来的损失是完全可以接受的。 

对图形学上的一些应用,我们可以直接将计算结果作为管线的输入,因此不需要GPU到CPU的拷贝。一个典型的例子是模糊的计算,通过compute shader计算模糊后的纹理,然后将纹理作为管线的输入显示在屏幕中。 

Compute shader并不是Direct3D渲染管线的直接组成部分。它可以让我们访问GPU并实现数据并行的算法而不需要绘制任何东西。如上面所提到的,这对GPGPU很有用,但同样有很多图形学效果使用compute shader来实现。因此Compute shader对图形学程序员来说也是关联十分紧密的。 

Threads and Thread groups 

在GUP编程中,需要执行的线程被分割为一系列线程组。每一个线程组在同一个微处理器中执行。也就是说,如果GPU有16个微处理器,那么你应该把你的问题分成至少16个线程组,这样每个微处理器都能有工作做。为获取更好的性能,你可以让至少两个线程在一个微处理器上执行,因为微处理器可以在线程组中切换当某个线程停转时(可能是在等待一个纹理操作)。 

每个线程组内的线程可以共享一块共享内存。一个线程无法访问其他线程组的共享内存。同一个组内的线程可以进行同步操作,但不同组内的线程无法同步。实际上,我们无法控制不同线程组之间的运行顺序,因为不同线程组运行在不同的微处理器上。 

一个线程组包含n个线程。实际上硬件会把这些线程分为数个warps(每个warp包含32个线程),微处理器按照SIMD32(同时执行32个线程中的相同指令)方式处理warp。每个CUDA核心处理一个线程,一个“Fermi”构架的微处理器有32个CUDA核心。在D3D中,你可以指定非32倍数的n值,但考虑到性能,线程组的维度最好是warp尺寸的整数倍,256是一个比较常用的值。你可以试验不同的值然后选择最合适的值。 

COMPUTE SHADER的一般结构 

一个compute shader一般由以下几个部分组成 

1.全局值例如constant buffers. 

2.输入输出资源 

3.[numthreads(X, Y, Z)] 属性, 指明了一个线程组内的线程数 

4.在每个线程中要执行的着色器函数主体。 

5.线程Id 

数据输入与输出资源 

两种类型的资源能作为compute shader的输入:缓冲区和纹理。 

compute shader的输出比较特别,输出类型有一个特殊的前缀“RW”,表示为可读且可写。相反的,一般的纹理是只读的。另外需要通过模板参数指明输出类型。例如: 

RWTexture2D<float4> gOutput; 

RWTexture2D<int2> gOutput; 

绑定一个资源到compute shader 的输出需要使用一个新的视图类型unordered access view (UAV),在C++中由ID3D11UnorderedAccessView表示。创建的过程与一般资源和资源视图的创建过程类似,但需要用D3D11_BIND_UNORDERED_ACCESS指明这个资源会被用作一个UAV。一但创建完成,我们可以使用SetUnorderedAccessView来把资源视图设置到shader中。 

线程ID系统 

  1. Group ID 系统为每个线程组分配一个唯一的group ID,使用语义SV_GroupID表示。线程组是一个三维数组,所以如果线程组数量为Gx*Gy*Gz,则group ID范围是从(0,0,0)到(Gx-1,Gy-1,Gz-1)。 
  2. 2.线程组内的每个线程同样被分配了一个组内的ID,在shader中用SV_GroupThreadID。同样如果一个组的尺寸是X*Y*Z那么group thread ID的范围是(0,0,0)到(X-1,Y-1,Z-1)。 
  3. 3.每一次Dispatch都会对所有的线程生成一个唯一ID称为dispatch thread ID。它使用系统语义SV_DispatchThreadID。可以由组ID和组内ID推出Dispatch ID,公式如下 
    dispatchThreadID.xyz = groupID.xyz * ThreadGroupSize.xyz + groupThreadID.xyz; 
  4. 此外还有一个线性的线程组索引SV_GroupIndex, 
groupIndex = groupThreadID.z*ThreadGroupSize.x*ThreadGroupSize.y +groupThreadID.y*ThreadGroupSize. 

线程ID是并行算法的重要工具,因为我们在拆分问题之后需要把问题分配到各个线程中,使用线程ID来划分每个线程的工作范围,例如不同的线程ID对应处理图片上不同的纹理像素,从而达到并行的效果。 

纹理索引和采样 

通常我们可以使用sample函数来对纹理进行采样,但在Compute Shader中,由于不是直接用于渲染图形,采样函数无法自动选择最合适的mipmap,因此在Compute Shader中,我们应该使用SmapleLevel指定我们需要的mipmap等级并进行采样。 

由于我们并行计算中使用的线程id不是标准的uv坐标,所以在取样前应该向把id换算成uv坐标,即 

u=x/width, v=y/height. 

另外可以直接将纹理理解为数组,不使用采样而是直接通过坐标方式来索引纹理数据。 

结构化buffer资源 

StructuredBuffer<T>可以在HLSL中定义自定义结构的资源缓冲。 

在C++端,需要创建缓冲资源。类似一般的缓冲区,不过这里应该要指定StructureByteStride,即结构体的尺寸。同样的,要将资源绑定到管线中时需要为资源创建资源视图。 

比较不同的是,创建资源视图时使用的格式是DXGI_FORMAT_UNKNOWN因为自定义结构体对Direct11是未知的。 

拷贝CS结果到Memory 

使用CopyResource将资源拷贝到一个staging的资源中,然后再使用map的方式来获取staging资源中的结果。 

ConsumeStructuredBuffer和AppendStructuredBuffer

是可以被减少(consume)添加(append)的buffer。每个线程中consume的顺序都是不同的,一个元素只能被一个线程consume。另外Appendbuffer的尺寸是不会自动增加的,所以要保证buffer的大小足够大。 

共享内存和同步 

线程组能被分配一块共享内存或者叫做线程本地内存。访问这块内存的速度是非常快的。在compute shader中。可以声明为 

groupshared float4 gCache[256]; 

尺寸可以是任意的,但组共享内存的最大尺寸是32kb。使用过多的共享内存会引起性能问题。假设一个微处理器支持32kb的共享内存,你的compute shader要求一个20kb的共享内存。这意味着只能有一个线程组能够在微处理器中得到满足因为没有足够的内存空间给另一个线程组使用。这会影响到GPU的并行性,因为这种情况下微处理器无法在线程组中切换来避免延迟。因此尽管硬件支持32kb共享内存,但为性能考虑还是应该尽量减少共享内存尺寸。 

共享内存的一个常见应用是用来存储需要使用的纹理值。例如模糊算法中,需要从纹理中多次获取同一个图素。纹理采样是GPU中一个较慢的操作,因为GPU的存储带宽和延迟并没有提高很多。通过预先获取需要的纹理元素并存放在共享内存数组中,线程组可以避免冗余的纹理获取操作。之后算法再从共享内存中查找纹理元素,而这个操作是非常快的。 

猜你喜欢

转载自blog.csdn.net/Zealot_Alie/article/details/82289832