Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

Beware of GPU memory bandwidth    

仅供个人学习使用,请勿转载,勿用于任何商业用途。

        前段时间,写了一系列Post-Process effect,包括screen spance的motion blur, 折射,散射等等。大部分shader都非常简单,无非是把一个full screen quad渲染到屏幕,通常不超过10行ps代码,不含任何分支和循环指令,只需要sm1.4就可以运行。可是当在Geforce 7300GT上,同时打开多个post process效果做测试时,发现fps掉的非常厉害。检查了所有代码,确保已经进行了最大优化,但问题依然存在。最后得出结论是fill rate瓶颈,毕竟所有人在讲解post process技术时,都会提到填充率是最大的瓶颈。

     为了测试在60fps下可以使用多少post effect,我新建了一个空白项目,查看7300GT在1024 * 768的分辨率下,可以渲染多少个带纹理的full screen quad:

//c# code 
protected override void Draw(GameTime gameTime) 

    GraphicsDevice.Clear(Color.Black); 

    effect.Begin(); 
    effect.CurrentTechnique.Passes[
0].Begin(); 
    
for (int i = 0; i < maxQuadCount; i++
        quad.Draw(); 
    effect.CurrentTechnique.Passes[
0].End(); 
    effect.End(); 
    
base.Draw(gameTime); 


//shader 
float2 screenParam; 
texture sourceTexture; 

sampler sourceSpl:register(s0) 
= sampler_state 

    Texture 
= <sourceTexture>
    MinFilter 
= None; 
    MagFilter 
= Linear; 
    MipFilter 
= Linear; 
    AddressU 
= clamp; 
    AddressV 
= clamp; 
}; 

void FullScreenQuadVS(float3 iPosition:POSITION, 
    
out float4 oPosition:POSITION, 
    inout float2 texCoord:TEXCOORD0) 

    oPosition 
= float4(iPosition,1); 
    texCoord 
+= 0.5 / screenParam; 


float4 FilmScratchPS(float2 texCoord:TEXCOORD0):COLOR 

    float3 color 
= tex2D(sourceSpl,texCoord).xyz; 
    
return float4(color,0.02); 


technique Default 

    pass P0 
    { 
        VertexShader 
= compile vs_1_1 FullScreenQuadVS(); 
        PixelShader 
= compile ps_2_0 FilmScratchPS(); 
    } 


      结果大大出乎意料,当数量超过20个,fps就低于60了。1024*768的分辨率下,每帧需要填充786432个像素,每秒60帧,每帧20次渲染,可以算出当前填充率大约是0.943 billion pixel/sec. 但7300GT的填充率是2.8 billion pixel/sec !!!. 当然,标准规格仅仅是一个理论值,可是结果相差的也太大了,仅仅是标准的1/3!

     瓶颈究竟是在哪里呢?是硬件实际性能缩水的一塌糊涂,还是XNA内部代码造成的?我觉得后者的可能性比较大,于是在Creator Club发帖询问,后来有人告诉我,这种情况多半是带宽瓶颈。

     那么究竟是不是呢?来计算一下上面的代码使用了多少带宽:每渲染一个像素时,需要读取一次RGBA纹理,4byte,把颜色写入backbuffer,4byte,一次深度读取和写入,2 * 4byte, 总共是16byte。每秒60帧,每帧20次渲染,大约是15Gb/s。再看7300GT的带宽参数,10.7GB/s。显然,仅仅从数据上来说,确实是带宽瓶颈。不过你肯定也注意到了,实际性能怎么可能比理论值高那么多呢?此时我也不确定究竟是不是带宽瓶颈,于是把纹理从原来的纹理(1024*768)改为了一张2*2的纹理。再次测试fps马上有了很大提高,至此,最终确信是带宽瓶颈。 

     你可能奇怪,为什么这样就能确定是带宽的问题,如何解释实际值比理论值还高呢?这就要从GPU的硬件构架上来说了。在一般的广告或者资料中,通常只会看到介绍显卡有多大显存,实际上,除显存以外,GPU和CPU一样,在芯片上还有一块高速的缓存,只不过这块缓存通常只有几KB。当GPU缓存命中失败时,才会访问显存,产生带宽流量。对于2*2的纹理来说,最多只有16byte,总是能放在缓存中。通过减少带宽,提高了fps,这足以说明之前遇到的瓶颈确实是带宽了,也在一定程度上解释了为什么我们的计算结果会比标准参数高那么多。 

     如果你和我一样,想到对于full screen quad来说,可以关闭深度读写来提高性能,那可能会发现另一个很有趣的问题:关闭深度读写几乎没有什么性能提升!在8800GT上的,关闭和打开深度读写,性能仅相差10fps而已。问题出在哪里?是我们计算带宽的方法不对,还是前面的推理完全是错误的?对于这个问题,我并没有确切的答案,不过还是找到了一些可以对此进行解释的资料。对GPU来说,由于要频繁读写color buffer和depth buffer,它们实际上是以一种压缩的格式存在于显存中,并且由硬件支持进行非常快速的压缩和解压,具体的算法和原理非常复杂,如果感兴趣可以去google一下。这里可以打一个简单的比喻来说明为什么压缩可以提高效率减少带宽: 如果把1024*1024的buffer压缩为64*64的buffer,当向这块buffer中写入的数据都一样时,比如深度都为1,那么不必依次像1024*1024个像素中写入数据,而只需要把64*64的数据块都标记为1就可以了(这也正是为什么clear()在现代显卡上非常快的原因)。我们遇到的情况刚好属于理想状态,组成quad的两个三角形都在深度相同的平面上,而且覆盖了整个屏幕,这就让深度读写变的异常迅速,也从另一方面解释了前面提到的实际带宽比理论计算要低。

     至此,得出3个结论:
