PBO是OpenGL最高效的像素拷贝方式吗?那你就大错特错了
欢迎大家关注一下我开源的一个音视频库,HardwareVideoCodec是一个高效的Android音视频编码库,支持软编和硬编。使用它你可以很容易的实现任何分辨率的视频编码,无需关心摄像头预览大小。一切都如此简单。目前已迭代多个稳定版本,欢迎查阅学习和使用,如有BUG或建议,欢迎Issue。
OpenGL ES
作为移动设备的主要图形API,是客户端调用GPU的主要入口,不管是做游戏还是音视频,都给我们提供了强大的支持。
而在音视频领域,相信不少同鞋都有从FBO读取像素数据的需求,熟悉OpenGL ES
的童鞋应该首先想到了glReadPixels
,而了解更为深入的童鞋相信都会使用更为高效的PBO
。
在Android
平台上,PBO
是从FBO读取像素数据最高效的的方法吗。显然不是,否则这篇文章就没有意义了。下面我们来盘点Android
下有哪些从FBO
读取像素数据的方式,以及最高效的方式。
一、glReadPixels
glReadPixels
是OpenGL ES 2.0
和OpenGL 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
PBO
是OpenGL 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
(内存映射)的方式。
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像素数据的正确方式!
四、知识点:
- Android平台下的FBO像素读取方式。
- 如何高效的从FBO读取像素数据。