暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

OpenGL3.0特性之PBO

周末随心分享 2021-05-15
3580
背景

在前几次介绍中,周末君都是基于OpenGL2.0提供的功能给大家进行介绍,但是随着对渲染性能要求的逐渐提高,OpenGL也紧跟时代步伐,推出了3.0版本,而3.0里面很多都是对渲染性能的优化,今天就给大家介绍其中一种:Pixel Buffer Object (PBO)

PBO的主要优点是可以通过 DMA (Direct Memory Access) 快速地在显卡上传递像素数据,而不影响CPU的时钟周期(中断)。另一个优势是它还具备异步 DMA 传输。让我们对比使用 PBO 前后的纹理传送方法。

  • 传统方式是从图像文件或视频中加载纹理。首先,资源被加载到系统内存(Client)中,然后使用 glTexImage2D() 函数从系统内存复制到 OpenGL 纹理对象中(Client->Server)。这两次数据传输(加载和复制)完全由 CPU 执行
  • PBO传输方式则是由OpenGL控制的。虽然CPU有参与加载纹理到PBO,但不涉及将像素数据从PBO传输到纹理对象的工作,而是由GPU(OpenGL驱动)来负责PBO到纹理对象的数据传输的,这也就意味着OpenGL执行DMA传输操作不会占用CPU的时钟周期。此外,OpenGL还可以安排稍后执行的异步DMA传输。所以glTexImage2D()可以立即返回,CPU也无需等待像素数据的传输了,可以继续其他工作
  • 最后用两张图对比一下:


基于上面说的优点,PBO有一个典型的应用就是视频软解,由于它异步传输的特性,在视频软解时就可以在解出一帧数据后通知GPU来取数据,然后CPU马上去解下一帧数据,这样每帧耗时相对传统方式就能减少一半时间


说了这么多PBO优点,那怎么使用它呢?接下来就来实例演示一下PBO如何把一个视频进行离屏渲染并保存!



初始化PBO环境

PBO是OpenGL3.0才引入的特性,因此android minSdkVersion如果小于18,那么你需要下载google提供的gl3stub.c文件,不然编译都通过不了,会一直报错找不到GLESv3,具体可以参考google提供的demo:

https://github.com/googlesamples/android-ndk/tree/master/gles3jni

