OpenGL ES 共享上下文實現多線程渲染

OpenGL ES 共享上下文時,能夠共享哪些資源?c++

該原創文章首發於微信公衆號:字節流動git

OpenGL ES 共享上下文實現多線程渲染

EGL 概念回顧

EGL 是 OpenGL ES 和本地窗口系統(Native Window System)之間的通訊接口,它的主要做用:github

  • 與設備的原生窗口系統通訊;
  • 查詢繪圖表面的可用類型和配置;
  • 建立繪圖表面;
  • 在OpenGL ES 和其餘圖形渲染API之間同步渲染;
  • 管理紋理貼圖等渲染資源。

OpenGL ES 的平臺無關性正是藉助 EGL 實現的,EGL 屏蔽了不一樣平臺的差別(Apple 提供了本身的 EGL API 的 iOS 實現,自稱 EAGL)。數組

本地窗口相關的 API 提供了訪問本地窗口系統的接口,而 EGL 能夠建立渲染表面 EGLSurface ,同時提供了圖形渲染上下文 EGLContext,用來進行狀態管理,接下來 OpenGL ES 就能夠在這個渲染表面上繪製。微信

egl、opengles 和設備之間的關係

圖片中:markdown

  • Display(EGLDisplay) 是對實際顯示設備的抽象;
  • Surface(EGLSurface)是對用來存儲圖像的內存區域 FrameBuffer 的抽象,包括 Color Buffer(顏色緩衝區), Stencil Buffer(模板緩衝區) ,Depth Buffer(深度緩衝區);
  • Context (EGLContext) 存儲 OpenGL ES 繪圖的一些狀態信息;

在 Android 平臺上開發 OpenGL ES 應用時,類 GLSurfaceView 已經爲咱們提供了對 Display , Surface , Context 的管理,即 GLSurfaceView 內部實現了對 EGL 的封裝,能夠很方便地利用接口 GLSurfaceView.Renderer 的實現,使用 OpenGL ES API 進行渲染繪製,很大程度上提高了 OpenGLES 開發的便利性。多線程

固然咱們也能夠本身實現對 EGL 的封裝,本文就是在 Native 層對 EGL 進行封裝,不借助於 GLSurfaceView ,實現圖片後臺渲染,利用 GPU 完成對圖像的高效處理。函數

關於 EGL 更詳細的使用結束,能夠參考系列文章中的 OpenGL ES 3.0 開發(六):EGLoop

共享上下文時能夠共享哪些資源

共享上下文時,能夠跨線程共享哪些資源?這個是本文要講的重點。post

爲了照顧一些讀者大人的耐心,這裏直接說結論。

能夠共享的資源:

  • 紋理;
  • shader;
  • program 着色器程序;
  • buffer 類對象,如 VBO、 EBO、 RBO 等 。

不能夠共享的資源:

  • FBO 幀緩衝區對象(不屬於 buffer 類);
  • VAO 頂點數組對象(不屬於 buffer 類)。

這裏解釋下,在不能夠共享的資源中,FBO 和 VAO 屬於資源管理型對象,FBO 負責管理幾種緩衝區,自己不佔用資源,VAO 負責管理 VBO 或 EBO ,自己也不佔用資源。

結論說完了,將在下一節進行結論驗證,咱們將在主渲染線程以外開闢一個新的渲染線程,而後將主渲染線程生成的紋理、 program 等資源分享給新的渲染線程使用。

共享上下文多線程渲染

繪圖1.png

本小節將在主渲染線程以外經過共享 EGLContext 的方式開闢一個新的離屏渲染線程,以後將主渲染線程生成的紋理、 program 、VBO 資源分享給新的渲染線程使用,最後將保存(新渲染線程)渲染結果的紋理返回給主線程進行上屏渲染。

共享上下文

在 EGL_VERSION_1_4 (Android 5.0)版本,在當前渲染線程直接調用 eglGetCurrentContext 就能夠直接獲取到上下文對象 EGLContext 。

C++ ,Java 層均有對應獲取上下文對象的 API 實現:

//Java
EGL14.eglGetCurrentContext();

