OpenGL进阶之像素缓冲PixelBuffer

Posted 木大白易

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGL进阶之像素缓冲PixelBuffer相关的知识,希望对你有一定的参考价值。

本系列文章为Learn OpenGL个人学习总结!
OpenGL入门(一)之认识OpenGL和创建Window
OpenGL入门(二)之渲染管线pipeline,VAO、VBO和EBO
OpenGL入门(三)之着色器Shader
OpenGL入门(四)之纹理Texture
OpenGL入门(五)之Matrix矩阵操作和坐标系统
OpenGL进阶(一)之帧缓冲FrameBuffer
OpenGL进阶(二)之像素缓冲PixelBuffer

PBO

说起PBO,不得不提一篇文章:
http://www.songho.ca/opengl/gl_pbo.html

本篇文章,更多的是对该文章的翻译和提炼总结!另外文章提到的PBO当时还是OpenGL中的一个拓展,在v2.1版本开始已经是核心功能了!

什么是PBO

PBO(Pixel Buffer Object)非常类似VBO,以便于将顶点数据和像素数据存储到缓冲对象中,这种存储像素数据的缓冲区对象称为像素缓冲区对象 (PBO)。
另外,添加了2个额外的“目标”标志。这些标志协助绑定的PBO内存管理器(OpenGL驱动)决定缓冲对象存储的最好的位置:系统内存,共享内存或者显存
另外,目标标志清楚的指出PBO绑定将用于2种操作:GL_PIXEL_PACK_BUFFER用于将像素数据传送到PBO,或者GL_PIXEL_UNPACK_BUFFER将PBO传输到像素数据。

举个例子,glReadPixels()glGetTexImage()是“打包(pack)”像素操作, 而glDrawPixels(), glTexImage2D()glTexSubImage2D()是“解压(unpack)”操作.当一个PBO绑定到GL_PIXEL_PACK_BUFFER标志上时,glReadPixels()从OpenGL的帧缓冲(framebuffer)读取像素数据(pixel data),然后把数据画(打包pack)到PBO中。当一个PBO绑定到GL_PIXEL_UNPACK标志上时,glDrawPixels()从PBO读取(unpack)或复制像素数据到OpenGL帧缓冲(framebuffer)中。

为什么要用PBO

PBO的主要优势是通过直接的内存访问(Direct Memory Access,DMA)而不用涉及到CPU周期就可以快速的将像素数据传输到或者传输出显卡。PBO的另一个优势是异步的直接内存访问传输,让我们比较下传统的纹理传输方法和使用像素缓冲对象。下面左图是用传统的方式从图像源(图像文件或者视频流)去加载图片数据.图像源数据首先加载到系统内存中,然后,通过glTexImage2D()从系统内存里面拷贝到OpenGL纹理对象。这2个传输步骤(加载和拷贝)都是CPU执行的。

相反,在右侧图中,图像源可以直接加载到由 OpenGL 控制的 PBO 中。CPU 仍然涉及将图像源加载到 PBO,但不用于将像素数据从 PBO 传输到纹理对象。相反,GPU(OpenGL 驱动程序)管理将数据从 PBO 复制到纹理对象。这意味着 OpenGL 在不浪费 CPU 周期的情况下执行 DMA 传输操作。此外,OpenGL 可以安排异步 DMA 传输以供以后执行。因此,glTexImage2D() 立即返回,CPU 可以执行其他操作,而无需等待像素传输完成。

有两种主要的 PBO 方法可以提高像素数据传输的性能:流式纹理更新从帧缓冲区异步回读

创建PBO

开头提到PBO和VBO非常类似,不同的是多了两个额外标志GL_PIXEL_PACK_BUFFERGL_PIXEL_UNPACK_BUFFERGL_PIXEL_PACK_BUFFER是用来将像素数据从OpenGL传输到你的应用的,而GL_PIXEL_UNPACK_BUFFER是用来将像素数据从应用传输到OpenGL中的。OpenGL参考这些标志决定PBO的最佳内存空间,例如,用于上传(解包unpacking)纹理的视频内存,或用于读取(打包packing)帧缓冲区的系统内存。然后,这些目标标志只是用来提示而已。OpenGL驱动会为你决定合适的位置。

创建 PBO 需要 3 个步骤;

  • 使用glGenBuffers()生成一个新的缓冲区对象。
  • 使用glBindBuffer()绑定缓冲区对象。
  • 使用glBufferData()将像素数据复制到缓冲区对象。