下载好gl3stub.h和gl3stub.c后把它放到你的工程jni目录下,编译选项加入它们,然后在你的cpp或者c文件内引用它,比如:

    #include <GLES2/gl2.h>#include "gl3stub.h"
    注意:
    • 这个头文件里面只包含了GLES3.0独有的api,所以你如果还用到了GLES2.0的api,你需要包含gl2.h,这样调用才不会报错
    • 在包含gl3stub头文件后还不能使用3.0特性,如果你认真看gl3stub文件内容你会发现需要手动调用gl3stubInit()这个函数后才能真正开始调用

     

     

    创建PBO

    因为PBO是3.0才有的特性,所以在上面初始化环境后就可以使用PBO了,首先创建两个pbo用于交替使用,至于为什么创建两个后面会逐步介绍:
      void initPBO(int width, int height) {    
      glVersion = (const char *) glGetString(GL_VERSION);
        LOGI("opengl version:%s", glVersion);    
        mPboIndex = 0;    
        mPboNewIndex = 1;    
        mInitRecord = true;
        //每个像素包含RGBA四个字节,所以乘于4    
        mPboSize = width * height * 4;    
        LOGI("picture width,height:%d,%d", width, height);    
        glGenBuffers(2, mPboIds);    
        if (mPboIds[0] == 0 || mPboIds[1] == 0) {        
         LOGI("%s""generate pbo fail");        
         return;    
        }    
        //绑定到第一个PBO    
        glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[0]);    
        //设置内存大小    
        glBufferData(GL_PIXEL_PACK_BUFFER, mPboSize, NULL, GL_STATIC_READ);    
        //绑定到第而个PBO    
        glBindBuffer(GL_PIXEL_PACK_BUFFER, mPboIds[1]);    
        //设置内存大小    
        glBufferData(GL_PIXEL_PACK_BUFFER, mPboSize, NULL, GL_STATIC_READ);    
        //解除绑定PBO    
        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
      }
      上面代码比较简单,就是初始化PBO,不过注意的一点是如果是YUV420数据,这边的mPboSize = mWidth * mHeight * 3 2,因为YUV420数据Y大小是width * height,U和V都是1/4 * width * height,加起来的话就是3/2 * width * height

       


      使用PBO进行视频离屏渲染并保存:

      在软解过程中,第n帧,PBO1正被glTexSubImage2D()函数使用。而PBO2用于读取新的纹理。在第n+1帧时,2个PBO交换角色,并继续更新纹理。因为DMA传输的异步性,更新和复制可被同时执行。CPU将新纹理更新到一个PBO中,同时GPU从另一个PBO中复制纹理

      1、视频解码并进行渲染

        // "index" 用于从PBO中拷贝像素数据至texture object
        // "nextIndex" 用于更新另一个PBO中的像素数据
        index = (index + 1) % 2;
        nextIndex = (index + 1) % 2;

        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, textureId);
        // 绑定PBO
        glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[index]);
        // 从PBO中拷贝像素数据至texture object
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT,GL_BGRA, GL_UNSIGNED_BYTE, 0);
        // 绑定另一个PBO,用texture source对它进行更新
        glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, pboIds[nextIndex]);
        // 注意:glMapBufferARB()会引起同步问题
        // 如果GPU正在使用这块buffer, glMapBufferARB()将会等待
        // 直到GPU完成操作. 为了避免等待,你可以先调用
        // glBufferDataARB() ,并传入NULL指针, 然后再调用glMapBufferARB()。
        // 如果按照上面的方法调用的话, PBO之前存储的数据将会被丢弃,并且
        // glMapBufferARB() 将会立即返回一个新分配的指针,
        // 即使GPU仍然在使用之前的数据
        glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, DATA_SIZE, 0, GL_STREAM_DRAW_ARB);
        // 映射buffer object(PBO)到客户端内存
        GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB,GL_WRITE_ONLY_ARB);
        if(ptr){
        // 直接更新映射的buffer
        updatePixels(ptr, DATA_SIZE);
        glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB);
        // release the mapped buffer
        }
        // 在使用完PBO以后,通过ID 0 来释放PBO
        // 一旦绑定到0,所有的像素操作都将被重置
        glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);

        2、读取渲染后的视频帧数据并保存

          // "index" 用于从FBO中读取像素到PBO
          // "nextIndex" 用于更新另一个PBO中的像素
          index = (index + 1) % 2;
          nextIndex = (index + 1) % 2;

          // 设置读取的目标framebuffer
          glReadBuffer(GL_FRONT);

          // 从FBO中读取像素至PBO
          // glReadPixels()将会立刻返回
          glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, pboIds[index]);
          glReadPixels(0, 0, WIDTH, HEIGHT, GL_BGRA, GL_UNSIGNED_BYTE, 0);

          // 映射PBO到客户端,并通过CPU处理其数据
          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);
          }

          // 重置PBO的像素操作
          glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0);

          这边有几点说明一下:

          1)首先是为什么使用2个PBO

          • 一个pbo用于存放已经解码的数据,等待GPU来读取,另一个pbo用于准备下一帧数据,这样复制和更新就能同时进行

          • 然后pbo个数为什么是2个而不是3个或4个呢,因为通常情况下CPU读取一帧的时间和GPU处理一帧的时间不会相差太大,所以两个pbo已经足够用了,当然如果CPU处理比较快的情况可以设置多个pbo用于提前存放解码数据

          2)如果是YUV数据的话,glReadPixels大小就得改了,如下:

          • glReadPixels(0, 0, width, height * 3 / 8, GL_RGBA, GL_UNSIGNED_BYTE, 0);

          • 3/8怎么来的呢,很简单,之前说过总大小是3/2,总大小 4(RGBA) 就是3/8了,至于为什么是height乘3/8的原因就是像素填充是从左到右,从上到下的,所以要从前往后读,宽度必须填满,高度就是决定实际读多少了。

          3)glMapBufferRange的作用就是将OpenGL控制的pbo地址映射到CPU来,所以这个地址是可以直接使用的,不用再调用memcpy来拷贝一遍,不然反而失去了PBO的优势


          4)glReadPixels传入的数组必须是malloc或者new创建的,不能是直接声明大小的,比如uint8_t data[size]这种是会出现fault addr错误, new 或者malloc才不会报错


          5)这边再强调一下,pbo是针对连续多帧渲染保存这种情况比较有优势,所以一般用于视频的渲染保存,单个pbo或者只读取一两次就不需要使用pbo了,不会提升多大速度

           


          总结

          经过上面的初步介绍,小伙伴们应该对PBO有了大概的印象,总的来说PBO优点就是针对视频这种需要快速处理多帧数据情况,传统方式会同步等待,而PBO由于异步DMA特点可以节省将近一半时间,对于离屏渲染来说是极大的优化


          好了,周末君关于OpenGL的介绍暂告一段落了,当然OpenGL3.0中还有像VAO、ETC2这种关于性能和内存的优化,后面有机会将会继续介绍,今天的分享就到这了^_^

          文章转载自周末随心分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论