Go back

Modifying GLSurfaceView for Zero-Copy Texture Sharing between Views

Published:  at  05:10 AM
⏱️ 1575 words • 8 min read

阅读中文版

Forking AOSP's GLSurfaceView into GL14SurfaceView to share GPU textures between EGL14 contexts and render to multiple views without CPU copies.

Problem: Textures cannot be shared between two GLSurfaceViews

Let’s talk about an actual scenario: you use OpenGL ES to render content (such as video frames, 3D scenes, custom GL animations) on a GLSurfaceView, and now you want to display the same screen on another View.

The intuitive approach is to pass the textures ID and let the second View draw it directly glBindTexture. But this doesn’t work - the two GLSurfaceViews each have independent EGL Contexts, and the textures created by Context A do not exist in Context B.

The EGL specification provides a solution: when creating Context B, you can specify it to be shared with Context A. After sharing, A’s textures, Buffer Object, and Shader Program are all visible in B.

This mechanism is called Shared Context, and the third parameter of eglCreateContext is used to pass shared objects.

The problem is that Android’s GLSurfaceView doesn’t give you this opportunity.

GLSurfaceView EGL version problem

Android’s GLSurfaceView internally uses the javax.microedition.khronos.egl package - this is the Java binding for EGL 1.0, available since API 1. Its EGLContextFactory interface looks like this:

// android.opengl.GLSurfaceView.EGLContextFactory
public interface EGLContextFactory {
    javax.microedition.khronos.egl.EGLContext createContext(
        javax.microedition.khronos.egl.EGL10 egl,
        javax.microedition.khronos.egl.EGLDisplay display,
        javax.microedition.khronos.egl.EGLConfig eglConfig
    );
    void destroyContext(
        javax.microedition.khronos.egl.EGL10 egl,
        javax.microedition.khronos.egl.EGLDisplay display,
        javax.microedition.khronos.egl.EGLContext context
    );
}

The parameters and return values are all types under javax.microedition.khronos.egl.

Starting from API 17, Android has provided android.opengl.EGL14, EGL 1.4 binding, which is also the EGL interface currently in mainstream use. Many third-party SDKs (video SDKs, camera libraries, etc.) use EGL14 internally, and the Context type called back to you is android.opengl.EGLContext.

There is no inheritance relationship between these two classes javax.microedition.khronos.egl.EGLContext and android.opengl.EGLContext, and they cannot be converted to each other:

javax.microedition.khronos.egl.EGLContext  -- GLSurfaceView 用的
android.opengl.EGLContext                   -- EGL14 / 第三方 SDK 用的

So if you want a GLSurfaceView to share textures with an EGL14 Context, the standard API can’t do it. The parameter type of EGLContextFactory blocks the road.

Even if a third-party SDK is not involved, if you want to share Context between two GLSurfaceViews, the original interface is very awkward - you have to fiddle with the old EGL10 interface, and the EGL10 API is much more difficult to use than EGL14.

Solution: fork GL14SurfaceView from AOSP

The idea is very straightforward: copy out the source code of GLSurfaceView and replace all internal javax.microedition.khronos.egl calls with android.opengl.EGL14.

This is GL14SurfaceView. The logic is exactly the same as the original version - GLThread rendering loop, EglHelper managing EGL life cycle, SurfaceHolder.Callback handling Surface creation and destruction - only the EGL interface has been changed from 1.0 to 1.4.

Interface transformation

Original DefaultContextFactory:

// 原版 GLSurfaceView 内部 (javax.microedition.khronos.egl)
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
    return egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
}

Change to EGL14:

// GL14SurfaceView 内部 (android.opengl.EGL14)
public android.opengl.EGLContext createContext(
    android.opengl.EGLDisplay display,
    android.opengl.EGLConfig config
) {
    int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL14.EGL_NONE};
    return EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, attrib_list, 0);
}

The externally exposed EGLContextFactory interface has also changed:

