Android NDK-EGL 初级

Android NDK-EGL 初级

在最近的工作中,发现很少有资料直接介绍android EGL的。

在翻越GLSurfaceView和Skia的源码之后,将我自己的NDK-EGL编程整理如下,供有需要的开发者取用

什么是EGL

EGL at a glance
EGL provides mechanisms for creating rendering surfaces onto which client APIs like OpenGL ES and OpenVG can draw, creates graphics contexts for client APIs, and synchronizes drawing by client APIs as well as native platform rendering APIs. This enables seamless rendering using both OpenGL ES and OpenVG for high-performance, accelerated, mixed-mode 2D and 3D rendering.

翻译:

EGL概览
EGL提供了创建surface的机制。该机制使得:客户端的绘图API,能够在surface上面绘制图形;能够创建图形上下文;能够同步客户端绘图API和本地平台渲染API。

这使得OpenGL ES和OpenVG 在高性能,加速,2D和3D混合渲染上,能够无缝处理。

EGL的使用步骤是什么

  1. 众所周知,要绘制就得知道绘制到什么什么屏幕上。故第一步就是指定需要绘制的屏幕
eglGetDisplay(EGL_DEFAULT_DISPLAY);
返回的是display
  1. 在知道display之后,就要对其进行初始化了
eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);
major,minor为返回的EGL版本
  1. 在知道显示屏幕之后,我们需要知道显示屏支持哪些格式,比如像素的格式是什么样子的,有没有哪些扩展格式,等
eglChooseConfig (EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);
attrib_list是需要查询的一些属性
config为查询到的配置

一般情况下,会选择config中的第一个,后文代码中会提及,注意查看
  1. 知道了display,知道了版本,也知道了config。接下来就是使用这些信息,绘制需要的上下文
eglCreateContext (EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);
唉,这里就是关键了

每次绘制都需要一个绘制上下文,这个上下文,保存绘制相关的一些状态。且上下文在各个线程中是不共享的。为了共享,可传递给第三个参数。
  1. 接下来就是创建,绘制区域了,绘制api将图形绘制在该区域上
 eglCreateWindowSurface (EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);

 该函数表示,创建一个能够显示在Window系统中的绘制区域(Surface)
 对应的window由EGLNativeWindowType代表

  1. 创建了上下文,也创建了绘制区域。接下来就是将当前线程和上下文进行绑定
eglMakeCurrent (EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
该函数将当前线程和绘制上下文进行绑定,并绑定draw和read surface

  1. 一切准备就绪,我们使用skia api进行绘制,放在后面介绍

  2. 绘制完成之后,将上面创建的surface,context,display分别销毁

eglDestroySurface (EGLDisplay dpy, EGLSurface surface);
销毁surface

eglDestroyContext (EGLDisplay dpy, EGLContext ctx);
销毁context

eglTerminate (EGLDisplay dpy);
销毁display

EGL如何在NDK中使用

上面说明了EGL的大致流程,接下来,看看如何在NDK中使用它。
首先创建一个SurfaceView.并为该SurfaceView设置一个回调监听,即SurfaceHolder.Callback.如下:

private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
    
    
        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
    
    
            native_surfaceCreate(surfaceHolder.getSurface());
            Timber.v("surfaceCreated");
        }

        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int width, int height) {
    
    
            native_surfaceChange(width, height);
            Timber.v("surfaceChanged");
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
    
    
            Timber.v("surfaceDestroyed");
            native_surfaceDestroy();
        }
    };

native_surfaceCreated,将会在c++ 层面创建一个绘制线程,在此不表,不过可以使用的api较多,如c++的std::thread,或者pthread,或者android独有的Android::Thread(只有系统编程人员可用)。在线程创建初期,进行EGL的初始化如下:

//下面的代码,严格按照上面EGL的步骤而设置
try {
    
    
        getDisplay();
        initilize();
        chooseConfig();
        createContext();
        lostContext = false;
    }
    catch (const std::runtime_error error) {
    
    
        ALOGE("error %s", error.what());
        throw error;
    }

初始化完成之后,接下来将SurfaceView创建的surface,传递给NDK中使用。

注意注意:前面说过SurfaceHolder.Callback的回调surfaceCreated,就已经创建好了surface,那么应该怎么传递给EGL使用呢?

这里就不得不说到,android的SurfaceFlinger和SurfaceView了。
1.SurfaceFlinger提供了一种机制,让别人能够调用SurfaceFlinger,创建一种图形产生和消耗的数据结构BufferQueue.这种机制之一叫做SurfaceControl.

2. SurfaceView使用SurfaceControl,让SurfaceFlinger产生一个BufferQueue.这个BufferQueue一头是SurfaceFlinger(消耗方).一头是SurfaceView(生产方).  这个BufferQueue创建出来的绘制区域都可以称为Surface。

3. 因此SurfaceHolder.Callback中的回调surfaceCreated,表示SurfaceView(生产方)使用的绘制区域已经创建完成。

4. 这个绘制区域在C层的实现就是使用的EGLNativeWindowType。因此调用eglCreateWindowSurface,就能创建对应的能在NDK中使用的绘制区域的代理对象了

从surface的java对象中获取,EGLNativeWindowType对象,如下:

