blog

AndroidのSurfaceTextureについて語ろう!

SurfaceTextureはAndroidでレンダリングするためのコアコンポーネントで、SurfaceとOpenGL ESテクスチャを組み合わせたもので、GLESテクスチャSurfaceに出力を提...

Apr 5, 2020 · 11 min. read
シェア

はじめに:この記事では、SurfaceTextureの使用と、あまり多くの記述の一般的な使用の原則を紹介しますが、テクスチャを共有する2つの方法の実装を含む内部実装とEGLImageに焦点を当てています。

SurfaceTextureとは

Androidのレンダリングシステムで言えば、SurfaceTextureはBufferQueueのコンシューマであり、アプリケーションがonFrameAvailable()コールバックによって通知されます。SurfaceTextureはBufferQueueのコンシューマであり、プロデューサが新しいバッファをキューにアップロードすると、onFrameAvailable()コールバックがアプリに通知します。アプリケーションは次にupdateTexImage()を呼び出します。updateTexImage()は、以前に占有されていたバッファを解放し、キューから新しいバッファをフェッチしてEGLコールを実行し、バッファを外部テクスチャとしてGLESが利用できるようにします。

基本的なプロセスは以下の通り:

  1. カメラ、ビデオデコーダー、OpenGLを通してイメージストリームを生成します。
  2. イメージストリームはSurfaceを通してBufferQueueにキューイングされ、GLConsumerに通知されます。
  3. OESテクスチャを取得したら、ユーザー側でノーマルテクスチャに変換し、エフェクトをかけたり、アップスクリーンにしたりすることができます。

SurfaceTexture

SurfaceTextureの最も一般的なアプリケーションシナリオは、カメラやビデオデコーダの出力です:

// SurfaceTextureGLSurfaceViewを使ったレンダリングのキーコード
// 表面テクスチャを初期化する
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)
}
// カメラプレビューテクスチャを設定する
camera.setPreviewTexture(surfaceTexture)
// glスレッドでテクスチャを更新する
fun updateTexImage() {
 internalSurfaceTexture.updateTexImage()
 internalSurfaceTexture.getTransformMatrix(transformMatrix)
}

SurfaceTextureの内部実装は- EGLImageKHR

SurfaceTexture を使用する場合の主な 2 つの方法:

  • SurfaceTexture // SurfaceTextureの作成
  • void updateTexImage () // 現在のイメージストリームをテクスチャに更新します。

SurfaceTexture作成方法

まず、SurfaceTexture に渡すテクスチャ ID を作成する必要があります。

 //テクスチャ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単一バッファであるかどうか、デフォルトはfalseである。
public SurfaceTexture(int texName, boolean singleBufferMode) {
 mCreatorLooper = Looper.myLooper();
 mIsSingleBuffered = singleBufferMode;
 //nativeメソッドnativeInit
 nativeInit(false, texName, singleBufferMode, new WeakReference<SurfaceTexture>(this));
}
//frameworks\base\core\jni\SurfaceTexture.cpp
//texNameアプリケーション用のテクスチャ名を作成する
//weakThizSurfaceTextureオブジェクトを弱く参照する。
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は偽である
 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);
 //プロデューサをenvに保存する。
 SurfaceTexture_setProducer(env, thiz, producer);
 jclass clazz = env->GetObjectClass(thiz);
 //JNISurfaceTextureContextGLConsumerから継承::FrameAvailableListener
 sp<JNISurfaceTextureContext> ctx(new JNISurfaceTextureContext(env, weakThiz,
 clazz));
 //surfaceTextureフレーム・コールバック・オブジェクトctxを設定する。
 //フレームデータを受け取ると、ctxがトリガーされる->onFrameAvailable方法
 surfaceTexture->setFrameAvailableListener(ctx);
 SurfaceTexture_setFrameAvailableListener(env, thiz, ctx);
}

JNISurfaceTextureContextSurfaceTexture.postEventFromNativeSurfaceTextureが初期化された後、リスナーがGLConsumerに設定され、Javaレイヤーのメソッドにコールバックされます。が利用可能であることを通知します。この時点で、ビジネスレイヤーはupdateTexImageを呼び出して、GraphicBufferをテクスチャに更新できます。

3.2 updateTexImageがテクスチャのデータを更新する方法

OnFrameAvailableListener.onFrameAvailableドキュメントによると、コールバックはどのスレッドでも起こりうるので、コールバックで直接updateTexImageを呼び出すことはできませんが、updateTexImageを呼び出すにはOpenGLスレッドに切り替えなければならないので、内部的にはどのように行われているのでしょうか?

GLConsumer::updateTexImage()アプリケーションレイヤーのupdateTexImageは、最終的にネイティブレイヤーのメソッドを呼び出します。