如果在 glBufferData() 中指定指向源数组的 NULL 指针,则 PBO 仅分配具有给定数据大小的内存空间。glBufferData() 的最后一个参数是 PBO 提供如何使用缓冲区对象的另一个性能提示。GL_STREAM_DRAW用于流式纹理上传,GL_STREAM_READ用于异步帧缓冲区回读

映射PBO(Mapping PBO)

PBO 提供了一种内存映射机制,将 OpenGL 控制的缓冲区对象映射到客户端的内存地址空间(GPU—>CPU)。因此,客户端可以使用glMapBuffer()glUnmapBuffer()修改缓冲区对象的一部分或整个缓冲区。

void* glMapBuffer(GLenum target, GLenum access)

GLboolean glUnmapBuffer(GLenum target)

如果成功,glMapBuffer() 返回指向缓冲区对象的指针。否则返回 NULL。目标参数是GL_PIXEL_PACK_BUFFERGL_PIXEL_UNPACK_BUFFER 。第二个参数,access指定如何处理映射的缓冲区;从 PBO 读取数据 (GL_READ_ONLY),将数据写入 PBO (GL_WRITE_ONLY),或两者兼而有之 (GL_READ_WRITE)。

请注意,如果 GPU 仍在使用缓冲区对象,则glMapBuffer()将不会返回,直到 GPU 完成其与相应缓冲区对象的工作。为了避免这种停顿(等待),请在glMapBuffer()之前使用 NULL 指针调用 glBufferData()。然后,OpenGL 会丢弃旧的缓冲区,并为缓冲区对象分配新的内存空间。

使用 PBO 后,必须使用 glUnmapBuffer() 取消映射缓冲区对象。如果成功,glUnmapBuffer() 返回 GL_TRUE。否则,它返回 GL_FALSE。

两个使用PBO提高像素传输性能的例子

流式纹理更新

纹理源在 PBO 模式下的每一帧都直接写入映射的像素缓冲区。然后,使用 glTexSubImage2D() 将这些数据从 PBO 传输到纹理对象。通过使用 PBO,OpenGL 可以在 PBO 和纹理对象之间执行异步 DMA 传输。它显着提高了纹理上传性能。如果支持异步 DMA 传输,glTexSubImage2D() 应该立即返回,CPU 可以处理其他作业而无需等待实际的纹理复制。

为了最大限度地提高流传输性能,您可以使用多个像素缓冲区对象。上图显示同时使用了 2 个 PBO;glTexSubImage2D() 从一个 PBO 复制像素数据,同时将纹理源写入另一个 PBO。

对于第n帧,PBO 1用于 glTexSubImage2D() 并且PBO 2用于获取新的纹理源。对于第n+1帧,2 个像素缓冲区正在切换角色并继续更新纹理。由于异步 DMA 传输,可以同时执行更新和复制过程。CPU 将纹理源更新为 PBO,而 GPU 从另一个 PBO 复制纹理。

下边我们使用前边文章中的木箱纹理,来模拟一下两个PBO更新纹理的流程:

    //加载图片
    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true); // OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。
    //这里会拿到图像的宽高和颜色通道个数  3
    unsigned char *data = stbi_load("../dependency/stb/container.jpg", &width, &height, &nrChannels, 0);

    int data_size = width*height*3;

    unsigned int pboIds[2]; // IDs of PBO
    // create 2 pixel buffer objects, you need to delete them when program exits.
    // glBufferData() with NULL pointer reserves only memory space.
    // 第三个参数data传NULL,只保留内存空间
    glGenBuffers(2, pboIds);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[0]);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);

    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[1]);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);

    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    //生成纹理
    unsigned int texture1;
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    //data传0
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);

    //传统的直接上传纹理
    // glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);

    ....
    
    //渲染
    while (!glfwWindowShouldClose(window))
    
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);               
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        static int index = 0;
        int nextIndex = 0; // pbo index used for next frame
        // In dual PBO mode, increment current index first then get the next index
        index = (index + 1) % 2;
        nextIndex = (index + 1) % 2;

        // bind the texture and PBO
        glBindTexture(GL_TEXTURE_2D, texture1);
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[index]);
        /*________________________________*/

        // ----start----
        // copy pixels from PBO to texture object
        // Use offset instead of ponter.
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
        
        // bind PBO to update pixel values
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIds[nextIndex]);

        // map the buffer object into client's memory
        // Note that glMapBuffer() causes sync issue.
        // If GPU is working with this buffer, glMapBuffer() will wait(stall)
        // for GPU to finish its job. To avoid waiting (stall), you can call
        // first glBufferData() with NULL pointer before glMapBuffer().
        // If you do that, the previous data in PBO will be discarded and
        // glMapBuffer() returns a new allocated pointer immediately
        // even if GPU is still working with the previous data.
        glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);
        unsigned char *ptr = (unsigned char*)glMapBuffer(GL_PIXEL_UNPACK_BUFFER, GL_WRITE_ONLY);
        if (ptr)
        
            // update data directly on the mapped buffer
            memcpy(ptr, data, data_size);
            glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); // release pointer to mapping buffer
        
        // ----end----
        // it is good idea to release PBOs with ID 0 after use.
        // Once bound with 0, all pixel operations behave normal ways.
        glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        ....
    

