暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

OpenGL之EGLImage

周末随心分享 2021-07-11
10409

背景

EGLImage代表一种由EGL客户API(如OpenGL,OpenVG)创建的共享资源类型。它的本意是共享2D图像数据,但是并没有明确限定共享数据的格式以及共享的目的,所以理论上来讲,应用程序以及相关的客户API可以基于任意的目的创建任意类型的共享数据


相信小伙伴们都知道,客户端程序无法直接访问GPU内存,所以我们上传纹理数据或者读取纹理都要经历一次拷贝过程,如果只是一两次操作还能接受,但是像视频渲染那种需要1秒内达到30帧速度情况就会很吃力,那目前是如何解决这个问题的呢?这就涉及今天要介绍的EGLImage了


EGLImage原理

1、EGLImage创建,我们先来看一段样例代码:

    #include <EGL/eglext.h>
    #include <GLES2/gl2ext.h>
    #ifdef ANDROID
    GraphicBuffer * pGraphicBuffer = new GraphicBuffer(ImageWidth, ImageHeight, PIXEL_FORMAT_RGB_565, GraphicBuffer::USAGE_SW_WRITE_OFTEN | GraphicBuffer::USAGE_HW_TEXTURE);
    // Lock the buffer to get a pointer
    unsigned char * pBitmap = NULL;
    pGraphicBuffer->lock(GraphicBuffer::USAGE_SW_WRITE_OFTEN,(void **)&pBitmap);
    // Write 2D image to pBitmap


    // Unlock to allow OpenGL ES to use it
    pGraphicBuffer->unlock();


    EGLClientBuffer ClientBufferAddress = pGraphicBuffer->getNativeBuffer();
    EGLint SurfaceType = EGL_NATIVE_BUFFER_ANDROID;
    #else
    EGLint SurfaceType = EGL_GL_TEXTURE_2D_KHR;
    #endif


    // Make an EGL Image at the same address of the native client buffer
    EGLDisplay eglDisplayHandle = eglGetDisplay(EGL_DEFAULT_DISPLAY);


    // Create an EGL Image with these attributes
    EGLint eglImageAttributes[] = {EGL_WIDTH, ImageWidth, EGL_HEIGHT, ImageHeight, EGL_MATCH_FORMAT_KHR, EGL_FORMAT_RGB_565_KHR, EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};


    EGLImageKHR eglImageHandle = eglCreateImageKHR(eglDisplayHandle, EGL_NO_CONTEXT, SurfaceType, ClientBufferAddress, eglImageAttributes);


    // Create a texture and bind it to GL_TEXTURE_2D
    EGLint TextureHandle;
    glGenTextures(1, &TextureHandle);
    glBindTexture(GL_TEXTURE_2D, TextureHandle);


    // Attach the EGL Image to the same texture
    glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImageHandle);
    复制

    上面代码介绍了如何创建一个EGLImage,以及如何将它与纹理绑定,主要过程包括:

    • 创建GraphicBuffer,该对象定义在framework/native中,因此必须引入framework相关头文件

    • 创建EGLImage,创建过程需要把GraphicBuffer的buffer地址传进去作为数据载体

    • 将EGLImage于纹理绑定,绑定目的是让纹理与GraphicBuffer共享数据地址,这样就能减少一次拷贝,极大提高数据传输速度

    那么问题来了,为什么GraphicBuffer能够与纹理共享数据呢?不急,我们接着往下看


    2、GraphicBuffer原理

    定义:

    GraphicBuffer是可以进程间共享的buffer,进程间传递的时候,除了一些GraphicBuffer中宽高参数传递之外,还有一个重要的参数,就是这个buffer对应的fd。GraphicBuffer对应的物理buffer,在kernel中有对应的管理,也就是物理buffer的引用计数,对于上层不同的应用,每个应用都可以用一个fd对应这个物理buffer,这个fd在不同的进程中也不一定相同


    在介绍之前,大家可以先回顾一下binder机制,GraphicBuffer同样利用了binder机制进行创建与传递,先来看一下它的定义:

      status_t GraphicBuffer::flatten(void* buffer, size_t size,
      int fds[], size_t count) const
      {
      size_t sizeNeeded = GraphicBuffer::getFlattenedSize();
      if (size < sizeNeeded) return NO_MEMORY;
      size_t fdCountNeeded = GraphicBuffer::getFdCount();
      if (count < fdCountNeeded) return NO_MEMORY;
      int* buf = static_cast<int*>(buffer);
      buf[0] = 'GBFR';
      buf[1] = width;
      buf[2] = height;
      buf[3] = stride;
      buf[4] = format;
      buf[5] = usage;
      buf[6] = 0;
      buf[7] = 0;
      if (handle) {
      buf[6] = handle->numFds;
      buf[7] = handle->numInts;
      native_handle_t const* const h = handle;
      memcpy(fds, h->data, h->numFds*sizeof(int));
      memcpy(&buf[8], h->data + h->numFds, h->numInts*sizeof(int));
      }
      return NO_ERROR;
      }
      复制

      GraphicBuffer继承了flatten对象,而flatten是binder数据传输的结构体,因此GraphicBuffer就能像普通数据一样通过binder传输给其他进程,所以说它是进程间可共享结构


      那如何获取IGraphicBufferAlloc的远程Binder代理对象呢?IGraphicBufferAlloc被定义为无名Binder对象,并没有注册到ServiceManager进程中,但SurfaceFlinger是有名Binder对象,因此可以通过SurfaceFlinger创建IGraphicBufferAlloc的本地对象GraphicBufferAlloc,并返回其远程代理对象BpGraphicBufferAlloc给客户端进程。


      那客户端进程又是如何请求SurfaceFlinger创建IGraphicBufferAlloc的本地对象的呢?客户端进程首先从ServiceManager进程中查询SurfaceFlinger的远程代理对象BpSurfaceComposer,然后通过它来进行创建,主要流程如下:


      通过binder机制传输,客户端就能创建GraphicBuffer以及拥有它的句柄,当需要读写数据时只要从句柄所在的地址中读写数据,然后通知Opengl重新渲染就可以完成CPU与GPU数据的传输


      可能小伙伴还会疑惑,为什么GraphicBuffer可以直接与纹理绑定数据呢,答案很简单,因为GraphicBuffer分配的空间也是在GPU中,所以纹理可以访问的到,自然也就能进行绑定了


      3、EGLImage使用

      经过上面介绍,大家对EGLImage有了初步了解,那平时开发过程中我们有用到它吗?答案是肯定的,举个例子:

      做过相机应用的同学都对SurfaceTexture不陌生吧,它可以与相机实例绑定,当相机预览数据过来时会首先通知SurfaceTexture,然后我们调用updateTexImage释放之前持有的缓冲区,将新的缓冲区作为纹理数据。


      那这里面是用到了什么技术呢,为什么会比普通纹理数据上传快那么多?,先来看一下shader脚本吧:

        #extension GL_OES_EGL_image_external : require


        precision mediump float;


        varying vec2 vTextureCoord;
        uniform samplerExternalOES sTexture;


        const vec3 monoMultiplier = vec3(0.299, 0.587, 0.114);


        void main() {
        vec4 color = texture2D(sTexture, vTextureCoord);
        float monoColor = dot(color.rgb,monoMultiplier);
        gl_FragColor = vec4(monoColor, monoColor, monoColor, 1.0);
        }
        复制

        上面就是使用SurfaceTexture时使用的shader脚本,是不是看到了熟悉的字眼:EGL_image,没错,它就是系统提供的EGLImage扩展,通过使用它就和我们一开始给的样例代码一样会创建EGLImage对象,并且更新相机预览数据时直接更新GraphicBuffer指向的数据就行。


        EGLImage除了持有GraphicBuffer句柄以外还会处理YUV和RGB的转换问题,所以视频数据能够在shader脚本里直接使用



        总结

        EGLImage的使用使得应用某种意义上能够“直接访问GPU”,但是Android NDK没有暴露GraphicBuffer相关接口,因此如果直接使用需要自行下载Android源码,编译并打包成动态库,需要注意的是,在API26之后,android已经禁止了私有native api的调用(意味着反射也无法访问一些未暴露接口)。不过在API 26以后,Android NDK提供了Hardware Buffer APIs类,HardwareBuffer 是 Android 8 API >= 26 提供的用于替换 GraphicBuffer 的接口,使用方式和GraphicBuffer一样


        对比读取相同格式 3k 左右分辨率图像的性能,其中 ImageReader、 PBO 和 HardwareBuffer 明显优于 glReadPixels 方式,HardwareBuffer、 ImageReader 以及 PBO 三种方式性能相差不大,但是理论上 HardwareBuffer 性能最优。


        四种方式中,glReadPixels 使用最方便,HardwareBuffer 实现最复杂,实现复杂度:HardwareBuffer > PBO > ImageReader > glReadPixels 。

        结合实测性能和实现难度,Native 层建议选择 PBO 方式,超大分辨率建议尝试 HardwareBuffer 方式,Java 层建议使用 ImageReader 方式


        好了,今天的分享就到这了,下周继续~

        文章转载自周末随心分享,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

        评论