//frameworks
ative\libs\gui\GLConsumer.cpp
status_t GLConsumer::updateTexImage() {
 ....
 // mEglContexteglGetCurrentContextについて
 // 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
ative\libs\gui\GLConsumer.cpp
status_t GLConsumer::acquireBufferLocked(BufferItem *item,
 nsecs_t presentWhen, uint64_t maxFrameNumber) {
 //コンシューマーの現在の表示内容を取得する 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;
 //項目->mGraphicBufferEglImageを生成し、それを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テクスチャの種類
 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;
}

キーとなるステートメントはeglCreateImageKHRで、ここでEGLImageKHRオブジェクトが生成されていることがわかります。GL_TEXTURE_EXTERNAL_OESEGLImageKHRイメージが生成されたので、次にEGLImageKHRイメージがどのようにテクスチャにバインドされるかを分析します。

//frameworks
ative\libs\gui\GLConsumer.cpp
void GLConsumer::EglImage::bindToTextureTarget(uint32_t texTarget) {
 //テクスチャtexTargetのデータをmEglImageに更新する。
 //glTexImage2DまたはglTexSubImage2Dと同等である。
 glEGLImageTargetTexture2DOES(texTarget,
 static_cast<GLeglImageOES>(mEglImage));
}

EGLImageKHR

以上の分析を通して、SurfaceTextureの内部は主にEGLImageKHRを通していることが分かります。では、EGLImageKHRの役割は何でしょうか?EGLImageKHR2Dイメージデータを共有するためにEGLが定義した拡張フォーマットで、EGLの様々なクライアントapi間でデータを共有することができ、その意図は2Dイメージデータを共有することですが、共有されるデータの形式や共有の目的には明確な制限はありません。EGLImageKHRのcreate関数のプロトタイプは以下の通りです:

EGLImageKHR eglCreateImageKHR(
 EGLDisplay dpy,
 EGLContext ctx,
 EGLenum target,
 EGLClientBuffer buffer,
 const EGLint *attrib_list)

EGL_NATIVE_BUFFER_ANDROIDANativeWindowBufferを通してEGLImageオブジェクトの作成をサポートするために、AndroidではTarget calledが特別に定義されており、これはEGLImageオブジェクトの作成に使用されるデータに対応しています。

#define EGL_NATIVE_BUFFER_ANDROID 0x3140
#define EGL_IMAGE_PRESERVED_KHR 0x30D2
GraphicBuffer* buffer = new GraphicBuffer(, 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フォーマットである場合、テクスチャターゲットをGL_TEXTURE_EXTERNAL_OESとして使用することもできます。

4.1 アクセス権の問題

Android NDKはGraphicBuffer関連のインターフェイスを公開していないので、直接使用する場合はAndroidのソースコードをダウンロードし、コンパイルしてダイナミックライブラリにパッケージする必要があります。しかし、API 26以降、Android NDKはクラスを提供し、このような機能を実現しています。とはいえ、SurfaceTextureを直接使うのが一般的です。

EGLImageKHR

EGLImageKHRは2Dテクスチャデータを共有するように設計されているため、ドライバはCPUとGPUが同じリソースにアクセスするための基礎実装に実装されることが多く、データ共有がコピーなしで実現できるため、消費電力を削減し、パフォーマンスを向上させることができます。

Androidプラットフォームでは、これを理解することが非常に重要です。一方、iOSプラットフォームでは、EGLの代わりにEAGLを使用するため、EGLImageは使用しませんが、データをマッピングする独自の方法があります。

5 共有テクスチャの2つの実装

以上のことから、共有テクスチャを実装することで、2つの実装が可能であることがわかります:

5.1 ShareContext

EGLのShareContextは、コンテキストを共有する一般的な方法です。

/**
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)

share_contextパラメータが他のEGLのコンテキストに渡されると、2つのEGLContextはテクスチャやVBOなどを共有することができます。

コンテナ・オブジェクトは共有できないことに注意してください:

  • Framebuffer objects
  • Vertex array objects
  • Transform feedback objects
  • Program pipeline objects

5.2 EGLImageKHR

これは実際には共有テクスチャを実現するためにメモリを共有する方法であり、最も簡単なのはSurfaceTextureを直接使うことです。もちろん、EGLImageKHRでHardwareBufferを使うこともできます。

Read next

J32 論理と(||) 論理または(&&)

1.||:論理または2.&&:論理と1、条件判断の代わりにまたはそれぞれ2、JS式に表示され、対応する値に応じて、対応する結果の操作3、論理またはX = A | | B;まず第一に、Aの値が真または偽であることを確認するには、A場合

Apr 5, 2020 · 3 min read

アレイ・フラット化の実装

Apr 5, 2020 · 1 min read

redisストリーム

Apr 4, 2020 · 5 min read

アプレット

Apr 4, 2020 · 6 min read

ssrについて学ぶ

Apr 4, 2020 · 4 min read