上边几个地方需要注意:

  1. glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);中第三个参数传0,只保留内存空间
  2. glTexImage2D()生成纹理时,最后一个参数data传0,同样是保留内存空间
  3. 在渲染中glTexSubImage2D()即通知GPU将当前绑定的PBO中数据上传到纹理对象中。因为绑定了PBO,所以最后一个参数data也传0。
    题外话:使用glTexSubImage2D()要求宽高和通道数都要与glTexImage2D()中的相同!
  4. 绑定另外一个PBO,将图像数据更新到另一个PBO中。因为这里使用了PBO的内存映射机制,所以在此之前glBufferData(GL_PIXEL_UNPACK_BUFFER, data_size, 0, GL_STREAM_DRAW);第三个参数data也传0。如果不用下边的内存映射,可以直接传图像数据data。

从帧缓冲区异步回读

传统的 glReadPixels() 会阻塞管道并等待所有像素数据传输完毕。然后,它将控制权返回给应用程序。相反,带有 PBO 的 glReadPixels() 可以安排异步 DMA 传输并立即返回而不会停顿。因此,应用程序(CPU)可以立即执行其他进程,同时通过 OpenGL(GPU)使用 DMA 传输数据。

上图使用 2 个像素缓冲区。在第 n帧,应用程序使用 glReadPixels()将像素数据从 OpenGL 帧缓冲区读取到PBO 1 ,并处理PBO 2中的像素数据。这些读取和处理可以同时执行,因为对PBO 1的 glReadPixels() 会立即返回,并且 CPU 会立即开始处理PBO 2中的数据。而且,我们在每一帧上 交替使用PBO 1和PBO 2 。

关于FBO的内容,可以参考上一篇文章:OpenGL进阶(一)之帧缓冲FrameBuffer

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    
    //初始化
    unsigned int pboIds[2]; // IDs of PBO
    // create 2 pixel buffer objects, you need to delete them when program exits.
    // glBufferData() with NULL pointer reserves only memory space.
    // 第三个参数data传NULL,只保留内存空间
    glGenBuffers(2, pboIds);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[0]);
    glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);

    glBindBuffer(GL_PIXEL_PACK_BUFFER, pboIds[1]);
    glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);

    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);

    ...
    

首先创建没有什么好说的,跟上边上传纹是一样的,只不过类型修改一下GL_PIXEL_PACK_BUFFERGL_STREAM_READ

    //绑定到第一个PBO
    glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[index]);
    //调用glReadPixels通知GPU把数据拷贝到第一个pbo里
    glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, 0);
    
    //绑定到第二个PBO
    glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[mPboIdNewIndex]);

    //映射内存,pbo->cpu
    void *pixelsPtr = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, data_size, GL_MAP_READ_BIT);
    if (pixelsPtr) 
        memcpy(data, static_cast<unsigned char *>(pixelsPtr), data_size);
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    
    
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    index = (index + 1) % 2;
    nextIndex = (nextIndex + 1) % 2;

首先glReadPixels()函数:
参数1,2:要从frame buffer中读取的第一个像素的坐标
参数3:指定像素数据的格式
参数4:指定像素数据的数据类型
参数5:具体的像素数据指针。这里使用了FBO,所以传0

然后,glMapBufferRange()和上边glMapBuffer()相同,使用内存映射,将PBO2中的数据读取复制出来!
最后交换索引!

以上是关于OpenGL进阶之像素缓冲PixelBuffer的主要内容,如果未能解决你的问题,请参考以下文章

OpenGL进阶之像素缓冲PixelBuffer

OpenGL进阶之帧缓冲FrameBuffer

OpenGL进阶之帧缓冲FrameBuffer

使用 glDrawPixels() 在 OpenGL 上绘制像素

OpenGL ES之深入解析PBOUBO与TBO的功能和使用

我的OpenGL学习进阶之旅介绍一下 映射缓冲区对象和复制缓冲区对象