PBO是OpenGL最高效的像素拷贝方式吗?

3,504 阅读7分钟

  欢迎大家关注一下我开源的一个音视频库,HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。

  OpenGL ES作为移动设备的主要图形API,是客户端调用GPU的主要入口,不管是做游戏还是音视频,都给我们提供了强大的支持。   而在音视频领域,相信不少同鞋都有从FBO读取像素数据的需求,熟悉OpenGL ES的童鞋应该首先想到了glReadPixels,而了解更为深入的童鞋相信都会使用更为高效的PBO。   在Android平台上,PBO是从FBO读取像素数据最高效的的方法吗。显然不是,否则这篇文章就没有意义了。下面我们来盘点Android下有哪些从FBO读取像素数据的方式,以及最高效的方式。 ##一、glReadPixels   glReadPixelsOpenGL ES 2.0OpenGL ES 3.0都支持的api,使用最为简单广泛,只需要绑定一个FBO,然后就可以通过glReadPixels来读取像素数据到一个指定的缓冲区就可以了。这是本文所有方式中最为低效的,但因为其简单通用,所以使用广泛。

private fun readPixelsFromFBO(frameBuffer: Int) {
    if (buffer == null) {//申请一个用于缓存像素数据和的缓冲区
        buffer = ByteBuffer.allocate(width * height * 4)
        buffer.order(ByteOrder.nativeOrder())
    }
    //读取像素之前记得清空一下
    buffer!!.clear()
    //绑定一个需要读取的FBO
    GLES20.glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer)
    //从FBO中读取像素数据,并立即返回
    GLES20.glReadPixels(0, 0, width, height, Egl.GL_CLOLR_DEFAULT,
            GLES20.GL_UNSIGNED_BYTE, buffer)
    //取消绑定FBO
    GLES20.glBindFramebuffer(GL_FRAMEBUFFER, GLES20.GL_NONE)
}

##二、PBO   PBOOpenGL ES 3.0开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。由于现在Android的生态还有大部分只支持到OpenGL ES 2.0的硬件存在,所以通常需要跟glReadPixels配合使用。判断硬件api版本,如果是3.0就使用PBO,否则使用glReadPixels。虽然使用起来比第一中方法要复杂很多,但是却能大幅提高性能,所以还是值得的。   PBO的主要优点是可以通过DMA (Direct Memory Access)快速地在显卡上传递像素数据,而不影响CPU的时钟周期(中断)。另一个优势是它还具备异步DMA传输。也正因为这个特性,使得在使用单个PBO的情况下,性能提升并不明显,所以通常需要两个PBO配合使用。   在使用的时候,先绑定第一个PBO,然后调用另一个特殊的glReadPixels异步读取像素数据,这时候会立即返回,而不是像第一种方法那样需要等待。于此同时,去取出第二个PBO的数据(如果已经准备好),PBO数据的读取主要通过glMapBufferRange(内存映射)的方式。图片出自OpenGL Pixel Buffer Object (PBO)

不使用PBO加载纹理

使用PBO加载纹理

private fun initPBOs() {
    val size = width * height * 4
    pbos = IntArray(2)
    GLES30.glGenBuffers(2, pbos, 0)
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos!![0])
    GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, size, null, GLES30.GL_STATIC_READ)
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos!![1])
    GLES30.glBufferData(GLES30.GL_PIXEL_PACK_BUFFER, size, null, GLES30.GL_STATIC_READ)
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, 0)
}
private fun readPixelsFromPBO(frameBuffer: Int) {
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, frameBuffer)
    //绑定到第一个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos!![index])
    GLHelper.glReadPixels(0, 0, width, height, Egl.GL_CLOLR_DEFAULT, GLES30.GL_UNSIGNED_BYTE)
    //绑定到第二个PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, pbos!![nextIndex])
    //glMapBufferRange会等待DMA传输完成,所以需要交替使用pbo
    //映射内存
    pixelsBuffer = PixelsBuffer.wrap(GLES30.glMapBufferRange(GLES30.GL_PIXEL_PACK_BUFFER,
            0, width * height * Egl.COLOR_CHANNELS, GLES30.GL_MAP_READ_BIT) as ByteBuffer)
    //            PushLog.e("glMapBufferRange: " + GLES30.glGetError());
    //解除映射
    GLES30.glUnmapBuffer(GLES30.GL_PIXEL_PACK_BUFFER)
    //解除绑定PBO
    GLES30.glBindBuffer(GLES30.GL_PIXEL_PACK_BUFFER, GLES20.GL_NONE)
    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, GLES20.GL_NONE)
    //交换索引
    index = (index + 1) % 2
    nextIndex = (nextIndex + 1) % 2
}
/**
 * 最后记得要释放
 */
private fun releasePBO() {
    if (null != pbos) {
        GLES20.glDeleteBuffers(pbos!!.size, pbos, 0)
    }
}

  由于这个过程中我们需要使用另一个特殊的glReadPixels,而这个api是没有提供jni接口的,所以需要我们自己开一个jni接口,以供java层调用。

glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);

  GLHelper.kt

object GLHelper {
    init {
        System.loadLibrary("glhelper")
    }

    private val PBO_SUPPORT_VERSION = 0x30000
    external fun glReadPixels(x: Int,
                              y: Int,
                              width: Int,
                              height: Int,
                              format: Int,
                              type: Int)
}

  com_lmy_codec_helper_GLHelper.c

