利用GPGPU方法将大量数据通过纹理传输至GPU进行计算
在显卡并行计算起源的领域,比如说游戏开发领域,GPU的发展使得其迈向了新的纪元,画面愈加精致,效果愈加震撼,同时并行计算框架CUDA和OpenCL的兴起,则将显卡的计算潜能充分地释放了出来。最后令人高兴的是,以前就受惠于显卡发展的游戏开发领域,将显卡的功能做了新的扩展,以前只用来渲染用的显卡,目前可以做更多逻辑领域相关的事儿了。
原创文章,反对未声明的引用。原博客地址:http://blog.csdn.net/gamesdev/article/details/19507325
源代码下载地址:这里
目前在游戏领域尽管有CUDA和OpenCL这些框架助力GPU并行程序开发,但对于游戏开发群体来说,使用传统的GPGPU方法的好处是GPGPU似乎是与生俱来的,并不需要依赖任何SDK就能用基本的C语言开发在GPU上运行的程序;此外游戏开发者大多数都对计算机图形学十分了解,对于GPGPU中所提的一些图形学概念也不会陌生;最后,使用GPGPU方法让GPU进行通用计算,考验人的能力,人们对它的痴迷程度不亚于用Lua的API实现面向对象编程。
下面我将使用Qt和OpenGL来做一次GPGPU的尝试。想做GPGPU计算,需要显卡支持至少OpenGL2.0版本,因为这些版本能够支持着色器,这是一种针对渲染而研制的小程序,我们要利用它来进行通用计算。
首先需要考虑,数据如何传入着色器程序?熟悉的同行们都知道,使用attribute和uniform都可以将数据传入。没错,这些都可以满足需求,但是这些支持都是有限制的,比如说vertex的attribute要通过VBO传入,而且在着色器中只有一组attribute是可见的,我们无法获取下一组的attribute;另外系统对uniform的个数都有所限制,比如OpenGLES2中只支持128个vec4的入口,对于大数据的计算来说实在是有点儿少。于是我们只能将希望寄托在纹理身上了。
纹理是一组数据,它既可以贮存在内存中也可以贮存在显存中。在OpenGL的客户端(CPU)中对纹理的操作是通过纹理对象来实现的,在OpenGL的服务端(GPU)中对纹理的操作则是通过一套texture函数来进行采样,最终写入帧缓存(Framebuffer)。OpenGL可以支持宽和高都很大的纹理,基本上纹理的像素格式是R8G8B8A8,即32位一像素,32位实在是太好了,因为它可和float相互交换,假设我想将一float型的数组传入着色器,那么将不会带来太大的麻烦。
程序使用的是Qt对于OpenGL封装的一些类,有QOpenGLBuffer、QOpenGLShaderProgram、QOpenGLTexture。因为使用OpenGL的API是在是有些麻烦,Qt又是一个非常出色的跨平台框架,Qt5.2以后对OpenGL的封装已经很好了,因此这回我将采用Qt的相关类进行实现。
我们这个演示程序要达到一个怎样的效果?我们这个程序是这样的:将一个4×1的纹理传至着色器,纹理内容表示一红、绿蓝和alpha的浮点值,这些值将被片断着色器中使用,绘制出矩形的颜色。
代码较长,我还是将其中的核心部分展示出来吧:
/*---------------------------------------------------------------------------*/ void GLWindow::InitGL( void ) { float vertices[] = { 0.2f, 0.2f, 0.0f, 1.0f, 0.2f, 0.8f, 0.0f, 1.0f, 0.8f, 0.2f, 0.0f, 1.0f, 0.8f, 0.8f, 0.0f, 1.0f }; m_Colors.append( 0.8f ); m_Colors.append( 0.5f ); m_Colors.append( 0.2f ); m_Colors.append( 0.9f ); m_Texture.setMagnificationFilter( QOpenGLTexture::Nearest ); m_Texture.setMinificationFilter( QOpenGLTexture::Nearest ); m_Texture.setSize( 4, 1 ); m_Texture.setFormat( QOpenGLTexture::LuminanceFormat ); m_Texture.allocateStorage( ); m_Texture.setData( QOpenGLTexture::Luminance, QOpenGLTexture::Float32, m_Colors.data( ) ); m_VertexBuffer.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_VertexBuffer.create( ); m_VertexBuffer.bind( ); m_VertexBuffer.allocate( vertices, sizeof( vertices ) ); m_ShaderProgram.addShaderFromSourceFile( QOpenGLShader::Vertex, ":/Texture.vsh" ); m_ShaderProgram.addShaderFromSourceFile( QOpenGLShader::Fragment, ":/Texture.fsh" ); m_ShaderProgram.link( ); } /*---------------------------------------------------------------------------*/ void GLWindow::ResizeGL( int width, int height ) { glViewport( 0, 0, width, height ); } /*---------------------------------------------------------------------------*/ void GLWindow::RenderGL( void ) { glClearColor( 0.2f, 0.5f, 0.8f, 1.0f ); glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); m_ShaderProgram.bind( ); m_VertexBuffer.bind( ); int positionLoc = m_ShaderProgram.attributeLocation( "position" ); m_ShaderProgram.enableAttributeArray( positionLoc ); m_ShaderProgram.setAttributeBuffer( "position", GL_FLOAT, 0, 4 ); glVertexAttribPointer( positionLoc, // 属性在着色器中的位置 4, // 属性的个数 GL_FLOAT, // 属性的类型 GL_FALSE, // 属性是否被单位化 0, // 跨度(又称迈) Q_NULLPTR ); // 属性指针(如果绑定VBO,那么此项为空) glActiveTexture( GL_TEXTURE0 ); m_Texture.bind( ); m_ShaderProgram.setUniformValue( "texID", 0 ); m_ShaderProgram.setUniformValue( "texSize", m_Colors.size( ) ); glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 );// 绘图 m_Texture.release( ); m_VertexBuffer.release( ); m_ShaderProgram.release( ); } /*---------------------------------------------------------------------------*/
需要注意的是,程序需要在Qt5.2中才能编译通过,因为QOpenGLTexture这个类是Qt5.2才引进的。
这里需要注意的是,m_Texture.setMagnificationFilter(QOpenGLTexture::Nearest );和m_Texture.setMinificationFilter(QOpenGLTexture::Nearest );设定了纹理滤波,我们需要最近点采样的滤波,否则会出现纹理的值因为滤波而取得错误的值进行计算的问题。此外,考虑到OpenGL ES2不支持浮点纹理(它的扩展能支持半浮点纹理,不在我们考虑范围内),因此最适合的内部格式(internal format)应该只有GL_LUMINANCE了,所以有m_Texture.setFormat(QOpenGLTexture::LuminanceFormat );这条语句。随后m_Texture.setData( )函数的参数如上所示,经测试发现通过上述做法才能保证浮点值能够正确地被传至着色器中。
随后我们看看片断着色器,因为颜色的指定是在这儿进行的。片断着色器的内容如下:
uniform sampler2D texID; uniform int texSize; void main( void ) { float color[4]; float texStep = 0.0; for ( int i = 0; i < texSize; ++i ) { texStep = float( i ) / float( texSize ); color[i] = texture2D( texID, vec2( texStep, 0.0 ) ).r; } gl_FragColor = vec4( color[0], color[1], color[2], 1.0 ); }
OpenGL的内部格式GL_LUMINANCE,它的纹理元(texel)是这样的:luminance,luminance, luminance, 1.0,也就是说想要取出我们设定的浮点值,使用r、g、b分量都可以。通过这种手段,我们设定的纹理内容从客户端的m_Colors顺利地传入了color[4]中了。
下面是演示程序的运行截图:
矩形的颜色是橙色(0.8,0.5,0.2,0.9),就是m_Colors所表示的颜色。
演示程序并不是很完善,但是它证明使用GPGPU方法通过纹理将大量数据传入着色器进行通用计算是可能的。当然,我们讨论的只是GPGPU方法的一部分,完整的GPGPU方法是将片断着色器当作核,通过将纹理绑定在帧缓存上的方法将片断数据保存在纹理中(也称渲染到纹理,RTT)。如需进行多次迭代计算,可以使用“乒乓技术”将纹理来回写出和载入,从而达到高效运行的目的。