Android 的 SurfaceTexture原理
1 什么是表面纹理
SurfaceTexture是Android上做渲染的核心组件,它是 Surface 和 OpenGL ES纹理的组合,用于提供输出到 GLES 纹理的 Surface。从安卓渲染系统上来说, 是一个BufferQueue的消费者,当生产方将新的缓冲区排入队列时, 回调会通知应用。然后应用调用 ,这会释放先前占有的缓冲区,从队列中获取新缓冲区并执行 EGL 调用,从而使 GLES 可将此缓冲区作为外部纹理使用。
基本流程如下:
- 通过Camera、Video解码器、OpenGL生成图像流。
- 图像流通过Surface入队到BufferQueue,并通知到GLConsumer。
- GLConsumer从BufferQueue获取图像流GraphicBuffer,并转换为EXTERNAL OES纹理。
- 得到OES纹理后,用户方就可以将其转换成普通的纹理,然后应用特效或者上屏。
2 SurfaceTexture的常见应用 - 相机与视频解码
SurfaceTexture的最常见应用场景是作为相机或者视频解码器的输出,这种应用场景非常常见,这里就不做详细描述了,以相机为例:
// SurfaceTexture配合GLSurfaceView实现渲染的关键代码
// 初始化surface texture
fun initSurfaceTexture(textureCallback: (surfaceTexture: SurfaceTexture) -> Unit) {
val args = IntArray(1)
GLES20.glGenTextures(args.size, args, 0)
surfaceTexName = args[0]
internalSurfaceTexture = SurfaceTexture(surfaceTexName)
textureCallback(internalSurfaceTexture)
}
// 收到OnFrameAvailableListener回调时,请求刷新GLSurfaceView
cameraSurfaceTexture.initSurfaceTexture {
it.setOnFrameAvailableListener {
requestRender()
}
cameraSurfaceTextureListener?.onSurfaceReady(cameraSurfaceTexture)
}
// 设置相机preview texture
camera.setPreviewTexture(surfaceTexture)
// 在gl线程更新texture
fun updateTexImage() {
internalSurfaceTexture.updateTexImage()
internalSurfaceTexture.getTransformMatrix(transformMatrix)
}
3 SurfaceTexture的内部实现 - EGLImageKHR
SurfaceTexture使用时,最主要的两个方法:
- SurfaceTexture (int texName) // 创建SurfaceTexture
- void updateTexImage () // 将当前图片流更新到纹理
3.1 SurfaceTexture是如何创建的
首先需要自己创建一个纹理ID,传递给SurfaceTexture,比如
//创建纹理id
int[] tex = new int[1];
GLES20.glGenTextures(1, tex, 0);
//创建SurfaceTexture并传入tex[0]
mSurfaceTexture = new SurfaceTexture(tex[0]);
Framework层的SurfaceTexture创建代码如下
// frameworks\base\graphics\java\android\graphics
// 构造函数
public SurfaceTexture(int texName) {
this(texName, false);
}
// 构造函数
// singleBufferMode是否是单buffer,默认为false
public SurfaceTexture(int texName, boolean singleBufferMode) {
mCreatorLooper = Looper.myLooper();
mIsSingleBuffered = singleBufferMode;
//native方法nativeInit
nativeInit(false, texName, singleBufferMode, new WeakReference<SurfaceTexture>(this));
}
framework层会调用到方法,最终调用到nativeInit``SurfaceTexture_init
方法。
// frameworks\base\core\jni\SurfaceTexture.cpp
// texName为应用创建texture名
// weakThiz为SurfaceTexture对象弱引用
static void SurfaceTexture_init(JNIEnv* env, jobject thiz, jboolean isDetached,
jint texName, jboolean singleBufferMode, jobject weakThiz)
{
sp<IGraphicBufferProducer> producer;
sp<IGraphicBufferConsumer> consumer;
// 创建IGraphicBufferProducer及IGraphicBufferConsumer
BufferQueue::createBufferQueue(&producer, &consumer);
if (singleBufferMode) {
consumer->setMaxBufferCount(1);
}
sp<GLConsumer> surfaceTexture;
// isDetached为false
if (isDetached) {
....
} else {
// 将consumer和texName封装为GLConsumer类对象surfaceTexture
surfaceTexture = new GLConsumer(consumer, texName,
GL_TEXTURE_EXTERNAL_OES, true, !singleBufferMode);
}
....
// 设置surfaceTexture名字
surfaceTexture->setName(String8::format("SurfaceTexture-%d-%d-%d",
(isDetached ? 0 : texName),
getpid(),
createProcessUniqueId()));
// If the current context is protected, inform the producer.
consumer->setConsumerIsProtected(isProtectedContext());
// 将surfaceTexture保存到env中
SurfaceTexture_setSurfaceTexture(env, thiz, surfaceTexture);
// 将producer保存到env中
SurfaceTexture_setProducer(env, thiz, producer);
jclass clazz = env->GetObjectClass(thiz);
// JNISurfaceTextureContext继承了GLConsumer::FrameAvailableListener
sp<JNISurfaceTextureContext> ctx(new JNISurfaceTextureContext(env, weakThiz,
clazz));
// surfaceTexture设置帧回调对象ctx,
// 收到帧数据是会触发ctx->onFrameAvailable方法
surfaceTexture->setFrameAvailableListener(ctx);
SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);
}
SurfaceTexture初始化后,向设置了监听器,该监听器会回调到Java层方法,进一步回调到注册到SurfaceTexture中的OnFrameAvailableListener监听器,用于通知业务层有新的入队了。这时候业务层就可以调用将GraphicBuffer更新到纹理。GLConsumer``JNISurfaceTextureContext``SurfaceTexture.postEventFromNative``GraphicBuffer``updateTexImage
3.2 updateTexImage是如何将数据更新到纹理的
按文档所说,回调可以发生在任意线程,所以不能在回调中直接调用,而是必须切换到OpenGL线程调用,那么内部是怎么实现的呢?OnFrameAvailableListener.onFrameAvailable``updateTexImage``updateTexImage
应用层的会最终调用到native层的方法。updateTexImage``GLConsumer::updateTexImage()
//frameworks\native\libs\gui\GLConsumer.cpp
status_t GLConsumer::updateTexImage() {
....
// mEglContext为eglGetCurrentContext
// Make sure the EGL state is the same as in previous calls.
status_t err = checkAndUpdateEglStateLocked();
....
BufferItem item;
// Acquire the next buffer.
// In asynchronous mode the list is guaranteed to be one buffer
// deep, while in synchronous mode we use the oldest buffer.
err = acquireBufferLocked(&item, 0);
.....
// Update the Current GLConsumer state.
// Release the previous buffer.
err = updateAndReleaseLocked(item);
.....
// Bind the new buffer to the GL texture, and wait until it's ready.
return bindTextureImageLocked();
}
可以看到获取帧数据是方法。acquireBufferLocked
// frameworks\native\libs\gui\GLConsumer.cpp
status_t GLConsumer::acquireBufferLocked(BufferItem *item,
nsecs_t presentWhen, uint64_t maxFrameNumber) {
// 获取Consumer当前的显示内容BufferItem
// 既获取相机预览帧数据
status_t err = ConsumerBase::acquireBufferLocked(item, presentWhen,
maxFrameNumber);
...
// If item->mGraphicBuffer is not null, this buffer has not been acquired
// before, so any prior EglImage created is using a stale buffer. This
// replaces any old EglImage with a new one (using the new buffer).
if (item->mGraphicBuffer != NULL) {
int slot = item->mSlot;
// 由获取的item->mGraphicBuffer生成一个EglImage,并赋值给mEglSlots[slot].mEglImage
mEglSlots[slot].mEglImage = new EglImage(item->mGraphicBuffer);
}
return NO_ERROR;
}
可以看到,SurfaceTexture最终会把GraphicBuffer创建一个EglImage对象,这个对象就保存了帧数据。
那么接下来的是如何将帧数据绑定到纹理上的呢?bindTextureImageLocked
status_t GLConsumer::bindTextureImageLocked() {
....
GLenum error;
....
// mTexTarget为应用创建的GL_TEXTURE_EXTERNAL_OES型texture
glBindTexture(mTexTarget, mTexName);
...
// 由mGraphicBuffer生成EGLImageKHR
status_t err = mCurrentTextureImage->createIfNeeded(mEglDisplay,
mCurrentCrop);
.....
// 将mCurrentTextureImage内容绑定到mTexTarget上
mCurrentTextureImage->bindToTextureTarget(mTexTarget);
.....
// Wait for the new buffer to be ready.
return doGLFenceWaitLocked();
}
再看下方法,它会最终调用到createIfNeeded``GLConsumer::EglImage::createImage
EGLImageKHR GLConsumer::EglImage::createImage(EGLDisplay dpy,
const sp<GraphicBuffer>& graphicBuffer, const Rect& crop) {
EGLClientBuffer cbuf =
static_cast<EGLClientBuffer>(graphicBuffer->getNativeBuffer());
const bool createProtectedImage =
(graphicBuffer->getUsage() & GRALLOC_USAGE_PROTECTED) &&
hasEglProtectedContent();
EGLint attrs[] = {
EGL_IMAGE_PRESERVED_KHR, EGL_TRUE,
EGL_IMAGE_CROP_LEFT_ANDROID, crop.left,
EGL_IMAGE_CROP_TOP_ANDROID, crop.top,
EGL_IMAGE_CROP_RIGHT_ANDROID, crop.right,
EGL_IMAGE_CROP_BOTTOM_ANDROID, crop.bottom,
createProtectedImage ? EGL_PROTECTED_CONTENT_EXT : EGL_NONE,
createProtectedImage ? EGL_TRUE : EGL_NONE,
EGL_NONE,
};
.....
eglInitialize(dpy, 0, 0);
// 调用eglCreateImageKHR创建EGLImageKHR 对象
EGLImageKHR image = eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID, cbuf, attrs);
...
return image;
}
关键语句是,可以看到,这里创建了EGLImageKHR 对象。现在已经生成了EGLImageKHR图像,接下来分析下 图像是如何绑定的纹理的。eglCreateImageKHR``EGLImageKHR``GL_TEXTURE_EXTERNAL_OES
// frameworks\native\libs\gui\GLConsumer.cpp
void GLConsumer::EglImage::bindToTextureTarget(uint32_t texTarget) {
// 更新纹理texTarget的数据为mEglImage
// 相当于glTexImage2D或者glTexSubImage2D
glEGLImageTargetTexture2DOES(texTarget,
static_cast<GLeglImageOES>(mEglImage));
}
小结一下,整个方法大致分为这几步:
- 生成纹理内容EGLImageKHR 对象,通过获取当前的显示内容(比如相机帧数据),然后由此生成一个图像。
ConsumerBase::acquireBufferLocked``GraphicBuffer``GraphicBuffer``EGLImageKHR
- 将图像通过绑定到型纹理上。
EGLImageKHR``glEGLImageTargetTexture2DOES``GL_TEXTURE_EXTERNAL_OES
4 EGLImageKHR
通过上面分析可以看到,SurfaceTexture的内部,主要是通过EGLImageKHR实现的,那么EGLImageKHR有什么作用呢?EGLImageKHR是EGL定义的一种专门用于共享2D图像数据的扩展格式,它可以在EGL的各种client api之间共享数据(如OpenGL,OpenVG),它的本意是共享2D图像数据,但是并没有明确限定共享数据的格式以及共享的目的。EGLImageKHR的创建函数原型是:
EGLImageKHR eglCreateImageKHR(
EGLDisplay dpy,
EGLContext ctx,
EGLenum target,
EGLClientBuffer buffer,
const EGLint *attrib_list)
在Android系统中专门定义了一个称为的Target,支持通创建EGLImage对象,而Buffer则对应创建EGLImage对象时使用数据。EGL_NATIVE_BUFFER_ANDROID``ANativeWindowBuffer
而在Android上定义的是在native中定义的类,综上,EGLClientBuffer``GraphicBuffer``EGLImageKHR
的基本使用流程如下:
#define EGL_NATIVE_BUFFER_ANDROID 0x3140
#define EGL_IMAGE_PRESERVED_KHR 0x30D2
GraphicBuffer* buffer = new GraphicBuffer(1024, 1024, PIXEL_FORMAT_RGB_565,
GraphicBuffer::USAGE_SW_WRITE_OFTEN |
GraphicBuffer::USAGE_HW_TEXTURE);
unsigned char* bits = NULL;
buffer->lock(GraphicBuffer::USAGE_SW_WRITE_OFTEN, (void**)&bits);
// Write bitmap data into 'bits' here
buffer->unlock();
// Create the EGLImageKHR from the native buffer
EGLint eglImgAttrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE, EGL_NONE };
EGLImageKHR img = eglCreateImageKHR(eglGetDisplay(EGL_DEFAULT_DISPLAY), EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID,
(EGLClientBuffer)buffer->getNativeBuffer(),
eglImgAttrs);
// Create GL texture, bind to GL_TEXTURE_2D, etc.
// Attach the EGLImage to whatever texture is bound to GL_TEXTURE_2D
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, img);
如果EGLClientBuffer的数据是YUV格式的,还可以使用纹理Target为:GL_TEXTURE_EXTERNAL_OES, 该Target也是主要用于从EGLImage中产生纹理的情景。
4.1 使用权限问题
Android NDK没有暴露相关接口,因此如果直接使用需要自行下载Android源码,编译并打包成动态库,需要注意的是,在API26之后,android已经禁止了私有native api的调用。不过在API 26以后,Android NDK提供了Hardware Buffer APIs类,来实现这样的功能,这个后续文章会详细说明,欢迎订阅。虽然大部分情况下我们直接使用SurfaceTexture即可。
4.2 EGLImageKHR的重要性质
EGLImageKHR其设计目的就是为了共享2D纹理数据的,因此驱动程序在底层实现的时候,往往实现了CPU与GPU对同一资源的访问,这样就可以做到无需拷贝的数据共享,降低功耗与提高性能。
在Android平台上,了解这点是非常重要的。而iOS平台由于使用了EAGL而不是EGL,因此并不会使用EGLImage,但也有自己的数据映射的方式。
5 共享纹理的两种实现
从上面可以知道,实现共享纹理,就可以有两种实现方式:
- 一种是EGL的ShareContext机制
- 一种是共享内存,这里就是EGLImageKHR
5.1 共享上下文
EGL的ShareContext是常见的共享上下文的方式(iOS平台的EAGL叫ShareGroup)。
/**
share_context:
Specifies another EGL rendering context with which to share data, as defined by the client API corresponding to the contexts. Data is also shared with all other contexts with which share_context shares data. EGL_NO_CONTEXT indicates that no sharing is to take place.
**/
EGLContext eglCreateContext( EGLDisplay display,
EGLConfig config,
EGLContext share_context,
EGLint const * attrib_list)
当参数传入另一个EGL的context时,这两个EGLContext就可以共享纹理以及VBO等。share_context
需要注意的是container objects不能被共享,比如:
- 帧缓冲器对象
- 顶点数组对象
- 转换反馈对象
- 程序管道对象
5.2 EGLIMAGEKHR
这实际上是一种共享内存的方式,以实现共享纹理,最简单就是直接使用SurfaceTexture,这里不再详细说明。当然也可以使用HardwareBuffer与EGLImageKHR来实现。