Android 的 SurfaceTexture原理

1 什么是表面纹理

SurfaceTexture是Android上做渲染的核心组件,它是 Surface 和 OpenGL ES纹理的组合,用于提供输出到 GLES 纹理的 Surface。从安卓渲染系统上来说, 是一个BufferQueue的消费者,当生产方将新的缓冲区排入队列时, 回调会通知应用。然后应用调用 ,这会释放先前占有的缓冲区,从队列中获取新缓冲区并执行 EGL 调用,从而使 GLES 可将此缓冲区作为外部纹理使用。

基本流程如下:

  1. 通过Camera、Video解码器、OpenGL生成图像流。
  2. 图像流通过Surface入队到BufferQueue,并通知到GLConsumer。
  3. GLConsumer从BufferQueue获取图像流GraphicBuffer,并转换为EXTERNAL OES纹理。
  4. 得到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来实现。

原内容:https://www.androidos.net.cn/doc/2022/7/4/335.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容