public interface EGLContextFactory {
    android.opengl.EGLContext createContext(EGLDisplay display, EGLConfig eglConfig);
    void destroyContext(EGLDisplay display, android.opengl.EGLContext context);
}

EglHelper transformation

In EglHelper, the entire EGL life cycle is switched to the EGL14 static method. The changes are mechanical, here are a few examples:

// 获取 Display
mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);

// 初始化
EGL14.eglInitialize(mEglDisplay, version, 0, version, 1);

// 创建 Context(通过 EGLContextFactory,允许外部注入 sharedContext)
mEglContext = view.mEGLContextFactory.createContext(mEglDisplay, mEglConfig);

// 创建 Window Surface
mEglSurface = EGL14.eglCreateWindowSurface(display, config, nativeWindow, null, 0);

// 绑定
EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);

// 交换缓冲
EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);

In addition, add an additional method to allow the outside world to obtain the EGLContext of the current GLThread:

public EGLContext getEglContext() {
    return mGLThread.getEglContext();
}

This will be used later when doing Context sharing.

Usage: Shared across View textures

With GL14SurfaceView, Context sharing is natural.

SharedEGLContextFactory

Write an implementation of EGLContextFactory and pass the Context to be shared when eglCreateContext:

class SharedEGLContextFactory(
    private val sharedContext: EGLContext
) : GL14SurfaceView.EGLContextFactory {

    private val EGL_CONTEXT_CLIENT_VERSION = 0x3098

    override fun createContext(display: EGLDisplay, eglConfig: EGLConfig): EGLContext {
        val attribList = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
        return EGL14.eglCreateContext(
            display,
            eglConfig,
            sharedContext,  // 替代 EGL_NO_CONTEXT
            attribList,
            0
        )
    }

    override fun destroyContext(display: EGLDisplay?, context: EGLContext?) {
        EGL14.eglDestroyContext(display, context)
    }
}

The change is just one line - EGL14.EGL_NO_CONTEXT replaced by sharedContext. The effect is that the new Context and the incoming Context share the textures namespace.

Example: Two Views display the same textures

Assume that viewA is the main rendering View, which creates a texture and draws things on it. Now I want viewB to display the same content.

// viewA: 主渲染 View,正常创建
val viewA = GL14SurfaceView(context).apply {
    setEGLContextClientVersion(2)
    setRenderer(mainRenderer)
}

// mainRenderer 在 onSurfaceCreated 里创建了一个纹理
// textureId 保存在某个共享变量里

After the GL thread of viewA runs and gets the EGLContext, create viewB:

// viewB: 共享 viewA 的 Context
val viewB = GL14SurfaceView(context).apply {
    setEGLContextFactory(SharedEGLContextFactory(viewA.eglContext))
    setEGLContextClientVersion(2)
    setRenderer(mirrorRenderer)  // 直接用 viewA 创建的 textureId 绘制
    renderMode = GL14SurfaceView.RENDERMODE_WHEN_DIRTY
}

The textures created by viewA can be directly tied to onDrawFrame of mirrorRenderer:

val mirrorRenderer = object : GL14SurfaceView.Renderer {
    override fun onSurfaceCreated(config: EGLConfig?) {
        // 不需要再创建纹理,viewA 的纹理在这个 Context 里已经可见
    }
    override fun onSurfaceChanged(width: Int, height: Int) {
        GLES20.glViewport(0, 0, width, height)
    }
    override fun onDrawFrame() {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
        // 直接用 viewA 的 textureId 画
        drawTexture(sharedTextureId)
    }
}

There is no glReadPixels, no Bitmap transfer, and no pixel data transfer involving the CPU. The two Views draw the same texture data in the GPU memory.

Can continue to expand

The same Context can be shared by multiple Views. When creating viewC and viewD, they all point to the same sharedContext, and N-way distribution can be done. Each additional channel only adds one draw call overhead.

