8.3_基于图形互操作性的GPU波纹示例

8.3 基于图形互操作性的GPU波纹示例

在8.2节中,我们多次提到了第5章的GPU波纹示例。在第5章的示例程序中创建了一个CPUAnimBitmap并将其传递给一个函数,每当需要生成一帧图像时都会调用这个函数。

int main(void) {  
    DataBlock data;  
    CPUAnimBitmap bitmap(DIM, DIM, &data);  
    data.bitmap = &bitmap;  
    HANDLE_ERROR(cudaMalloc((void**)&data.dev_bitmap, bitmap(image_size())));  
    bitmap anim_and_exit((void*)(void*, int)) generate_frame, (void*)(void*))cleanup());  
}

根据在前面章节中学到的技术,我们希望创建一个GPUAnimBitmap结构。这个结构的作用与CPUAnimBitmap相同,但在GPUAnimBitmap中,CUDA和OpenGL组件能够不需要CPU的介入而实现相互协作。当在应用程序中使用GPUAnimBitmap()时,main()函数将变得更加简单,如下所示:

int main(void) {
    GPUAnimBitmap bitmap(DIM, DIM, NULL);
    bitmap anim_and_exit(
        (void (*)(uchar4*, void*, int)) generate_frame, NULL);
}

GPUAnimBitmap使用了在8.2一节中介绍的相同函数调用。然而,现在这些调用将被放入GPUAnimBitmap结构中,从而使示例(或者包括你自己的应用程序)变得更为简洁。

8.3.1 GPUAnimBitmap结构

在GPUAnimBitmpa结构中有几个数据成员与8.2节中介绍的内容十分相似。