//C++
#include "egl.h"
eglGetCurrentContext();
複製代碼

咱們在新線程中使用 EGL 建立渲染環境時,經過主渲染線程獲取的 sharedContext 來建立新線程的上下文對象。

EGLContext context = eglCreateContext(mEGLDisplay, config,
                                              sharedContext, attrib2_list);
複製代碼

因爲咱們在新線程要渲染到屏幕外的區域,須要建立 PbufferSurface 。

EGLSurface eglSurface = eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, surfaceAttribs);
複製代碼

國際慣例,咱們將 EGL 的操做都封裝到一個類 EglCore 中方便使用,具體代碼能夠參考文末的項目。

多線程渲染

類比 Android Java 層的 Looper 類,咱們在 C++ 實現 Looper 用於建立新線程並管理線程中的消息。

class Looper {

public:
    Looper();
    Looper&operator=(const Looper& ) = delete;
    Looper(Looper&) = delete;
    virtual ~Looper();

    void postMessage(int what, bool flush = false);
    void postMessage(int what, void *obj, bool flush = false);
    void postMessage(int what, int arg1, int arg2, bool flush = false);
    void postMessage(int what, int arg1, int arg2, void *obj, bool flush = false);

    void quit();

    virtual void handleMessage(LooperMessage *msg);

private:
    void addMessage(LooperMessage *msg, bool flush);

    static void *trampoline(void *p);

    void loop(void);

    LooperMessage *head;
    pthread_t worker;
    sem_t headWriteProtect;
    sem_t headDataAvailable;
    bool running;

};
複製代碼

在 GLRenderLooper 類中分別定義 OnSurfaceCreated、 OnSurfaceChanged、 OnDrawFrame 用於處理對應的事件。

enum {
    MSG_SurfaceCreated,
    MSG_SurfaceChanged,
    MSG_DrawFrame,
    MSG_SurfaceDestroyed,
};

class GLRenderLooper : public Looper {
public:
    GLRenderLooper();
    virtual ~GLRenderLooper();

    static GLRenderLooper* GetInstance();
    static void ReleaseInstance();

private:
    virtual void handleMessage(LooperMessage *msg);

    void OnSurfaceCreated();
    void OnSurfaceChanged(int w, int h);
    void OnDrawFrame();
    void OnSurfaceDestroyed();

    bool CreateFrameBufferObj();

private:
    static mutex m_Mutex;
    static GLRenderLooper* m_Instance;

    GLEnv *m_GLEnv;
    EglCore *m_EglCore = nullptr;
    OffscreenSurface *m_OffscreenSurface = nullptr;
    GLuint m_VaoId;
    GLuint m_FboTextureId;
    GLuint m_FboId;
};
複製代碼

在函數 GLRenderLooper::OnSurfaceCreated 中,利用 sharedContext 建立 OpenGL 渲染環境。

void GLRenderLooper::OnSurfaceCreated() {

	//利用 sharedContext 建立 OpenGL 離屏渲染環境
    m_EglCore = new EglCore(m_GLEnv->sharedCtx, FLAG_RECORDABLE);
    SizeF imgSizeF = m_GLEnv->imgSize;
    m_OffscreenSurface = new OffscreenSurface(m_EglCore, imgSizeF.width, imgSizeF.height);
    m_OffscreenSurface->makeCurrent();

    glGenVertexArrays(1, &m_VaoId);
    glBindVertexArray(m_VaoId);

    glBindBuffer(GL_ARRAY_BUFFER, m_GLEnv->vboIds[0]);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (const void *)0);
    glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);

    glBindBuffer(GL_ARRAY_BUFFER, m_GLEnv->vboIds[1]);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (const void *)0);
    glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_GLEnv->vboIds[2]);
    GO_CHECK_GL_ERROR();
    glBindVertexArray(GL_NONE);

    if (!CreateFrameBufferObj())
    {
        LOGCATE("GLRenderLooper::OnSurfaceCreated CreateFrameBufferObj fail");
    }
}
複製代碼

