导言:这篇文章介绍了SurfaceTexture的用法以及原理,对于常见用法不做过多描述,而重点介绍了内部实现以及EGLImage,包括实现共享纹理的两种方式。
1 什么是SurfaceTexture
2 SurfaceTexture的常见应用 - 相机与视频解码
3 SurfaceTexture的内部实现 - EGLImageKHR
3.1 SurfaceTexture是如何创建的
3.2 updateTexImage是如何将数据更新到纹理的
4 EGLImageKHR
4.1 使用权限问题
4.2 EGLImageKHR的重要性质
5 共享纹理的两种实现
5.1 ShareContext
5.2 EGLImageKHR
6 结语
1 什么是SurfaceTexture
SurfaceTexture
是Android上做渲染的核心组件,它是 Surface 和 OpenGL ES纹理的组合,用于提供输出到 GLES 纹理的 Surface。从安卓渲染系统上来说,SurfaceTexture
是一个BufferQueue的消费者,当生产方将新的缓冲区排入队列时,onFrameAvailable()
回调会通知应用。然后,应用调用 updateTexImage()
,这会释放先前占有的缓冲区,从队列中获取新缓冲区并执行 EGL 调用,从而使 GLES 可将此缓冲区作为外部纹理使用。
基本流程如下:
通过Camera、Video Decoder、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初始化后,向GLConsumer
设置了JNISurfaceTextureContext
监听器,该监听器会回调到Java层SurfaceTexture.postEventFromNative
方法,进一步回调到注册到SurfaceTexture中的OnFrameAvailableListener监听器,用于通知业务层有新的GraphicBuffer
入队了。这时候业务层就可以调用updateTexImage
将GraphicBuffer更新到纹理。
3.2 updateTexImage是如何将数据更新到纹理的
按文档所说,OnFrameAvailableListener.onFrameAvailable
回调可以发生在任意线程,所以不能在回调中直接调用updateTexImage
,而是必须切换到OpenGL线程调用updateTexImage
,那么内部是怎么实现的呢?
应用层的updateTexImage
会最终调用到native层的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;
}
关键语句是eglCreateImageKHR
,可以看到,这里创建了EGLImageKHR 对象。现在已经生成了EGLImageKHR图像,接下来分析下 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));
}
小结一下,整个updateTexImage
方法大致分为这几步:
生成纹理内容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系统中专门定义了一个称为EGL_NATIVE_BUFFER_ANDROID
的Target,支持通ANativeWindowBuffer
创建EGLImage对象,而Buffer则对应创建EGLImage对象时使用数据。
而在Android上定义的EGLClientBuffer
是在native中定义的GraphicBuffer
类,综上,EGLImageKHR
的基本使用流程如下:
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没有暴露GraphicBuffer
相关接口,因此如果直接使用需要自行下载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 ShareContext
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)
当share_context
参数传入另一个EGL的context时,这两个EGLContext就可以共享纹理以及VBO等。
需要注意的是container objects不能被共享,比如:
Framebuffer objects Vertex array objects Transform feedback objects Program pipeline objects
5.2 EGLImageKHR
这实际上是一种共享内存的方式,以实现共享纹理,最简单就是直接使用SurfaceTexture,这里不再详细说明。当然也可以使用HardwareBuffer与EGLImageKHR来实现。
6 结语
这篇文章主要介绍了SurfaceTexture的原理,下篇文章将主要介绍SurfaceTexture与共享内存的应用,敬请期待。
-- END --
进技术交流群,扫码添加我的微信:Byte-Flow
获取视频教程和源码
推荐:
Android FFmpeg 实现带滤镜的微信小视频录制功能