1,60fps, 1024*768的分辨率下,渲染20个带纹理的full screen quad就已经达到了7300GT的极限(虽然硬件做了众多优化减少带宽,这个数字仍然很让人失望).
2, 像素填充率是一个非常不可靠的参数, 不但受到顶点处理能力的影响( 当然,由于DX10采用了统一构架所以没有这个问题), 还受到带宽,采样延迟等很多因素的限制. 对现代游戏来说,几乎所有几何体都是带纹理的, 所以在遇到填充率瓶颈以前, 可能早就受制于带宽瓶颈了.
3. 实际带宽虽然可能比理论计算值低,但仍然非常容易出现瓶颈,特别是在底端显卡上。虽然显卡的计算能力每年都在飞速发展,带宽的增加却相对非常缓慢:Geforce 6800 Ultra 35.3G/s, 7950GT 44.8G/s, 8800GTX 86.4G/s,9800GTX 70.4G/s+(是的,你没看错,9800的带宽居然下降了), 280GTX 141G/s. 这里只列出了Nv每个系列最顶级的显卡带宽,实际上中低端产品带宽还要低很多。

     在实际渲染中,计算带宽要比上面的full screen quad复杂很多,以下所有操作都会带来数据流量:
1. 向backbuffer中写入数据
2. 打开alpha blend需要读取back buffer数据
3. 浮点纹理将使数据量翻倍
4. 读写深度/模板缓冲
5. 读取纹理(next gen游戏通常需要使用大量纹理来渲染物体,是最大的带宽消费者)
6. 打开Trilinear mipmapping过滤,每次采样可能需要读取8次纹理
7. 顶点数据(上面的讨论中我们都忽略了顶点,实际上,顶点也将占用大量带宽)

      不要忘了,实际情况下,同一个像素非常有可能被渲染多次(我们并不能保证总是从前向后渲染),轻易就突破数十G的流量。此外还要知道带宽效率永远也不可能达到100%,大概只有标准参数的80%左右。

      当然,也有一些简单的方法可以减少带宽使用率:
1. 使用mipmap,使用硬件支持的压缩格式,比如DTX。这是最简单,也是最容易的方法。不要被前面的第6条迷惑了,对3D渲染来说,mipmap通常可以减少每次提交的纹理大小,而压缩格式可以获得最好的缓存,也许2次读取就获得了所需要的数据。
2. 尽可能关闭深度读写和alpha blend。
3. 尽可能从前到后渲染。
4.  进行额外的depth pass,充分利用early-z删除不可见像素。
5. 分离顶点数据。比如把顶点位置和法线,纹理坐标放到不同的stream中,在渲染shadow map时,只使用包含位置的stream。

 

posted on 2009-05-17 21:16  clayman  阅读(1649)  评论(1编辑  收藏  举报