GLRenderLooper::OnDrawFrame 函數中,繪製完成注意交換緩衝區,而後將保存繪製結果的紋理,經過回調函數傳遞給主線程進行上屏渲染。

void GLRenderLooper::OnDrawFrame() {
    LOGCATE("GLRenderLooper::OnDrawFrame");
    SizeF imgSizeF = m_GLEnv->imgSize;

    glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);
    glViewport(0, 0, imgSizeF.width, imgSizeF.height);
    glUseProgram(m_GLEnv->program);
    glBindVertexArray(m_VaoId);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_GLEnv->inputTexId);
    GLUtils::setInt(m_GLEnv->program, "s_TextureMap", 0);
    float offset = (sin(m_FrameIndex * MATH_PI / 80) + 1.0f) / 2.0f;
    GLUtils::setFloat(m_GLEnv->program, "u_Offset", offset);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
	
	//注意交換緩衝區
    m_OffscreenSurface->swapBuffers();
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

	//將保存繪製結果的紋理 m_FboTextureId 傳遞給主線程進行上屏渲染 
    m_GLEnv->renderDone(m_GLEnv->callbackCtx, m_FboTextureId);
    m_FrameIndex++;
}
複製代碼

回到渲染主線程,Init 時將主渲染生成的紋理、 program 、VBO 資源以及 EGLContext 傳遞給新線程。

m_GLEnv.sharedCtx     = eglGetCurrentContext();
m_GLEnv.program       = m_FboProgramObj;
m_GLEnv.inputTexId    = m_ImageTextureId;
m_GLEnv.vboIds[0]     = m_VboIds[0];
m_GLEnv.vboIds[1]     = m_VboIds[2];
m_GLEnv.vboIds[2]     = m_VboIds[3];
m_GLEnv.imgSize       = imgSize;
m_GLEnv.renderDone    = OnAsyncRenderDone;//主線程回調函數
m_GLEnv.callbackCtx   = this;

//將共享的資源發送給新線程
GLRenderLooper::GetInstance()->postMessage(MSG_SurfaceCreated, &m_GLEnv);

GLRenderLooper::GetInstance()->postMessage(MSG_SurfaceChanged, m_RenderImage.width, m_RenderImage.height);
複製代碼

主線程渲染時,首先向新線程發送渲染指令,而後等待其渲染結束,新線程渲染結束後會調用 OnAsyncRenderDone 函數通知主線程進行上屏渲染。

void SharedEGLContextSample::Draw(int screenW, int screenH) {
	{
		//向新線程發送渲染指令,而後等待其渲染結束
		unique_lock<mutex> lock(m_Mutex);
		GLRenderLooper::GetInstance()->postMessage(MSG_DrawFrame);
		m_Cond.wait(lock);
	}

	//主線程進行上屏渲染
	glViewport(0, 0, screenW, screenH);
	glUseProgram(m_ProgramObj);
	GO_CHECK_GL_ERROR();
	glBindVertexArray(m_VaoId);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
	GLUtils::setInt(m_ProgramObj, "s_TextureMap", 0);
	GO_CHECK_GL_ERROR();
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
	GO_CHECK_GL_ERROR();
	glBindTexture(GL_TEXTURE_2D, GL_NONE);
	glBindVertexArray(GL_NONE);

}

void SharedEGLContextSample::OnAsyncRenderDone(void *callback, int fboTexId) {
	//新線程渲染結束後會調用 OnAsyncRenderDone 函數通知主線程進行上屏渲染
	SharedEGLContextSample *ctx = static_cast<SharedEGLContextSample *>(callback);
	unique_lock<mutex> lock(ctx->m_Mutex);
	ctx->m_FboTextureId = fboTexId;
	ctx->m_Cond.notify_all();
}
複製代碼

最後須要注意的是:多線程渲染要確保紋理等共享資源不會被同時訪問,不然會致使渲染出錯。

完整代碼參考下面項目,選擇 Multi-Thread Render:

https://github.com/githubhaohao/NDK_OpenGLES_3_0
複製代碼

技術交流

技術交流/獲取源碼能夠添加個人微信:Byte-Flow

相關文章
相關標籤/搜索