AndroidのOpenGL|ESで、オフスクリーン描画しててハマったぁ ガ━━(;゚Д゚)━━ン!!
そもそもは何がしたかったかと言うと、
- UVCカメラからの画像を複数のSurfaceに書き込みたい
直ぐに思いつくのはプレビュー表示用と、MediaCodecでの録画用Surfaceへの書き込みだけど、将来的にグレースケールとかセピア色にするとかのイメージプロセッシング処理を入れようとした時に、元画像と処理後を同時に表示したいですよね。 - 2つ目以降のSurfaceへの描画は、1つ目の描画時にCPUからGPUへ転送したのをGPU内で転送(描画)するだけにしたい。
ピクセルフォーマット変換や、CPU側のメモリをGPUへ転送する処理は負荷が高いから1回だけで済ませたいですよね。 - 画面に表示しなくても使えるようにしたい
今のUsbWebCamera/UsbWebCameraProの実装だと、プレビュー画面が必須なのでバックグラウンド録画とか出来ない。でも画面に表示しない方が消費電力的には有利だし、防犯カメラ的な用途だと常時表示する必要もないですよね。 - UI処理やイメージプロセッシング処理と、UVCカメラからの画像取得処理を分けたい
これは上の「画面に表示しなくても使えるようにしたい」ってとことも関係するけど、今は1個のアプリ内に全部押し込んでいるのでミドルウエア的な部分とアプリケーションレイヤが一体になっているけど、画像取得用のサービスとして分離したいってのもあるし、NDKを使わずにもっと簡単にUVCカメラへアクセス出来るようにしたい。
って言う事で、一般的にこういう時には、画面に直接描画するのではなくオフスククリーン描画ってのをします。いきなり本ちゃんアプリを修正するのはリスク高すぎなので、オフスクリーン描画のテスト用アプリを作りました。
オフスクリーン描画では、画面とは別にフレームバッファ的な?何かを作成して一旦そこに描画してから、実際の画面に転送(描画)しますが、メリット・デメリットはグルグル先生に聞いてもらうことにしましょう(笑)
今回のテストでは、プライベートスレッドでEGLContext・テクスチャ・SurfaceTextureを生成してそれをオフスクリーンとして使用しました。オフスクリーンへの描画後、SurfaceTexture#updateTexImageを呼び出してテクスチャを更新した後、#getTransformMatrixでテクスチャ変換行列を取得して、他のSurface等へ描画します。
問題となった描画周りはこんな感じのコードでした。mMasterTextureがオフスクリーン用のSurfaceTextureで、mClientsに描画先のオブジェクトが入っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
try { mMasterTexture.updateTexImage(); mMasterTexture.getTransformMatrix(mTexMatrix); } catch (Exception e) { Log.e(TAG, "draw:thread id =" + Thread.currentThread().getId(), e); return; } synchronized (mSync) { final int n = mClients.size(); for (int i = 0; i < n; i++) { mClients.valueAt(i).draw(mTexId, mTexMatrix); } } |
どこにハマったかというと、#updateTexImageです。手元のNexus5とNexus7では上のコードのままで普通に実行できたのですが、GT-N7100(GALAXY Note2のinternational版)では毎フレーム#updateTexImageでエラー発生するのです。正確に言うと、1フレーム目のみは正常に描画できて2フレーム目以降毎回エラーが発生していました。1フレーム目だけ描画できて後はカッチコチだなんて悔しいですっ(汗)
どんなエラーかというと、これです。上のコードのcatch文で出力したlogです。
1 2 3 4 5 6 7 8 |
[unnamed-19659-1] syncForReleaseLocked: error dup'ing native fence fd: 0x3000 draw:thread id =3115 java.lang.RuntimeException: Error during updateTexImage (see logcat for details) at android.graphics.SurfaceTexture.nativeUpdateTexImage(Native Method) at android.graphics.SurfaceTexture.updateTexImage(SurfaceTexture.java:169) at com.serenegiant.glutils.RendererHolder.draw(RendererHolder.java:188) at com.serenegiant.glutils.RendererHolder.run(RendererHolder.java:243) at java.lang.Thread.run(Thread.java:841) |
詳細はlogCatを見ろって言われたって、#updateTexImageがずっこけてる事以外判りません(●`ε´●)
グルグル先生に聞いてみても役に立つのは見つかりませんでした。
ちなみに今回のハマりどころとは関係ありませんが、SurfaceTexture#updateTexImageでエラーが起こる原因で一番多いのは実装上のミスで、SurfaceTextureを生成したのと異なるスレッドで#updateTexImageを呼び出してしまうことです。
でも今回はオフスクリーン描画なので専用スレッドに専用のEGLContextを生成して実行しているのでスレッドは間違えてません(上のコードでlogCatにスレッドIDを出力しているのは一応確認するためです)。
TextureViewから取得したSurfaceTextureを使う分にはこんなエラーは発生しないので、何か自分のコード内で設定が足りない/実装を間違っていることがあるようなのです。
SurfaceTextureのソースを見たり、大元のGLConsumerのソースを見たりしたものの原因不明。エラーが端末のOpenGL|ES/EGL関係のドライバ内で起こってるって事以外判りませんでした。
悩むこと3日
他の仕事もしてるので、これだけで3日潰したわけじゃなけどね。色々手を変え品を変え試した結果、ようやく解決出来ました。じゃじゃ〜ん、とりあえずGT-N7100でもうまく動くコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
try { mMasterTexture.updateTexImage(); mMasterTexture.getTransformMatrix(mTexMatrix); } catch (Exception e) { Log.e(TAG, "draw:thread id =" + Thread.currentThread().getId(), e); return; } synchronized (mSync) { final int n = mClients.size(); for (int i = 0; i < n; i++) { mClients.valueAt(i).draw(mTexId, mTexMatrix); } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glFlush(); |
どこが違うか分かりますか?
最後に描画処理と#glFlushを追加しただけです。結果をまとめるとこんな感じになります。updateTexImage後に、このEGLレンダリングコンテキスト内で、
- 描画命令+#glFlushはOK
- 描画命令+#glFinishはOK
- 描画命令だけはNG
- #glFlushだけはNG
- #glFinshだけはNG
- ステート変更だけのOpenGL|ESコマンド(#glEnableとか)+#glFlushはNG
- ステート変更だけのOpenGL|ESコマンド+#glFinishはNG
- カレントのEGLレンダリングコンテキストで#eglSwapBuffers呼び出してもNG
このスレッド&EGLレンダリングコンテキストはオフスクリーンのテクスチャ(&SurfaceTexture)を保持するのが目的で、実際のSurfaceへの描画は共有コンテキストを使って別スレッドで行っています。なので、プログラム上はここで描画する必要は全くないのですが・・・しかも、描画処理と言ってもとりあえず塗りつぶしているだけで、SurfaceTextureもその元になっているテクスチャもここでの描画には使っていません。もしかしたらテクスチャを使って描画した方が良いのかなぁ?。
エラーの内容、上の実行結果とコードから推測すると、GT-N7100のEGL/OpenGL|ESのドライバは、実際に描画命令を実行しないとOpenGL|ESの同期用オブジェクトの更新処理が出来ないようです。
こんなのわかんねーよぉ〜(~_~メ)
まぁ、元々EGL14(API>=17)を使って実装していたEGLContextの生成処理をEGL10(API>=1)を使うように変更したりもしたので、動くAPIレベルも広がって良かったと言うことにしときましょう。でなきゃやってらんねー(●`ε´●)
結論
と言う事で結論、
- SurfaceTextureを生成したスレッドでは何でもいいけど描画しないといけない。たぶん。
- SurfaceTextureを生成したスレッドでは描画した後#glFlushか#glFinishを呼び出さないといけない
です。
まぁテクスチャ&SurfaceTextureを生成してイメージをテクスチャとして取得したんなら普通は描画するはずなんでほとんどは問題ないんでしょうけどね。Nexus5とかNexus7では特に余計なことせずとも正常に動いたんだけど、EGL/OpenGL|ESの仕様としてはどっちが正常なんだろう?
またまたレアなのを引き当ててしまいました。でも狙ってハマってる訳じゃないからね。と言う事で今日はこれでおしまいというところですが、ちょっと短いのでおまけを追加です。
AndroidのOpenGL|ESでオフスクリーン描画
AndroidのOpenGL|ESでオフスクリーン描画する方法はいくつかあります。
- SurfaceTextureを使う(GL_TEXTURE_EXTERNAL_OESを使ったテクスチャへの描画)
- FBOを使ってテクスチャに描画する
- FBO(Frame Buffer Object)を使ってRBO(レンダーバッファーオブジェクト)に描画する
- PBufferを使う
- ダブルバッファの裏側を使う
- その他・・・
3つ目以降は、オフスクリーン描画そのものとしてはいいけど、更にそれを別の画面/Surfaceに描画するって事を考えるとかえって面倒で、オフスクリーン描画の結果がテクスチャになってくれるのが便利です。
アプリの実体が全てnative側なら2つ目の「FBOを使ってテクスチャに描画する」がいいんかなぁって思いますが、Javaが絡んでくるのであれば、1つ目の方が描画完了イベントが拾えるので処理をちょっと省略できるかな?
OpenGL|ES(のシェーダー)以外で描画やイメージプロセッシング等の処理をするのであれば、特にテクスチャへの描画に拘る必要はありませんけどね。
ところでOpenGL|ESで描画しようとすると、EGLContext(EGLレンダリングコンテキスト)ってのが必要です。GLSurfaceViewだとデフォルトでEGLレンダリングコンテキストが生成されて有効になっているので、GLSurfaceView.Rendererのメソッド内(#onDrawFrameとか)や、GLSurfaceView#queueEvent経由で実行するのであれば、何も考えずにOpenGL|ESで描画できます。
でも、そもそもの今回の目的の1つが画面に表示しないで描画をしたいが故のオフスクリーン描画なのでGLSurfaceViewを使うってのは論外ですし、GLSurfaceViewにはEGL Context Lostという難敵が存在します。
そこで、自前でEGLレンダリングコンテキストを生成することにします。どんな風に生成するかというと、以前の記事USBカメラから音&動画の同時キャプチャした〜い(その2)でちらっと触れたのと、GitHubに上げているサンプルプロジェクトUSBCameratest3に入っているのでコードは省略です(^_^;)今回の記事とも直接関係ないしね。
USBCameratest3では、TextureViewへの描画と、追加のSurface(MediaCodecでの動画エンコードの入力用)への描画を行っています。処理の流れ的にはこんな感じになっていました。
- TextureViewからSurfaceTextureを取得
- 描画スレッドを生成
- EGLContextを生成@描画スレッド
- TextureViewから取得したSurfaceTextureからEGLSurfaceを生成、eglMakeCurrentを呼び出す@描画スレッド
- テクスチャ名を生成@描画スレッド
- テクスチャ名からカメライメージ書き込み用のSurfaceTextureを生成&イベントハンドラをセット@描画スレッド
- カメラからのイメージを書き込み@別スレッド
- イベントハンドラ内で描画スレッドへ描画指示@別スレッド
- #updateTexImage & #getTransformMatrixを呼び出す@描画スレッド
- 録画用Surfaceへ書き込み指示@描画スレッド
- TextureViewから取得したSurfaceTextureへ描画(EGLSurface&OpenGL|ESを使用)@描画スレッド
- 終了指示が来るまで7〜11を繰返し
- 終了処理(リソース破棄・スレッド終了)
元々がTextureViewへの描画なので、TextureViewから取得したSurfaceTextureからEGLSurfaceを生成して#eglMakeCurrentを呼び出す事で、EGLレンダリングコンテキストを有効にしています。というのも、OpenGL|ES関係のコマンド呼び出しは、EGLレンダリングコンテキストが有効でないと行えないのです。
今思えば、このサンプルの場合は、元々のTextureViewから取得したSurfaceTextureでEGLレンダリングコンテキストを生成して、入力用のテクスチャ・SurfaceTextureを生成、しかもそのEGLレンダリングコンテキスト内で元のTextureViewへの描画をしていたから問題なく動いてたんですね。
Surface/SurfaceTexture/SurfaceHolderがあれば#eglCreateWindowSurfaceを使ってEGLSurfaceを作れるので簡単なのですが、今回はそもそもSurfaceViewもTextureViewも存在しない前提です。困りますよね。テクスチャ名があれば、SurfaceTextureを生成できますが、そもそものテクスチャ名生成(#glGenTextures)は OpenGL|ESのコマンドなので、EGLレンダリングコンテキストが有効でないと呼び出せません。堂々巡りになってしまいます。
ではどうするかと言うと、EGLのコマンドだけで生成できるダミーのEGLSurfaceを生成して#eglMakeCurrentを呼び出します。
今回のテストでは次のようにしています。
1 2 3 4 5 6 7 8 9 |
private final OnFrameAvailableListener mOnFrameAvailableListener = new OnFrameAvailableListener() { @Override public void onFrameAvailable(SurfaceTexture surfaceTexture) { synchronized (mSync) { requestDraw = isRunning; mSync.notifyAll(); } } }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
mMasterEgl = new EGLBase1(EGL10.EGL_NO_CONTEXT, false); mDummySurface = mMasterEgl.createOffscreen(1, 1); mDummySurface.makeCurrent(); // 共有テクスチャ名を生成 mTexId = GLDrawer2D.initTex(); mMasterTexture = new SurfaceTexture(mTexId); // 共有テクスチャへ書き込むためのSurfaceを生成、UVCカメラからの映像はこのSurfaceに書き込んでもらう mSurface = new Surface(mMasterTexture); // 共有テクスチャへ映像が書き込まれた時のイベントハンドラを設定 mMasterTexture.setOnFrameAvailableListener(mOnFrameAvailableListener); synchronized (mSync) { isRunning = true; while (isRunning) { if (requestDraw) { requestDraw = false; draw(); } else { try { mSync.wait(); } catch (InterruptedException e) { break; } } } } // 共有テクスチャと関係するリソースを破棄(省略) |
注目するのは3行目です。1ピクセルx1ピクセルの極小オフスクリーンを生成しています。先に書いたオフスクリーンの作り方のPBufferを使う方法の1つです。
オフスクリーン生成の実体は#eglCreatePbufferSurfaceです。と言っても実はUSBCameratest3のEGLBaseでは実装していないので、どさくさ紛れにここで載せちゃいましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
private EGLSurface createOffscreenSurface(int width, int height) { if (DEBUG) Log.v(TAG, "createOffscreenSurface:"); final int[] surfaceAttribs = { EGL10.EGL_WIDTH, width, EGL10.EGL_HEIGHT, height, EGL10.EGL_NONE }; EGLSurface result = null; try { result = mEgl.eglCreatePbufferSurface(mEglDisplay, mEglConfig, surfaceAttribs); checkEglError("eglCreatePbufferSurface"); if (result == null) { throw new RuntimeException("surface was null"); } } catch (IllegalArgumentException e) { Log.e(TAG, "createOffscreenSurface", e); } catch (RuntimeException e) { Log.e(TAG, "createOffscreenSurface", e); } return result; } |
とは言っても、上のコードはEGL10を使ってて、USBCameratest3のEGLBaseではEGL14を使っているのでそのままコピーしても動かないんだけどね(^_^;) EGL10だとインスタンスメソッドでEGL14はスタティックメソッドだとか、#eglCreatePbufferSurfaceの引数がちょっと違うとかはあるけど、大した違いはないので興味がある人は自分で挑戦してみてね。
でも、eglCreatePbufferSurfaceも動かない(いつもエラーを返す)某タブレットも有るんだよねぇ。買収されてレベル下がったって感じ↓ どうしたもんだろう?M*Kのチップセットのせい?(-_-;) EGL1.4対応だし取得できる情報からではPBuffer使えるはずなのに(T_T) バックグラウンド録画の実装はまだ暫く掛かりそうですm(_ _)m
っとか言ってる内に某タブレットの不具合は原因は判りました。eglCreatePbufferSurfaceでEGL_BAD_MATCHが返ってきてたのでまぁそこらへんだろうなぁとは思ってましたが。
答えは、eglChooseConfig呼ぶ時のパラメータにEGL_RECORDABLE_ANDROID(0x3142)を入れていたからでした。
某タブレットはAndroid4.1.2なので、このフラグは未サポートのようです。MediaCodecへの入力用のSurfaceへEGL/OpenGL|ESを使って描画する時に必要になるフラグですが・・・そもそもMediaCodec#createInputSurfaceはAPI18以上なので、Android4.1.2では対象外だったのでした。
でも、このタブレットで動かなかったのは別の不具合もあったからで、しかもLogCatに「今は実装してへんけどまた後で実装するわ〜」的なメッセージを出力しよるのです(本当は英語だけどね)。なんじゃそらぁ〜また後っていつなんじゃ〜(~_~メ)
きりがないので今日はここまで、お疲れ様でした。