#include <jni.h>
#include <GLES2/gl2.h>
JNIEXPORT void JNICALL Java_com_lmy_codec_helper_GLHelper_glReadPixels
        (JNIEnv *env, jobject thiz, jint x, jint y, jint width, jint height, jint format,
         jint type) {
    glReadPixels(x, y, width, height, format, type, 0);
}

  最后记得在编译的时候引入OpenGL ES 2.0,本文使用的是Android.mk,引入方法如下。

LOCAL_LDLIBS := -lGLESv2

  然而PBO还有一个非常坑的地方,经测试表明,在部分硬件上glMapBufferRange映射出来的Buffer拷贝极为耗时,可以高达30+ms,这对于音视频处理显然是不能接受的。通常,映射出来的是一个DirectByteBuffer,也是一个堆外内存(C内存),这部分内存本身只能通过Buffer.get(byte[])拷贝来拿到数据,但正常情况下只需要2-3ms。出现这种问题估计是硬件上留下的坑。   所以,在Android上使用PBO是有比较多的兼容性问题的,包括上面说的。正确使用PBO的方式是,首先判断是否支持PBO,如果支持,则还是先使用glReadPixels进行读取测试,记录平均耗时,然后再使用PBO进行读取测试,记录平均耗时,最后对比两个方式的耗时,选择最快的一个。这样动态处理是比较复杂的,然而在这种情况下你不得不这样做。那么有没有一种既简单又高效的方式呢?

##三、ImageReader(推荐)   在Android平台,提供了更为高效的像素数据读取方法,也就是ImageReader。   使用过MediaCodec的童鞋应该知道,我们可以从MediaCodec获取一个Surface,再生成一个EGL环境,然后我们就可以通过OpenGL往这个Surface绘制数据了,最后MediaCodec内部取出数据进行编码。整个过程就跟我们通过OpenGL绘制纹理到屏幕是一样的。   而且在Android最新的Camera 2.0中也提供了这样的应用方式,通过addTarget(Surface)把摄像头数据绘制Surface,然后从中取出数据。当然我们是没办法直接从Surface获取数据的,这需要借助于ImageReader。   废话不多说,首先我们生成ImageReader实例。第一和第二个参数分别是宽高。第二个是颜色格式,如果不是RGBA_8888会报错,貌似只支持RGBA_8888。第三个是缓存大小,ImageReader天然支持多级缓存。

/**
 *@param width 画面宽度
 *@param height 画面高度
 *@param format 目标数据格式
 *@param maxImages 最大缓存多少帧
 */
imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 5)

  接着给ImageReader设置一个回调,用来接收处理好的数据。第二个参数为Handler,不建议传空,而是指定一个子线程的Handler,这样子ImageReader就会在子线程中处理回调,当然你也可以在回调中把数据丢到子线程进行处理。

imageReader?.setOnImageAvailableListener(object :ImageReader.OnImageAvailableListener{
    override fun onImageAvailable(reader: ImageReader?) {
      val image = reader.acquireNextImage()
      handleImage(image)
      image?.close()
    }
}, null)

  实例化之后我们就可以通过ImageReader.getSurface()拿到一个Surface,根据这个Surface生成一个EGL环境,之后怎么使用跟MediaCodec和绘制到屏幕是一样的。当我们swapBuffers()之后,就能在回调中通过acquireNextImage来获取像素数据。相关源码会在文章最后提供。   接下来怎么处理拿到的Image是重点。由于Image中的缓冲区存在数据对齐,所以其大小不一定是我们生成ImageReader实例时指定的大小,ImageReader会自动为画面每一行最右侧添加一个padding,以进行对齐,对齐多少字节可能因硬件而异,所以我们在取出数据时需要忽略这一部分数据。   正确取出数据后,你就可以把帧送进x264进行编码,或者进行人脸识等别各种处理了。

/**
 * 处理返回的Image
 */
private fun handleImage(image: Image) {
    //获取所有平面,由于RGBA只有一个平面,所以只使用到平面0
    val planes = image.planes
    val width = image.width
    val height = image.height
    val rowStride = planes[0].rowStride
    val pixelStride = planes[0].pixelStride
    //计算padding
    val rowPadding = rowStride - pixelStride * width
    //把数据从buffer拷贝到data
    copyToByteArray(planes[0].buffer, width, height, rowPadding)
}

private fun copyToByteArray(buffer: ByteBuffer, width: Int, height: Int, rowPadding: Int) {
    if (null == data) {
        data = ByteArray(width * height * 4)
    }
    var offset = 0
    for (i in 0 until height) {
        buffer.position(offset + i * rowPadding)
        buffer.get(data, offset, width * 4)
        offset += width * 4
    }
}

  经测试,ImageReader是要比PBO快一点的。虽然ImageReader有对齐的问题,但是它却可以让你忽略PBO的兼容性。它使用简单标准;它天然支持多级缓存;它不需要OpenGL ES 3.0;它比PBO更为稳定和通用。正因为这样,ImageReader才是Android读取FBO像素数据的正确方式!

##四、知识点:

  1. Android平台下的FBO像素读取方式。
  2. 如何高效的从FBO读取像素数据。

##五、相关源码 HardwareVideoCodec项目


欢迎关注微信公众,及时获取一手多媒体技术资讯