struct GPUAnimBitmap {
    GLuint bufferObj;
    CUDAGraphicsResource *resource;
    int width, height;
    void *dataBlock;
    void (*fAnim)(uchar4*, void*, int);
    void (*animExit)(void*);
    void (*clickDrag)(void*, int, int, int, int);
    int dragStartX, dragStartY;

我们知道OpenGL和CUDA运行时对于GPU缓冲区分别有着不同的名字,并且还知道在调用OpenGL或CUDA C时需要使用不同的名字。因此,在结构中同时保存了OpenGL的名字bufferObj和CUDA运行时名字的resource。此外,由于代码计算的是一张将要显示的位图图像,因此这张图像需要包含宽度和高度。

为了在使用GPUAnimBitmap时可以注册特定的回调事件,我们还保存了一个void*指针dataBlock指向用户的数据。然而,在GPUAnimBitmap中将不会访问这个数据,而只是将其传递给已注册的回调函数。用户注册的回调函数将保存在fAnim、animExit以及clickDrag中。在每次调用glutIdleFunc()时都将调用fAnim(),这个函数将负责生成在动画中绘制的图像数据。在动画退出时,将调用函数animExit()一次。用户应该在这个函数中实现当动画结束时需要执行的清理代码。最后还有一个可选函数clickDrag(),这个函数将响应用户的鼠标点击/拖曳等事件。如果用户注册了这个函数,那么每当按下、拖曳或者释放鼠标时,都将调用该函数。鼠标点击的初始位置保存在(dragStartX, dragStartY)中,当释放鼠标时,点击或拖曳等事件的起始位置和结束位置就会传递给用户。这可以用于实现令人印象深刻的交互式动画。

在初始化GPUAnimBitmap之后紧接着是在前面示例中相同的代码。在将参数保存到相应的结构成员中后,首先向CUDA运行时查询CUDA设备。

GPUAnimBitmap( int w, int h, void *d ) { width = w; height = h; dataBlock = d; clickDrag = NULL; // 首先找到一个CUDA设备并将其设置到图形互操作中udaDeviceProp prop; int dev; memset( &prop, 0, sizeof(udaDeviceProp )); prop major = 1; prop.minor = 0; HANDLE_ERROR(udaChooseDevice( &dev, &prop ));

在找到一个兼容的CUDA设备后,调用CUDA运行时中的CUDASetGLDevice(),并通知运行时将dev作为与OpenGL互操作的设备:

CUDASetGLDevice( dev );

由于接下来将使用GLUT来创建一个窗口绘制环境,因此需要初始化GLUT。然而,在这个过程中存在一个问题,因为glutInit()希望将命令行参数传递给窗口系统。由于我们不需要传递任何参数,因此可以指定0个命令行参数。然而,在某些版本的GLUT中存在一个错误,即当传递0个参数时应用程序会崩溃。因此,我们要欺骗GLUT,使其认为正在传递一个参数,这样就不会使程序崩溃。

int c=1;  
char *foo = "name";  
glutInit(&c, &foo);

接下来,继续像前面示例中一样初始化GLUT。我们创建了一个要绘制的窗口,并将窗口

的标题指定为字符串“ bitmap”。当然,你也可以选择其他的名字作为窗口标题。

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);  
glutInitWindowSize(width, height);  
glutCreateWindow(" bitmap");

接下来,我们请求OpenGL驱动程序分配一个缓冲区句柄,并将其绑定到GL_PIXL_UNPACK_buffer_ARB,从而确保之后在调用glDrawPixels()时将绘制互操作缓冲区:

glGenBuffers(1, &bufferObj);  
glBindBuffer(GL_PIXL_UNPACK_buffer_arB, bufferObj);

最后,我们请求OpenGL驱动程序分配一块GPU内存。在完成分配操作后,将这个缓冲区通知给CUDA运行时,并通过cudaGraphicsGLRegisterBuffer()来注册bufferObj从而获得一个CUDA C名字。

glBufferData(GL_PIXUNPACK_BUFFERARB, width \* height \* 4, NULL, GL_DYNAMIC_DRAWARB);   
HANDLE_ERROR(udaGraphicsGLRegisterBuffer( &resource, bufferObj,udaGraphicsMapFlagsNone));

在设置好GPUAnimBitmap后,还剩下一个问题就是如何执行绘制操作。绘制操作的主要工作是在函数glutIdleFunction()中完成的。这个函数要做三件事情。首先,它将映射共享缓冲区并获得指向该缓冲区的GPU指针。

// 作为GLUT回调函数的静态方法

static void idle_func(void) {
    static int ticks = 1;
    GPUAnimBitmap* bitmap = *(get Bitmap_ptr());
    uchar4* devPtr;
    size_t size;
    HANDLE_ERROR(
        deductaGraphicsMapResources(1, & ( bitmap->resource), NULL);
    );
    HANDLE_ERROR(
        deductaGraphicsResourceGetMappedPointer((void**)&devPtr,
            &size,
            bitmap->resource);
    );

其次,它调用用户指定的函数fAnim(),这个函数可能启动一个CUDA C核函数并用图像数据来填充devPtr指向缓冲区。

bitmap->fAnim( devPtr, bitmap->dataBlock, ticks++);

最后,它将取消GPU指针的映射,这会释放OpenGL驱动程序使用的缓冲区。绘制图像的操作将在调用glutPostRedisplay()时触发。

HANDLE_ERROR(udaGraphicsUnmapResources(1, & ( bitmap->resource), NULL)); glutPostRedisplay();

GPUAnimBitmap结构中还包含了一些重要但却有些不太相关的基础代码。如果你对这些代码感兴趣,可以对其进行分析。然而,即使没有时间或者兴趣来理解GPUAnimBitmap的剩余代码,也没关系,你仍然能够理解接下来的内容。

8.3.2 重新实现基于GPU的波纹动画示例

现在,我们已经实现了一个GPU版本的CPUAnimBitmap,接下来可以继续将基于GPU的波纹应用程序修改为完全在GPU上执行动画。首先要包含gpu anim.h头文件,其中包含了GPUAnimBitmap的实现。我们还包含了与第5章中几乎相同的核函数。

include"../common/book.h" #include“../common/gpu_anim.h"   
#define DIM1024   
__global__void kernel(uchar4 \*ptr,int ticks){ //将threadIdx/BlockIdx映射到像素位置 int  $x =$  threadIdx.x  $^+$  blockIdx.x \*blockDim.x; int  $y =$  threadIdx.y  $^+$  blockIdx.y \*blockDim.y; int offset  $= x + y$  \*blockDim.x \*gridDim.x; //现在计算这个位置上的值 float  $\mathrm{fx} = \mathrm{x} - \mathrm{DIM} / 2;$  float  $\mathrm{fy} = \mathrm{y} - \mathrm{DIM} / 2;$  floatd  $=$  sqrtf(fx\*fx+fy\*fy); unsigned char grey  $=$  (unsigned char)(128.0f  $^+$  127.0f \* cos(d/10.0f-ticks/7.0f)/
(d/10.0f + 1.0f));  
ptr[offset].x = grey;  
ptr[offset].y = grey;  
ptr[offset].z = grey;  
ptr[offset].w = 255;

上面代码中的粗体部分是我们唯一进行的修改。这个修改的原因是因为OpenGL互操作要求共享内存满足图形操作的需求。由于在绘制过程中,每个元素通常包含4部分信息(红/绿/蓝/Alpha),因此目标缓冲区将不再像以前那样只是一个unsigned char数组。现在要求是一个uchar4类型的数组。在第5章中,我们将缓冲区的每四个字节作为一个数据,因此通过ptr[offset*4+k]对进行索引,其中k的取值范围是0到3。但现在,uchar4类型很好表达了在数据中包含四部分信息的语义。

由于CUDA函数kernel()将生成图像数据,因此剩下的工作就是编写一个主机函数,并将其作为回调函数保存在GPUAnimBitmap的成员idle_func()中。对于这里的应用程序,该函数执行的操作就是启动CUDA C核函数:

void generate_frame( uchar4 *pixels, void*, int ticks ) {
    dim3 grids(DIM/16, DIM/16);
    dim3 threads(16, 16);
    kernel<<<grids, threads>>>(pixels, ticks);
}

由于所有的辅助工作都是在GPUAnimBitmap结构中完成的,因此这个函数的代码很简洁。然后,我们只需创建一个GPUAnimBitmap,并且注册动画回调函数generate_frame()。

int main(void) {
    GPUAnimBitmap bitmap(DIM, DIM, NULL);
    bitmap anim_and_exit(
        (void*) (uchar4*, void*, int)) generate_frame, NULL);
}