相关主题:顶点缓冲区对象(VBO),帧缓冲区对象(FBO)
下载: pboUnpack.zip, pboPack.zip
- 概述
- 创建PBO
- 映射PBO
- 例子:使用PBO上传到纹理
- 例子:使用PBO进行异步地回读
概述
OpenGL PBO
OpenGL中的ARB_pixel_buffer_object扩展和ARB_vertex_buffer_object扩展非常类似。这样非常简单地扩充
ARB_vertex_buffer_object是为了不仅能在缓冲区对象中存储顶点数据也能存储像素数据。这个存储像素数据的缓冲区对象叫做像素缓冲区(PBO)。ARB_pixel_buffer_object扩展借用了所有VBO的框架和API,并且添加了两个额外的"target"值。这个“target”值方便PBO的内存管理机制决定缓冲区对象在内存中的最佳位置,是将缓冲区对象放置在系统内存中,还是共享内存中,还是显卡内存中。这个“target”值也清晰地指明了PBO的使用范围:
GL_PIXEL_PACK_BUFFER_ARB表示将像素数据“传入”到PBO,GL_PIXEL_UNPACK_BUFFER_ARB表示数据从PBO中“传出”。
例如,glReadPixels()和glGetTexImage()是“传入”操作,glDrawPixels(),glTexImage2D()和glTexSubImage2D()是“传出”操作。当一个PBO使用GL_PIXEL_PACK_BUFFER_ARB 显示使用范围时,glReadPixels()从OpenGL的帧缓冲区中读取像素并将数据写入(”传入“)PBO中。当一个PBO使用GL_PIXEL_UNPACK_BUFFER_ARB 显示使用范围时,glDrawPixels()从PBO中读取(”传出“)数据并将它们复制到OpenGL中的帧缓冲区中。
PBO最大的优点是使用DMA(Direct Memory Access,直接内存访问)的方式快速地将数据传入到显卡或从显卡传出,它不需要等待CPU的周期。PBO的另外一个优点是可以使用异步地DMA方式传输数据。让我们来比较一下传统的纹理传输的方式和使用PBO的区别。如下图所示,第一幅图显示传统的方式从数据源(图片文件或视频流)中加载纹理数据。数据源首先被加载到系统内存中,然后使用glTexImage2D()冲系统内存中复制到OpenGL中的纹理对象中。这两个步骤(加载和复制)都是在CPU中进行的。
第二幅图片则正好相反,数据源能直接被加载到PBO中,而PBO是被OpenGL控制的。CPU也参与了加载数据源到PBO,但是不会将PBO中的纹理数据传输到纹理对象中。反而由GPU(OpenGL驱动器)管理将数据从PBO复制到纹理对象中。这意味着OpenGL进行了DMA方式的传输操作而不需要等待CPU的周期。除此之外,OpenGL甚至还可以使用异步地传输方式。因此glTexImage2D()会立即返回,CPU可以执行其他的事情而不需要等待像素数据传输完。
不使用PBO载入纹理
使用PBO载入纹理
有两种主要的使用PBO提高传输像素数据的方式:上传到纹理和从帧缓冲区中进行异步回读。
创建PBO
像之间提到过的那样,像素缓冲区对象(PBO)借用了顶点缓冲区对象(VBO)中的所有API。唯一的区别是为”target“表示增加了两个值:GL_PIXEL_PACK_BUFFER_ARB和GL_PIXEL_UNPACK_BUFFER_ARB。GL_PIXEL_PACK_BUFFER_ARB用来表示将数据从OpenGL中传输到你的应用程序中,GL_PIXEL_UNPACK_BUFFER_ARB表示将数据从应用程序中传输到OpenGL中。OpenGL使用这些标示来决定将PBO放置在最合适的地方,例如,显卡内存用来上传(unpack)纹理,系统内存用来冲帧缓冲区中读取(pack)数据。然而,这些标记都是单独使用的,OpenGL驱动将会为你指定合适的位置。
创建PBO需要3步:
- 使用glGenBuffersARB()创建一个缓冲区对象
- 使用glBindBufferARB()绑定一个缓冲区对象
- 使用glBufferDataARB()将像素数据复制到缓冲区对象中
如果在glBufferDataARB()中为数据源数组传入了NULL,那么PBO只会分配一个指定大小的空间。glBufferDataARB()函数的最后一个参数是另个用来指定PBO如何使用的性能指标,GL_STREAM_DRAW_ARB表示通过流的方式上传纹理,GL_STREAM_READ_ARB表示异步地从帧缓冲区中进行回读。
请查看VBO中的更多细节。
映射PBO
PBO提供了内存映射的机制来将OpenGL中的缓冲区对象映射到客户端的内存区域中。因此客户端可以通过使用glMapBufferARB()和glUnmapBufferARB()修改缓冲中的部分内容或者整个缓冲区。
<span style="white-space:pre"> </span>void* glMapBufferARB(GLenum target, GLenum access)
<span style="white-space:pre"> </span>GLboolean glUnmapBufferARB(GLenum target)
glMapBufferARB()如果成功将会返回一个指向缓冲区对象的指针,否则将会返回NULL。target参数是
GL_PIXEL_PACK_BUFFER_ARB 或者 GL_PIXEL_UNPACK_BUFFER_ARB。第二个参数,access指定了对映射来的缓冲区的操作方式:从PBO中读取数据(GL_READ_ONLY_ARB),将数据写入到PBO(GL_WRITE_ONLY_ARB)中,或者两个都可以(GL_READ_WRITE_ARB)。
注意如果GPU仍然工作在一个缓冲区对象上,glMapBufferARB()将会在GPU在指定的缓冲区对象中工作完成之后才返回。为了避免等待,你可以首先使用一个空指针调用glBufferDataARB()函数,然后调用glMapBufferARB()。这样OpenGL将会废除旧的缓冲区,并为缓冲区对象分配新的空间。
缓冲区对象使用完后必须使用glUnmapBufferARB()解除映射,glUnmapBufferARB()成功时返回gl_TRUE,否则返回GL_FALSE。
例子:使用PBO上传到纹理
下载源文件和可执行程序: pboUnpack.zip
这个例子程序将纹理流从PBO上传到OpenGL中的纹理对象中。你可以通过按空格键来切换不同的模式(单个PBO,两个PBO,不使用PBO)并比较它们的性能差异。
在PBO模式中纹理源每帧都被直接写入到映射的像素缓冲区中。然后这些数据使用glTexSubImage2D()从PBO中传输到纹理对象中。通过使用PBO,OpenGL能在PBO和纹理对象中使用异步的DMA方式传输数据。它非常有效地提高了纹理传输的性能。如果异步的DMA方式被支持,glTexSubImage2D()会被立即返回,CPU不需要等待实际的纹理复制到PBO完成就可以继续做其他工作了。
使用两个PBO上传纹理
为了最大化地提示传输的性能,你可以使用两个像素缓冲区对象。上图显示了2个PBO同时被使用的情况;glTexSubImage2D()将像素数据从PBO中复制到纹理对象中时纹理源正好被写入到另一个PBO。
对第n帧而言,PBO1在执行glTexSubImage2D()操作,PBO2则在读取纹理。那么在第n+1帧时,2个PBO交换操作并且继续更新纹理。因为异步地DMA方式传输,更新和复制操作可以同时进行。CPU更新纹理源时GPU会从PBO会从PBO中复制数据。
// "index" 被用来从PBO中复制像素到纹理对象中
// "nextIndex" 被用来在另一个PBO中更新像素
index = (index + 1) % 2;
nextIndex = (index + 1) % 2;
// 绑定纹理和PBO
glBindTexture(GL_TEXTURE_2D, textureId);
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[index]);
// 从PBO中复制像素到到纹理对象中
// 使用偏移量而不是指针
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT,
GL_BGRA, GL_UNSIGNED_BYTE, 0);
// 绑定PBO来更新纹理源
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[nextIndex]);
//注意glMapBufferARB()将会导致一个同步的问题。
//如果GPU依然在顶点缓冲区中工作,那么glMapBufferARB()函数
//将会在GPU结束在指定的缓冲区的工作之后才返回。
//为了避免等待,你可以首先使用一个空指针调用glBufferDataARB()函数,
//然后调用glMapBufferARB()。在这种情况下,之前的数据将会被舍弃,
//glMapBufferARB()将立即返回一个新分配区域的指针,
//即使GPU依然在之前的数据上工作。
glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, DATA_SIZE, 0, GL_STREAM_DRAW_ARB);
// 将缓冲区对象映射到客户端内存中
GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB,
GL_WRITE_ONLY_ARB);
if(ptr)
{
// 在映射的缓冲区中直接更新数据
updatePixels(ptr, DATA_SIZE);
glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB); // 释放映射的缓冲区
}
// PBO使用完后使用将它绑定到0是一种释放PBO的很好的方法
// 当被绑定到0后, all pixel operations are back to normal ways.所有的像素操作变成了正常的方式
glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
例子:使用PBO进行异步地回读
![OpenGL系列教程之九:OpenGL像素缓冲区对象(PBO)_第4张图片](http://img.e-com-net.com/image/info5/edf81b5f8130493a98d5942b48bf5bc6.jpg)
下载源文件和可执行文件: pboPack.zip
这个例子程序从帧缓冲区中读取数据到PBO中,然后修改图片的亮度后又重新绘制在窗口中。你可以通过按空格键了切换PBO模式开启或关闭,来测试glReadPixels()函数的性能。
传统的glReadPixels()函数会阻塞渲染管线,它会一直等待直到所有的像素数据传输完成。然后才会将控制权交给应用程序。相反,使用PBO时glReadPixel()使用异步地DMA方式传输并且能立即返回。因此,应用程序(CPU)可以立即执行其他程序,这时数据会被OPenGL(GPU)使用DMA的方式传输。
这个例子使用两个像素缓冲区。在第n帧,应用程序使用glReadPixels()从OpenGL帧缓冲区中读取数据到PBO1中,并处理PBO2中的像素数据。这个读取和处理的过程可以同时进行,因此glReadPixels()函数会立即返回这样CPU开始处理PBO2中的数据。我们会在每一帧中交替PBO1和PBO2.
// "index" 用来表示将数据从帧缓冲区中读入PBO中
// "nextIndex" 更新另一个PBO的像素
index = (index + 1) % 2;
nextIndex = (index + 1) % 2;
// 设置帧缓冲区可读
glReadBuffer(GL_FRONT);
// 从帧缓冲区中读取数据到PBO中
// glReadPixels() 会立即返回.
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[index]);
glReadPixels(0, 0, WIDTH, HEIGHT, GL_BGRA, GL_UNSIGNED_BYTE, 0);
// 映射数据到PBO
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[nextIndex]);
GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB,
GL_READ_ONLY_ARB);
if(ptr)
{
processPixels(ptr, ...);
glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB);
}
// 返回正常的像素操作中
glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0);