赞
踩
熟悉实时图形的人都知道,GPU里面有很大一块是rasterizer,用来把VS输出的顶点数据光栅化成像素数据,交给PS。然而,这部分一直是一个黑盒。GPU是以什么样的顺序进行光栅化?传统光栅化和tile-based光栅化有什么区别?这些问题原先只能进行概念性的定性分析。
到了D3D11 feature level 11.0的时代,硬件支持对一个buffer的内容进行原子操作。也就是说,可以在PS里通过调用InterlockedAdd等方法,来让像素之间串行化执行。这样以来,等于给我们开启了探究光栅化顺序之门。不需要保密资料、不需要硬件知识,写个简单的shader就可以完全从结果上看出光栅化的奥秘。
下面的程序实现到了KlayGE里,一个称为RasterizationOrder的教程。由于还没完成OpenGL和OpenGLES插件里的rw buffer支持,D3D12的插件还有bug,目前只能用D3D11来运行。平台是Windows desktop和UWP。
要完成这件事情的代码非常简单。完全只是利用了Shader Model 5.0里面提供的功能。
- RWByteAddressBuffer ras_order_buff;
- ...
- uint GetRasterizationOrder()
- {
- uint old_value;
- ras_order_buff.InterlockedAdd(0, 1, old_value);
- return old_value;
- }
ras_order_buff是一个大小只有4字节的buffer,初始值为0。InterlockedAdd保证了原子+1,原先buffer里的值就通过old_value返回出来了。在PS里调用这个函数,就能得到自己是第几个到达此处的pixel。
有了这个顺序值之后,就可以把它编码成颜色输出。
- float4 RasterizationOrderPS() : SV_Target
- {
- uint order = GetRasterizationOrder();
- return float4((uint3(order >> uint3(0, 8, 16)) & 0xFF) / 255.0f, 1);
- }
如果你觉得那样出来的颜色看不明白,还可以把它编码成color map输出。这里用的是Matlab里称为Parula的color map。从蓝到黄表示0到1。
Shader代码如下。
- float4 RasterizationOrderColorMapPS() : SV_Target
- {
- uint MAX_VALUE = 256 * 1024;
- uint order = GetRasterizationOrder();
- return color_map.SampleLevel(linear_sampler, float2((order & (MAX_VALUE - 1)) / float(MAX_VALUE), 0.5f), 0);
- }
有了这些PS,我们只要画一个超过屏幕大小的三角形,就能从颜色上探究GPU光栅化的工作方式了。
严格来说,我们看到的并不是光栅化的顺序,而是光栅化后到达PS的顺序。所以这也和调度有关。所以我们这里主要看的是光栅化的pattern,而不是严格的次序关系。
首先登场的是AMD FirePro V3900专业卡 (不要以为我光黑AMD,我也用)。它用的是Cayman核心,和桌面级Radeon HD 69xx相同。把顺序编码成颜色后,看起来是这样的。
确实不容易分辨,我们来看看color map的结果。
这下好多了。从颜色可以看出,FirePro V3900的顺序是从右到左这样倒着来的。以32个pixel的高度为单位,连续光栅化。这是个典型的传统光栅化的工作方式。另一个有意思的地方是,rasterizer仍然把整个三角形一次光栅化了,没有把它进行拆分。
之后我们就只贴color map的结果。
下一个测试的是NVIDIA Quadro 600,Fermi核心,和桌面级GeForce GTX 4xx相同。它的顺序是从左到右,以16个pixel的高度为单位,总的来说是连续光栅化,但有些跳变,表明PS的调度更随机。
再来一个Intel HD Graphics 520的。仍然和前面很相似,以16个pixel的高度为单位从左到右光栅化。
看了两个传统光栅化的GPU,再来看看tile-based的。这里选的是Lumia 950的Qualcomm Adreno 430。可以从结果上很明显地看出来,tile的大小是8x8,tile之内是zigzag的顺序,tile之间并非像传统光栅化那样连续前进。
不光硬件,我们还可以看看同一个程序在软件光栅化上运行会有什么样的结果。
最传统的D3D11 REF上,光栅化是一个像素一个像素,从左到右前进的。中规中矩的软件光栅化实现方法。同时也可以看出,一个大三角形被拆成了两个,分别光栅化。对于GPU来说,拆primitive会涉及到顶点数变化,比较费劲,不如就整个光栅化了,屏幕之外的像素裁掉。反正瓶颈在IO而不是光栅化本身。而软件光栅化正好相反。顶点变化无所谓,光栅化本身的开销大。所以就用了严格的clip,并只光栅化屏幕内的像素。
WARP也一样,拆成两个三角形。然而WARP融合了一定的tile-based做法,是以2x2的小tile为单位,向前推进。所以看起来比较碎。
最神奇的,是NVIDIA Geforce GTX 960上的结果。之前有传言说Maxwell和Pascal的GPU偷偷地用了tile-based。但我们在这里看到的是这样的顺序。
这看起来像是以256像素位宽度,纵向切分成块,再以4x8为tile大小,光栅化块内的区域。所以这并不是tile-based,而是在传统和tile-based之间有个折衷。很可能是为了结合传统的高效和tile-based的节能优势。
通过这些分析,我们可以大致的知道每种硬件的光栅化单位。在写PS的时候,如果分支是这样的单位大小的整倍数,那么分支就是无开销的。不要再认为GPU上的if是把两个分支都执行一遍,取结果了。只有在一定条件下才是那样。
因为我能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。