JNIEXPORT void  JNICALL
Java_cn_xxxx_native_surfaceCreate(JNIEnv *env,jobject thiz,jobject surface) {
    
    
    ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
    if (window == nullptr) {
    
    
        ALOGE("can't get native window from surface object");
        return;
    }
    if (window != nullptr) {
    
    
        ANativeWindow_release(window);
        window = nullptr;
    }
}

其中ANativeWindow就是EGLNativeWindowType

故下面的代码直接创建EGLSurface
void xxxEgl::createSurface(EGLNativeWindowType win) {
    
    

    EGLint attr_none[] = {
    
    EGL_NONE};
    eglSurface = eglCreateWindowSurface(display, firstConfig, win, attr_none);
    if (eglSurface == NULL && eglGetError() != EGL_SUCCESS) {
    
    
        wrapMessageOrException(string("create surface error reason:") + to_string(eglGetError()));
    }
    if (eglMakeCurrent(display, eglSurface, eglSurface, context) == EGL_FALSE ||
        eglGetError() != EGL_SUCCESS) {
    
    
        wrapMessageOrException(
                string("create new surface context error reason:") + to_string(eglGetError()));
    }
}

接下来就是native_surfaceChange了。在这里面,将初始化skia库,并进行绘制我们想要的图片。

初始化skia的步骤如下

void xxxSkia::initEnvironment(int32_t width, int32_t height) {
    
    

	//GrGLCreateNativeInterface:返回与当前操作系统相关的OpenGL接口
	//使用GrGLInterface接管,返回的接口。后续使用通过GrGLInterface进行
    sk_sp<const GrGLInterface> interface(GrGLCreateNativeInterface());
    //接口不能为空
    SkASSERT(NULL != interface);
    
    //使用上面获取的GrGLInterface来创建并初始化一个图形上下文GrContext
    sk_sp<GrContext> grContext(
            GrContext::Create(kOpenGL_GrBackend, (GrBackendContext) interface.get()));//后端使用OPENGLES
    //图形上下文不能为空
    SkASSERT(grContext);
    //GR_GL_GetIntegerv获取interface对应的GR_GL_FRAMEBUFFER_BINDING参数的值,并保存在buffer中
    GrGLint buffer;
    GR_GL_GetIntegerv(interface.get(), GR_GL_FRAMEBUFFER_BINDING, &buffer);
    //创建后端渲染目标对象GrBackendRenderTarget,创建的时候,指定宽,高,采样数(默认是0),
    //模版缓冲区位数(默认是8),以及像素格式。
    //其中info就是关联的OpenGL帧缓冲信息。
    GrGLFramebufferInfo info;
    info.fFBOID = (GrGLuint) buffer;
    GrBackendRenderTarget target(width, height, sampleCount, stencilBit,
                                 kSkia8888_GrPixelConfig, info);
    //SkSurfaceProps 是 Skia 中用于描述绘图表面属性的类。它可以影响绘图表面的渲染行为和性能。
    //这里表示使用:传统字体托管(font host)相关的方式。
    SkSurfaceProps props(SkSurfaceProps::kLegacyFontHost_InitType);
    //有了后端渲染目标对象之后,就创建与之关联的绘制表面SkSurface.
    surface = SkSurface::MakeFromBackendRenderTarget(grContext.get(), target,
                                                     kBottomLeft_GrSurfaceOrigin,
                                                     nullptr, &props).release();
    mSurfaceHeight = height;
    mSurfaceWidth = width;
    //获取与surface相关的canvas,并在上面进行绘制。
    canvas = surface->getCanvas();
}

我们将使用skia的gpu后端,因此我们使用SkSurface::MakeFromBackendRenderTarget来创建对应的SkSurface对象。

在创建SkSurface对象时,需要传递,上下文接口,即grContext对象代表的逻辑。还需要传递目标绘制区域的一些信息,即target对象代表的逻辑。

创建完成之后,就可以使用skia的api进行绘制了。

那么需要在native_surfaceDestroy中,销毁必要的对象,除了EGL提及的,surface,context,display需要销毁以外,还需要销毁skia库中对应的SkSurface对象。请注意,请依照后创建先销毁的顺序,进行销毁

特别要注意的一点是:skia库,使用GPU后端进行渲染时。skia库可能创建一个gpu渲染上下文,因此有必要在绘制的地方,进行线程同步。比如,在我的例子中,进行了如下的同步:

void xxxSkia::draw() {
    
    
    //注意此处的锁
    std::lock_guard<std::mutex> _l(mutex);
    ATRACE_CALL();
    
    //一串字符串
    canvas->drawColor(SK_ColorWHITE);
    SkPaint paint;
    paint.setStyle(SkPaint::kFill_Style);
    paint.setAntiAlias(true);
    paint.setStrokeWidth(40);
    paint.setTextSize(40);
    paint.setColor(0xfffe938c);
    canvas->drawString("please set view", 100, 100, paint);
    ALOGD("this %p,context %p,surface %p",this,eglGetCurrentContext(),eglGetCurrentSurface(EGL_DRAW));
    
    canvas->flush();
}

使用上面的例子,成功将javacanvas的绘制,传递到了ndk中实现。附上一张实现结果图:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/xiaowanbiao123/article/details/122602403