Practical application scenarios

This solution can be used in many places:

  • Video conferencing multi-window: When the SDK only provides one Surface, use shared Context to distribute the same video to the main screen, thumbnails, and picture-in-picture
  • Third-party SDK textures interception: Many video/map/advertising SDKs use EGL14 internally for rendering. After getting the textureId and EGLContext through callbacks, you can use this solution for secondary rendering.
  • GL rendering result reuse: For example, the output of a 3D rendering engine needs to appear in the main interface and the floating window at the same time.
  • Multi-screen mirroring: The same rendering result is output to the internal screen and external monitor at the same time

Give an example of how to use a third-party SDK. For example, a video SDK gives you textureId and eglContext in the callback:

sdk.setVideoFrameCallback { textureId, eglContext ->
    if (surfaceView == null) {
        // 首次回调:创建共享 Context 的 GL14SurfaceView
        surfaceView = GL14SurfaceView(context).apply {
            setEGLContextFactory(SharedEGLContextFactory(eglContext))
            setEGLContextClientVersion(2)
            setRenderer(myRenderer)
            renderMode = GL14SurfaceView.RENDERMODE_WHEN_DIRTY
        }
        container.addView(surfaceView)
    }
    // 后续回调:更新纹理 ID,请求重绘
    currentTextureId = textureId
    surfaceView?.requestRender()
}

There is no need for YUV callbacks, no CPU transcoding, and no need to open multiple SDK instances.

Things to note

Thread safety

The GLThreads of the two Views are different threads. If the writing and reading of textures ID are in different threads, attention should be paid to synchronization. requestRender() itself is thread-safe (with synchronized internally) and can be called from any thread.

If you encounter occasional screen tearing, you can add glFenceSync / glWaitSync at the end of writing textures.

life cycle

Shared Context has dependencies: if viewA’s Context is destroyed, and viewB is still using shared textures, EGL_BAD_CONTEXT will crash directly.

Pay attention to the order of destruction - first stop the consumer (the sharing party), then stop the producer (the shared party).

preserveEGLContextOnPause

It is recommended to set both Views to true. Otherwise, the Context will be destroyed when the Activity is paused, and the sharing relationship will be broken after the Activity is resumed, and it will have to be re-established.

OES textures

If the shared textures type is GL_TEXTURE_EXTERNAL_OES (external textures, common in camera preview and video decoding), it must be declared in the Shader:

#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES sTexture;

Ordinary sampler2D cannot sample OES textures and will cause a black screen.

Comparison with YUV callback scheme

If Context sharing is not done, a common alternative is to read the pixel data from the GPU back to the CPU (glReadPixels or YUV callback), and then upload it to another View. The overhead at 1080P 30fps is approximately:

YUV CallbackShared Context
Data PathGPU -> CPU -> GPUGPU -> GPU
Extra CPU usage~25%Close to zero
Single frame delay40~60ms<5ms
Memory transfer per frame~6MB (1080P YUV)0
Expansion overheadDoubled for each additional channelOne more draw call for each additional channel

The source of the gap is very direct: the YUV solution has two cross-bus transmissions (GPU->CPU readback + CPU->GPU upload), and the data in the shared Context solution is always in the GPU memory.

Summary

The changes to GL14SurfaceView are not major - the EGL calls are changed from the AOSP fork, and the actual changes do not exceed 100 lines. SharedEGLContextFactory is shorter, about 20 lines. But with these two things, sharing of GPU textures across Views on Android is possible, and pixel data no longer needs to go around the CPU.

The code itself is not complicated. The complicated thing is to figure out why the standard GLSurfaceView can’t do this - the type isolation of the two sets of EGL interfaces is not easy to realize without looking through the AOSP source code.


Share this post on:

Previous Post
Publishing on Google Play as an Independent Developer: My 2025 Experience
Next Post
Driving a WS2812 RGB LED over SPI on the DWM3001CDK