要实现OpenGL和CUDA交互,最常用便捷的方式就是,在OpenGL中创建缓冲对象,将其注册并绑定到一个内存指针,将这个指针传入CUDA核函数中进行读写。关于这点,可以参考笔者之前的文章------基于C++与CUDA的N卡GPU并行程序——OpenGL图形互操作性。
// 创建窗口缓冲
int c=1;
char *dummy;
glutInit( &c, &dummy );
glutInitDisplayMode( GLUT_SINGLE | GLUT_RGBA );
glutInitWindowSize( width, height );
glutCreateWindow( "bitmap" );
// 创建缓冲对象
GLuint bufferObj;
cudaGraphicsResource *resource;
glGenBuffers( 1, &bufferObj );
glBindBuffer( GL_PIXEL_UNPACK_BUFFER_ARB, bufferObj );
glBufferData( GL_PIXEL_UNPACK_BUFFER_ARB, width * height * 4,
NULL, GL_DYNAMIC_DRAW_ARB );
cudaGraphicsGLRegisterBuffer( &resource, bufferObj, cudaGraphicsMapFlagsWriteDiscard );
// 每次循环,调用idle_func()函数,使用CUDA核函数计算
// static method used for glut callbacks
static void idle_func( void ) {
GPUAnimBitmap* bitmap = *(get_bitmap_ptr());
bitmap->ticks++;
int time = glutGet(GLUT_ELAPSED_TIME);
if(time - bitmap->timebase > 1000){
printf("帧率为: %4.2f\n", float(bitmap->ticks) * 1000.0 / float(time - bitmap->timebase));
bitmap->timebase = time;
bitmap->ticks = 1;
}
uchar4* devPtr;
size_t size;
// 将缓冲对象绑定到内存指针
cudaGraphicsMapResources( 1, &(bitmap->resource), NULL );
cudaGraphicsResourceGetMappedPointer( (void**)&devPtr, &size, bitmap->resource);
// 调用CUDA核函数
bitmap->fAnim( devPtr, bitmap->dataBlock, bitmap->render);//-------------------------
// 解除缓冲对象
cudaGraphicsUnmapResources( 1, &(bitmap->resource), NULL );
glutPostRedisplay();
}
// 每次循环,调用Draw()函数,绘制画面
// static method used for glut callbacks
static void Draw( void ) {
GPUAnimBitmap* bitmap = *(get_bitmap_ptr());
glDrawPixels( bitmap->width, bitmap->height, GL_RGBA,
GL_UNSIGNED_BYTE, 0 );
glFlush();
}
// 绘制主函数
void anim_and_exit( void (*f)(uchar4*,void*,void*), void(*e)(void*) ) {//----------------
GPUAnimBitmap** bitmap = get_bitmap_ptr();
*bitmap = this;
fAnim = f;
animExit = e;
glutDisplayFunc( Draw );
glutIdleFunc( idle_func );
glutMainLoop();
}
在这个链接的文章中,CUDA内核计算的结果会写到缓冲对象中,再使用glDrawPixels()绘制到屏幕上。但是这个glDrawPixels()函数有点太慢了。
glDrawPixels()函数太慢了,一般都不太会用了,可以使用速度更快的glTextureSubImage2D()函数。这里不再是创建缓冲对象,而是创建一个纹理缓冲,然后在着色器shader中,把这个纹理缓冲的内容,贴到屏幕上进行绘制。首先在主机内存上开辟空间,将纹理贴图的图片数据,保存到这个内存上,然后把这个纹理数据传到OpenGL的纹理缓冲上
// 主机内存上保存纹理贴图
uchar4 *textureImage = (uchar4*)malloc(width*height*sizeof(uchar4));
for(int i = 0; i < height; i++){
for(int j = 0; j < width/2; j++){
textureImage[i*width+j].x = (unsigned char)(100);
textureImage[i*width+j].y = (unsigned char)(1);
textureImage[i*width+j].z = (unsigned char)(0);
}
}
// 创建纹理缓冲,绑定纹理
glGenTextures(1, &texture);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
GLsizei imageWidth = width, imageHeight = height;
glTexImage2D(GL_TEXTURE_2D, 0, 4, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureImage);
glEnable(GL_TEXTURE_2D);//启用纹理贴图
glGenerateTextureMipmap(texture);
// 每次循环,调用Draw()函数,绘制画面
// static method used for glut callbacks
static void Draw( void ) {
GPUAnimBitmap* bitmap = *(get_bitmap_ptr());
glTextureSubImage2D(bitmap->texture, 0, 0, 0, bitmap->width, bitmap->height, GL_RGBA8, GL_UNSIGNED_BYTE, bitmap->textureImage);
// 绘制一个四边形片段,四边形正好就是这个这个屏幕上的边框
// 使用glTexCoord2f()定义纹理贴图的坐标
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f);
glVertex2f(-1.0f,-1.0f);
glTexCoord2f(1.0f,0.0f);
glVertex2f(1.0f,-1.0f);
glTexCoord2f(1.0f,1.0f);
glVertex2f(1.0f,1.0f);
glTexCoord2f(0.0f,1.0f);
glVertex2f(-1.0f,1.0f);
glEnd();
glFlush();
}
在Draw()中绘制一个正好覆盖到屏幕边框的四边形,使用glTexCoord2f()定义纹理贴图的坐标,这样就可以把这张纹理贴图绘制到窗口。
以上介绍的是使用glTextureSubImage2D()函数,将主机内存的图像数据,传输到纹理缓冲。但是从CPU主机内存传输数据到GPU显存上,需要使用DMA引擎经过PCIE总线进行访问,这个速度显然时比较慢的。而且CPU主机内存上的数据,无法直接用CUDA核函数进行计算和交互。
所以真正实用的一种方式,是将上面所述的两种方式结合起来,实现OpenGL和CUDA的快速交互。先使用OpenGL在显存上创建一个缓冲对象,将这个缓冲对象注册绑定到内存指针,之后CUDA核函数就可以对这个缓冲对象的内存进行读写交互。最后创建绑定一个纹理缓冲,使用glTextureSubImage2D()函数,将显存上的缓冲对象数据传输到纹理缓冲中,再显示到屏幕上。因为需要传输的数据本身就位于显卡上,不需要从CPU传过去,速度就更快了,而且可以直接使用CUDA核函数进行计算。
// 创建缓冲对象
GLuint bufferObj;
cudaGraphicsResource *resource;
glGenBuffers( 1, &bufferObj );
glBindBuffer( GL_PIXEL_UNPACK_BUFFER_ARB, bufferObj );
glBufferData( GL_PIXEL_UNPACK_BUFFER_ARB, width * height * 4,
NULL, GL_DYNAMIC_DRAW_ARB );
cudaGraphicsGLRegisterBuffer( &resource, bufferObj, cudaGraphicsMapFlagsWriteDiscard );
// 每次循环,调用Draw()函数,绘制画面
// static method used for glut callbacks
static void Draw( void ) {
GPUAnimBitmap* bitmap = *(get_bitmap_ptr());
glTextureSubImage2D(bitmap->texture, 0, 0, 0, bitmap->width, bitmap->height, GL_RGBA8, GL_UNSIGNED_BYTE, NULL);
// 绘制一个四边形片段,四边形正好就是这个这个屏幕上的边框
// 使用glTexCoord2f()定义纹理贴图的坐标
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f);
glVertex2f(-1.0f,-1.0f);
glTexCoord2f(1.0f,0.0f);
glVertex2f(1.0f,-1.0f);
glTexCoord2f(1.0f,1.0f);
glVertex2f(1.0f,1.0f);
glTexCoord2f(0.0f,1.0f);
glVertex2f(-1.0f,1.0f);
glEnd();
glFlush();
}
glTextureSubImage2D()的data参数可以通过两种方式来解释。第一种方式是通过用户程序中存储的自然数据指针解释。第二种data数据的解释方式,则是通过绑定到GL_PIXEL_UNPACK_BUFFER目标的缓存对象来完成,作为缓存对象的偏移位置。用户程序此时可以将数据存储到缓存对象当中,然后再传递到纹理对象里。如果GL_PIXEL_UNPACK_BUFFER目标没有绑定任何缓存对象,那么data会被解释成一个本地指针,如果绑定了缓存,那么data会被解释成缓存中的一个偏移位置。
使用缓存对象来存储纹理数据的一个主要的好处在于:数据不是立即从缓存对象向纹理进行传输的,而是在着色器请求数据的时候才会执行这一操作。因此应用程序的运行和数据的传输操作可以并行进行。如果数据在应用程序本地内存中,那么glTextureSubImage2D()需要先对数据进行拷贝,然后函数才会返回,这是不可能并行完成的。不过这种方法的好处在于:应用程序在函数返回之后依然可以自由修改之前传输的data数据。
除了上述的方法之外,另一种方式是使用帧缓存对象FBO的一个缓存附件——渲染缓存,使用glBlitFramebuffer()来完成。
相关资料有
网址1
网址2
网址3
网址4
网址5