eaglesakuraの技術ブログ

技術的な話題とか、メモとか。

複数スレッドでGLの処理が可能なGLSurfaceView作りました

複数スレッドでGLの処理が可能なGLSurfaceView作りました

何が出来るのか

GLSurfaceViewを継承したクラスです。動作には互換性があり、GLSurfaceViewをMultiContextGLSurfaceViewに切り替えるだけで使えます。

GLSurfaceViewとの違いは、標準で複数スレッドでのOpenGL ESコマンドの利用を可能にしている点です。GLSurfaceViewでテクスチャ等のリソースを非同期で読み込もうと思っても、最終的には GLSurfaceView#queueEvent にキューイングして、描画スレッドを止める必要があります。

GLSurfaceViewは後述のMaster & Slaveの仕組みを利用して、描画スレッドを止めることなく画像ロードを実現しています。

なぜ作ったか

今書いているOpenGL ES 2.0解説本で、付録としてGLSurfaceViewの拡張性の説明をしようと思って、その実証のため作りました。

どうやって実現しているのか

GLSurfaceViewは基本設定がアレで微妙に使いにくいです。(今は違うかもしれないけど)onPauseでGLに関するリソースをすべて解放するため、onPause/onResumeでリソースの再読み込みが必要になる場合があります。その代わり拡張性については非常に優れていて、特にConfigやContextの拡張して外部(開発者)に任せることが出来ます。

今回利用したのは、EGLContextを拡張するためのGLSurfaceView#EGLContextFactoryの仕組みです。

EGLContextFactoryとは

EGLContextFactoryはその名の通り、EGLContextの生成を行わせるクラスです。標準ではeglCreateContextを呼び出すだけですが、今回はMulti-Contextになるように仕掛けを施しています。

そもそもOpenGL ESで指定された値はどこに格納されるのか

例えばglClearColor等で指定された画面クリア用の色はどこに格納されるのでしょうか? GPU全体で1つの設定しか保持できないとしたら、複数のアプリで同時にGLを使うことが出来ませんし、Androidのシステム自体が描画できなくなってしまいます。

OpenGL ESの値が保存される領域がEGLContextです。GLのコマンドが呼び出されると、「そのThreadに紐付けられているEGLContextに対して暗黙的に」操作が行われます。この仕組のおかげで、複数箇所からGLのコマンドが呼び出されても問題なく処理が行われます。

テクスチャ等のメモリもEGLContextが包括して持っています。つまり、EGLContextはRAMとVRAMに跨る様々な情報を保持してくれています。

EGLContextは1プロセス内で(メモリが続く限り)いくつでも生成できます。また、あるEGLContextに対する操作(glClearColor等)を行ったとしても、別なEGLContextへは一切影響を及ぼしません。

EGLContextの制限

OpenGL ESの制限として、1つのThreadに対して1つのEGLContextしか指定できません。また、1つのEGLContextは1つのThreadに対してしかバインド出来ません。つまり、何処かのThreadで使われているEGLContextは別スレッドで利用することが出来ないということです。

そのため、EGLContextをアンバインドせずにThreadが終了してしまった場合、そのEGLContextは永遠に操作できないリーク状態になってしまいます。

さらにEGLContext同士のメモリは基本的に切り離されており、あるEGLContextで読み込んだテクスチャは別なEGLContextでは利用することが出来ません。

GLSurfaceViewはRendererで描画を行わせるためのEGLContextをEGLContextFactoryクラスによって生成させています。リークを避けるためかと思いますが、作成したEGLContextは外部からアクセスできず、勝手にバインド/アンバインド/解放されます。

shared_contextによる制限緩和

結論として、表題のようにOpenGL ESコマンドを複数Threadから呼ぶためにはEGLContextを複数生成し、利用したいThreadに対してバインドすればいいことになります。

但しEGLContext同士はそのままでは独立状態にあるため、仮にRendererが暗黙的に利用しているEGLContextと別なEGLContextを作っただけでは、例えばテクスチャ等の読み込んだデータを共有できません。

EGLContext同士の一部の領域(具体的にはVRAMに相当する部分)を共有するのが、shared_contextの仕組みです。

EGLContextを生成するeglCreateContextコマンドの第3引数に、shared_contextという値があります。それに適当なEGLContextを指定すると、テクスチャ等のVRAM領域を共有することが出来ます。

MultiContextGLSurfaceViewではshared_contextを設定したEGLContextを生成することで、レンダリングスレッドとワーカースレッドの並列性を保ちながらGLのコマンド呼び出しを行えるようにしています。

shared_contextが共有できるリソースは下記です。ちなみにEGL仕様ではBuffer&Textureと記述されています。

  1. テクスチャオブジェクト
  2. レンダリングバッファ(フレームバッファ
  3. VertexBuffer / IndexBuffer
  4. シェーダー&プログラムオブジェクト

Master&Slave方式

MultiContextGLSurfaceViewでは、便宜上EGLContextにMaster ContextとSlave Contextという名前をつけています。

  • Master Context
    • どのThreadにもバインドされず、shared_contextに指定するだけのEGLContext
    • Slave Contextの生成数・廃棄数を数え、Slaveの生存数が0になったら廃棄される
  • Slave Context
    • レンダリングスレッドやワーカースレッドに対してバインドされるEGLContext
    • 同一のMaster Contextから派生しているため、Slave Context同士もshared状態にある

サンプルとなるMainActivityは次のような順番でContextが生成・廃棄されます。

  1. Master Contextが生成される
  2. GLSurfaceViewがEGLContextFactoryに対してEGLContextの生成をリクエストする2.
  3. レンダリング用のSlave Contextを生成する
  4. onSurfaceCreatedが呼び出される
  5. ワーカースレッドが起動し、Slave Contextを生成する
  6. onSurfaceChangedが呼び出される
  7. onDrawFrameで描画を継続する(レンダリングスレッドは停止しない)
  8. ワーカースレッドでテクスチャの読み込みを完了する
  9. ワーカースレッド用のSlave Contextが廃棄される
  10. (アプリを終了させる)
  11. レンダリングスレッド用のSlave Contextが廃棄される
  12. Slave Context数が0になったので、Master Contextが廃棄される

その他:

  • Slave ContextをActivityが保持しておけば、onPause/onResumeによるレンダリングスレッド用のSlave Context廃棄(と、それに伴うMaster Context廃棄)を避けてリソースを保持し続けることが出来ます。サンプル用のMainActivityでonPause/onResumeでテクスチャ解放されていないのが確認できます。

ダミーサーフェイスの生成

EGLContextをバインドする=eglMakeCurrentを呼び出すためには、第2・第3引数に描画対象となるEGLSurfaceが必要です。

EGL_NO_SURFACEを指定することも出来ますが、基本的に呼び出しに失敗します。eglMakeCurrentはレンダリング対象となるEGLSurfaceの指定を義務付けています。(iOSではEGLContext相当となるEAGLContextだけでバインドができるので、その点で楽ですね)

MultiContextGLSurfaceViewでは、ワーカースレッドでeglMakeCurrentを呼び出すために、1x1ピクセルの極小のPBufferSurfaceを生成しています。PBufferSurfaceはウィンドウ(View)に紐付かないレンダリングターゲットです。

MultiContextGLSurfaceView#requestAsyncGLEventの処理

以上の情報を元にrequestAsyncGLEventの実装を見ると、なんとなくこのクラスがやっていることがわかるのでは無いでしょうか。

コールバック終了後、eglMakeCurrent直前にglFinishを呼び出しているのは、一部端末でワーカースレッドがフリーズする不具合を回避するためです。

    /**
     * GL動作を非同期で行う
     * Textureの読み込み等を想定
     * @param event
     */
    public void requestAsyncGLEvent(final Runnable event) {
        Thread thread = (new Thread() {
            @Override
            public void run() {
                // initialize EGL async device.
                final EGLDisplay display = mEGLDisplay;
                final EGLContext context = newSlaveContext();
                final EGLSurface surface = newDummySurface();

                try {
                    mEGL.eglMakeCurrent(display, surface, surface, context);

                    // call event
                    event.run();
                } finally {
                    GLES10.glFinish();
                    mEGL.eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

                    destroySlaveContext(context);
                    destroyDummySurface(surface);
                }
            }
        });

        thread.setName("GL-Background");
        thread.start();
    }

問題点

古い端末(概ね2010年前後に発売された端末。Nexus Oneとか。)ではワーカースレッドで読み込んだリソースが正常に利用できない場合があります。原因不明なので誰か原因(もしくはEGLの使い方がおかしい?)が分かる人おしえて下しあ。

以上の説明は非常に不親切なため、書籍ではちゃんと図説付きでなるべく親切にまとめます。

Mac OSでPerfHUD ESを利用してNexus7のOpenGL ES負荷を測定する

Mac OSでPerfHUD ESを利用してNexus7のOpenGL ES負荷を測定する

いろいろあってヨドバシポイントを2万円分手に入れたので、検証機としてNexus7を購入しました。Nexus7はTegraを搭載していて、便利なツールがMacでも使えるので、そのメモです。

標準ツールでのOpenGL ESデバッグ

現在のAndroid SDKやADTはOpenGL ES用のトレーサーを提供しています。ですが、トレーサーにはいくつかの問題点があり、実際のアプリ開発では有効活用出来ない場合があります。

例えば、次のような問題点があります。

  1. 起動時にしか有効にできない
    • アプリデバッグ中にトレーサーを起動してデバッグを開始するという使い方ができません
    • そのため、アプリ開始時のロードや不要なシーンをスキップする等の作業が行いにくくなります
      • トレーサーをONにすると、OpenGL ES処理に数十倍の時間がかかってしまうためかなりメンドウです
  2. リアルタイムのトレースが出来ない
    • 基本的に呼出履歴の保存とフレームバッファの保存のみが行えるため、リアルタイム性が無い
    • 内容はトレース停止後でしか確認できない

PerfHUD ESとは

PerfHUD ESNVIDIA社が提供している、Tegra系列GPU専用のプロファイリングツールです。

端末のroot不要であり、adb経由でリアルタイムにCPU/GPUの処理内容をトレースすることができます。

また、アプリ起動中からのアタッチ・デタッチも自由に行えるため、標準ツール以上の自由度があります。

Nexus7はNVIDIA社のTegra3を搭載していますので、PerfHUD ESを利用することができます。(ちなみに、ESがつかないほうのPerfHUDはデスクトップ用GPU向けのようです)

ツール自体はNVIDIAのDeveloper登録が必要になりますが、無料で手に入れることができます。

接続方法

Windows版はUSBのWifi接続が可能ですが、Mac版はWifi接続のadb限定のようです。 また、アプリ側に次の準備が必要になります。

その状態でPerfHUD ES付属の"enable_perfhud.sh"スクリプトをターミナルから実行すると、Nexus7側の設定がONになります。enable_perfhud.shは端末再起動ごとに無効化されますので、毎度実行する必要があります。

その状態でデバッグ対象のアプリを実行します。

f:id:eaglesakura:20130511144149j:plain

以上の状態でPerfHUD ESを起動し、右下の"Not Connected"ボタンをクリックするとIPアドレスの入力画面になります。
端末のIPアドレスを入力し、Refreshボタンを押すとスキャンが始まりますのでしばらく待ちます。

f:id:eaglesakura:20130511144141j:plain

該当のPIDが出現したら、選択してconnectします。

するとCPU負荷やGPU処理速度、フレームレート等が取得できるようになります。

PerformanceDashbord

デフォルトウィンドウであるPerformanceDashbordはリアルタイムの負荷を計測するのに役立ちます。

Tegra3はクアッドコアなのでCPU状態が4つ、GPU情報としてドライバの呼び出し時間や合計描画時間等、かなり有用な情報です。

フレームレートについてはeglSwapBuffer〜eglSwapBufferまでの時間を計測しているかと思います。

f:id:eaglesakura:20130511144228j:plain

FrameDebugger

もう一つのウィンドウであるFrameDebuggerに切り替えると、フレームごとのレンダリング情報を見ることができます。

FrameDebugger起動中はレンダリングが停止するため、「現在の描画フレームの状態」を得るのに役立ちます。

  • Frame Overview

    • 現在のフレーム各処理計測
  • Frame Scrubber

    • 各描画命令ごとのバッファ内容の確認ができます。
    • カラーバッファ、カラーバッファのα値、深度バッファ、ステンシルバッファそれぞれのプレビューも可能です。
    • 巻き戻しや、描画命令毎にどのピクセルがレンダリングされたのかを知ることもできます

f:id:eaglesakura:20130511144509p:plain

  • Call Trace
    • OpenGL ES命令の呼出履歴や戻り値の確認ができます

f:id:eaglesakura:20130511144537p:plain

  • Geometry Viewer

    • 実際にVBO/IBOの内容を読み取り、3D表示してくれます
    • バッファの内容が実際にどうなっているのかを知ることができます
  • State Viewer

    • 描画命令ごとにEGLContextの内容を知ることができます
    • 想定と違うStateが設定されている等のチェックを行えます
  • Texture Viewer

    • 読み込み済みのテクスチャ内容を表示してくれます
    • BGRA表示には非対応で、BGRAテクスチャは強制的にRGBA表示されるようです
    • mipmapを生成している場合、mipmapも表示します

f:id:eaglesakura:20130511144606p:plain

  • Shader Viewer
    • コンパイルされたシェーダーのuniform/attribute内容を確認できます
    • シェーダーのソースコードも表示され、ソースコードの修正・コンパイルチェック・修正後のUniform内容チェック・ソースコード保存も行えます
      • ただし、現状だとコンパイルしたシェーダーを端末に書き戻すことは出来ないようです
        • 自力で読み直せってことですね
        • その辺はアプリ側の工夫次第でどうにか、というレベル
    • uniform textureはテクスチャのプレビューも行なってくれます

f:id:eaglesakura:20130511144618p:plain

TextureViewでの問題点

TextureViewはAndroid 4.0から登場したハードウェアアクセラレーション対応のViewです。以前記事に書いたカメラの他、OpenGL ES等の処理が行えます。

もちろんこの状態でもPerfHUD ESは利用できますが、いくつかの問題点があるようです。

  • TextureViewでPerfHUDを接続すると、レンダリングが画面に反映されなくなります
    • プロファイリングは継続されますが、PerfHUD ES自体の動作も不安定になります
  • レンダリング自体の速度が非常に遅くなります

まとめ

調べてみるとかなり便利なツールです。ある程度不安定な動作を差し置いても、開発ではとても役立つと思います。

個人的にはTextureViewでの動作をどうにかして欲しいので、USB接続に対応してくれると非常にありがたいのですが・・・。

初めてのOpenGL ES

初めてのOpenGL ES

Android NDKで"Unknown Application ABI:" エラーが発生した場合の対症療法

Android NDKのデバッグは容易になった・・・はずなのに

Androidアプリは基本的にSDKを利用し、Java言語で記述します。ですが、Android NDKを利用することでC/C++言語でもアプリを記述できるのは周知のとおりです。

最初は「できるやつだけついてこい」的にほとんど整備されていなかったNDKの開発環境ですが、最近ではEclipseとの連携が可能になり、更にはコマンドラインベースだったデバッグEclipseを使ってGUI上でできるようになっています。

  1. Add Native Supportを設定する f:id:eaglesakura:20130317220540j:plain
  2. Debug As -> Native Application f:id:eaglesakura:20130317220640j:plain
  3. エディタ上でブレークポイントを設定 f:id:eaglesakura:20130317220728p:plain
  4. あとはステップ実行なり変数のウォッチなりが簡単にできる f:id:eaglesakura:20130317220755p:plain

そのおかげで、あんどろいどたん(Google Play)を始めとして最近の案件はAndroid NDKを多く利用していたわけですが、唐突に次のようなコンソールに出力されるようになり、デバッガが接続できなくなりました。

[2013-03-17 21:36:50 - アプリ名] Unknown Application ABI: 
[2013-03-17 21:36:50 - アプリ名] 
[2013-03-17 21:36:50 - アプリ名] Unable to detect application ABI's

対症療法

しばらくの間は「アルェ〜、ナンデナンデ!?」状態で仕方なくprintfデバッグをしてたわけですが、次の条件でデバッガが接続できることがわかりました。

無駄だったこと

  • 端末再起動
  • ndk-build -clean
  • キャッシュ的なファイルを全て削除
  • Eclipseを通常再起動
  • Eclipseの再インストール(再設定)
  • Eclipseの英語版を利用

効果的だったこと

  • eclipse -clean で起動する
    • ただしclean起動後も、通常起動したらまたデバッグできなくなる
    • つまりはデバッグしたくなる度にclean起動が必要

ですが、boost等の重いライブラリを利用していると、clean起動する度にインデクサーがガリガリと幾千のファイルをチェックしてしまうため、ストレスがマッハです。

デバッガが接続できるアプリ・できないアプリがあった

つい最近わかったことは、同一のEclipse環境・端末環境でもデバッグできるあぷり・できないアプリがあるということです。
チェックしてみると、比較的規模の大きな案件でビルドしたアプリは接続できず、小さいアプリは接続できるということでした。

原因

いろいろ調べた結果、(今のところ)理由は次の一点でした。

  • Android.mk/Application.mkで自分で作ったファイル(共通のmkファイル等)をincludeするとデバッガが接続できない
  • どちらか(もしくは両方)にincludeがあるとエラーが発生する
  • include先に書いてある内容をコピペして、includeしないようにすればおk
# ダメなApplication.mk

APP_MODULES := sample
include shared_application.mk

------
# shared_application.mkの中身

APP_STL:=gnustl_static
APP_ABI := armeabi-v7a
# 問題ないApplication.mk

APP_MODULES := sample
# コピペして持ってくれば大丈夫
APP_STL:=gnustl_static
APP_ABI := armeabi-v7a

反省

「本来includeは使わねーよwwww」とか、私が知らないだけでmakefileのルールに則ってないのかもしれませんが、共通部分をどっかにまとめとこう的なことはしばらく考えないほうがよさそうです。

Galaxy NexusでETC1テクスチャを利用する場合の注意

ETC1とGalaxy Nexus

AndroidOpenGL ES実装はETC1をサポートするようになっています。

ETC1テクスチャは4x4のピクセルブロックを基本とした圧縮を行うため、利用するには次のような制限があります。

  • 縦横のピクセル数が4の倍数であること
    • そのため、Android SDK付属のetc1toolを利用すると4で割り切れるように画像末尾に詰め物を入れられる
  • GL_OES_compressed_ETC1_RGB8_textureエクステンション対応のGPUであること

この制限に従っていれば、Power Of Two(2のn乗)である必要はありません。

Galaxy NexusのGPUはスペック上はnpot(Non Power Of Two = 2のn乗以外)テクスチャも扱えますし、ETC1もサポートしています。

f:id:eaglesakura:20130317155132j:plain

f:id:eaglesakura:20130317155228j:plain

当然、次のコマンドも正常に行えますし、assertも通過します。

glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_ETC1_RGB8_OES, width, height, 0, image_bytes, image);
assert(glGetError() == GL_NO_ERROR);

その状態でテクスチャをbindしてレンダリングすると、問答無用でアプリが落ちます。OpenGLは基本的にエラーが発生したらエラーをセットするだけのため、異常な動作だと思われます。

解決方法としては「npotのETC1を使わない」という点ですが、でっかい背景画像とかnpotのETC1を使いたい場所はそこそこあるので、他の端末で同様の不具合が起きないことを祈るのみです。

その他

回避方法、もしくは「ここが間違ってる」という指摘があったらコメントお願いしますm(__)m

初めてのOpenGL ES

初めてのOpenGL ES

SurfaceTextureでMediaPlayerやカメラ映像をOpenGLテクスチャとして使う場合の注意点

SurfaceTextureによるテクスチャへのマルチメディアレンダリング

Android 3.xからSurfaceTextureクラスが導入されて、OpenGLとカメラ・ビデオ等のマルチメディア連携が楽になりました。

具体的には、テクスチャとしてビデオやカメラの映像を取り込めるようになり、リアルタイムでシェーダーによる映像加工したり揺らしたりができるようになりました。

AndroidのコードをみてみるとVideoSurfaceView.javaというクラスでその実装を行なっているらしいですが、その他の情報が殆ど無いので実際に使ってみた結果のハマリどころメモです。

前提として、OpenGL ES 2.0を利用しています。

AndroidのSurfaceTextureは2Dテクスチャではない

正確には、GL_TEXTURE_2D定数を使ってはいけません。GL_TEXTURE_2Dは殆どの場合は「テクスチャを使う場合のおまじない」のような形でglTexXXX関数で使っていると思いますが、それを使うとドハマリします。

SurfaceTextureではGL_TEXTURE_2Dを使わず、GL_TEXTURE_EXTERNAL_OES定数を使います。もしテクスチャ系のUtilクラスを自作している場合、GL_TEXTURE_EXTERNAL_OES定数を使えるように加工しなければなりません。

例えば、私の場合は次のように変更しました。
glTexParameteriに与える引数が、定数直書きからメソッドの引数を利用するように変更になっています。

変更前テクスチャ管理クラス抜粋

    texture.alloc(device->getVRAM(), VRAM_Texture);

    {
        s32 index = state->getFreeTextureUnitIndex();
        this->bind(index);
        {
            // npotでは GL_CLAMP_TO_EDGE を指定する必要がある
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            context.wrapS = GL_CLAMP_TO_EDGE;
            context.wrapT = GL_CLAMP_TO_EDGE;

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            context.magFilter = GL_NEAREST;
            context.minFilter = GL_NEAREST;
        }
        this->unbind();
    }
変更後テクスチャ管理クラス抜粋

    texture.alloc(device->getVRAM(), VRAM_Texture);

    {
        s32 index = state->getFreeTextureUnitIndex();
        this->bind(index);
        {
            // npotでは GL_CLAMP_TO_EDGE を指定する必要がある
            glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
            glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
            context.wrapS = GL_CLAMP_TO_EDGE;
            context.wrapT = GL_CLAMP_TO_EDGE;

            glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            context.magFilter = GL_NEAREST;
            context.minFilter = GL_NEAREST;
        }
        this->unbind();
    }

テクスチャ用のメモリ確保はSurfaceTextureが行う

通常のテクスチャの場合glTexImage2D(GL_TEXTURE_2D, XXX)関数を使ってVRAMへテクスチャのピクセルをコピーしますが、SurfaceTextureではそれを行なってはいけません。

例えば、間違ってglTexImage2D(GL_TEXTURE_EXTERNAL_OES, 0, GL_RGB, ビデオ幅, ビデオ高さ, 0, GL_RGB, GL_RGBA, 0)のように確保しようとすると例外が投げられるかエラーログが吐かれるはずです。

glGenTextures()で取得したテクスチャ名(ID)は、glTexParameterで設定後はメモリを確保せずにSurfaceTextureへ渡します。

SurfaceTextureはビデオをレンダリングするために十分なテクスチャ領域を自動的に確保します。

この時の制限として、ビデオの縦横サイズはGPUの性能限界(GL_MAX_TEXTURE_SIZE)を超えてはいけません。
ただし、現在のGPU性能だと2048x2048〜4096x4096程度の確保は行えるので、フルHDレベルであれば問題ありません。

確保されるタイミングはSurfaceTextureに渡したタイミングかも知れませんし、SurfaceTexture#updateTexImage()初回実行時かもしれませんが、開発者自身で確保してはいけません。

シェーダーではsampler2Dは使えない

GL_TEXTURE_2Dではないため、シェーダーではsampler2Dによるテクスチャアクセスはできません。
そのため、sampler2D変数の代わりにsamplerExternalOES変数を定義する必要があります。

使い方はsampler2D変数と変わりません。

内部的には、GL_OES_EGL_image_external機能を利用しますので、シェーダーのextension要求として次が必要です。定義しない場合、コンパイルに失敗するかアプリが強制終了する等の問題が発生します(GNで確認)

#extension GL_OES_EGL_image_external : require
フラグメントシェーダー全文

static const charactor *FRAGMENT_EXTERNAL_SHADER_SOURCE = 
        "#extension GL_OES_EGL_image_external : require\n"
        "        precision mediump float;"
        // UV setting"
        "        varying vec2 fTexCoord;"
        // texture
        "        uniform samplerExternalOES tex;"
        // color
        "        uniform mediump vec4    blendColor;"
        "        void main() {"
        "            vec4 color = texture2D(tex, fTexCoord) * blendColor;"
        "            color.a *= blendColor.a;"
        "            gl_FragColor = color;"
        "        }"
//

レンダリング領域の設定にはクセがある

ビデオの再生を開始すると、定期的にSurfaceTexture#OnFrameAvailableListenerを通じてコールバックが呼ばれます。
コールバック後、次の条件を満たすときにSurfaceTexture#updateTexImage()メソッドを呼び出すことでテクスチャにビデオやカメラの内容がコピーされます。

  • テクスチャと同じEGLContextがeglMakeCurrentに設定されている
    • バインドされていれば、おそらくスレッドは自由です
  • テクスチャがGL_TEXTURE_EXTERNAL_OESにバインドされている
    • 事前にtexture nameを渡しているため、コレは不要かもしれません・・・

SurfaceTextureは渡された空のテクスチャに対してレンダリングするために十分な領域を確保しますが、ビデオのサイズと完全に同一ではありません。
例えば、320x240のビデオをSurfaceTextureを通して描画させると、次のようなテクスチャが出来上がります。

f:id:eaglesakura:20130306094701j:plain

左右や下側に不要領域が作られているのが分かります。ジャストサイズでない理由はおそらく効率上の理由ですが、具体的にどの領域がビデオとして使われるかは問い合わせすることができません。

その代わり、SurfaceTexture#getTransformMatrix()メソッドでUV座標0.0〜1.0を利用する場合に設定するUV行列を取得することができます。引数にfloat[16]の配列を指定することで、4x4の行列として利用できます。

VertexShaderでUV座標を次のように処理すると、レンダリングに必要なUV領域として利用できます。
ただし、単純に行列を当てると、上下が反転してしまいます。

// SurfaceTexture#getTransformMatrix()で取得した行列
uniform mediump mat4    unif_texm;

-- 省略 --

fTexCoord = (unif_texm * vTexCoord).xy;

f:id:eaglesakura:20130306094742j:plain

その対策として、レンダリング用のポリゴンを作る時点で次のようにUV座標を上下反転させています。

これで見た目的に正常なレンダリング領域を設定出来ます。
頂点シェーダーを使ってもいいですが、どうせ毎度同じ処理をするならばUV単位で事前設定してしまったほうが負荷的にもプログラム的にも楽になります。

通常の四角形の場合

const static Quad::QuadVertex g_vertices = {
//
        /**
         // 位置情報
         left, top, //!< 左上
         right, top, //!< 右上
         left, bottom, //!< 左下
         right, bottom, //!< 右下

         //! UV情報
         0, 0, //!< 左上
         1, 0, //!< 右上
         0, 1, //!< 左下
         1, 1, //!< 右下
         */
        // 左上
        { LEFT, TOP, 0.0f, 0.0f, },
        // 右上
        { RIGHT, TOP, 1.0f, 0.0f },
        // 左下
        { LEFT, BOTTOM, 0.0f, 1.0f },
        // 右下
        { RIGHT, BOTTOM, 1.0f, 1.0f },
// end
        };
}

ビデオレンダリング用の四角形の場合

const static Quad::QuadVertex g_revert_vertices = {
// 左上
        { -0.5, 0.5, 0.0f, 1.0f, },
        // 右上
        { 0.5, 0.5, 1.0f, 1.0f },
        // 左下
        { -0.5, -0.5, 0.0f, 0.0f },
        // 右下
        { 0.5, -0.5, 1.0f, 0.0f }, };
//
}

f:id:eaglesakura:20130306094759j:plain

複数のSurfaceTextureを同時に利用する場合の注意点

一枚のSurfaceTextureで一つのVideoを再生するだけなら特に注意点はありません。
複数のビデオを同時に再生する場合や、効率化のためにテクスチャユニット等を細かく制御している場合はドハマリする場合があります。

GL_TEXTURE_EXTERNAL_OESは同時に一つのテクスチャユニットにしか割り当てることができません。
例えば、次のような制御を行うと意図しないテクスチャにビデオがレンダリングされる恐れがあります。

  1. glActiveTextureで0番ユニットに切り替える
  2. ビデオAをレンダリングさせる
  3. glActiveTextureで1番ユニットに切り替える
  4. ビデオBをレンダリングさせる
  5. 〜〜次のフレーム〜〜
  6. glActiveTextureで0番ユニットに切り替える
  7. ビデオAをレンダリングさせる
  8. 何故かビデオBがレンダリングされてる/(^o^)\

GL_TEXTURE_EXTERNAL_OES自体の仕様かAndroidが効率化のために設けた仕様かバグかは不明です。
コレを回避するために、私の場合はビデオ用のテクスチャを必ず同一のテクスチャユニットにバインドすることにしました。

必ず末尾のテクスチャユニットを利用する

    this->textureUnit = GPUCapacity::getMaxTextureUnits() - 1;

追記 3/7

単にActiveTextureの呼び出し忘れ(Utilクラスのバグ)だった疑惑(´・ω・`)

追記 3/8

単にActiveTextureの呼び出し忘れ(Utilクラスのバグ)だった。
glActiveTexture(index)されていて、かつglBindTexture(target, name)されているテクスチャに転送されるらしい。

最後に

SurfaceTextureを使えばカメラとかビデオとかと連携できて夢が広がりますね。
間違いとかあったら教えて下さいm(__)m

初めてのOpenGL ES

初めてのOpenGL ES

特定のAnnotationが付いたクラスやメソッドだけをProguard対象から外す

Android NDKとProguardは相性が悪い

Proguardとは

今更説明することも無いかと思いますが、ProguardといえばAndroiderにとっては欠かせないお友達です。
Javaのコードは容易に逆コンパイルされて、簡単に内容を解析されてしまいます。

逆コンパイル自体を防ぐことはできませんが、コードの解析を難しくしてくれるツールがProguardの大きな役割です。 Proguardを使う目的は、次の点が大きいでしょう。

  1. コードを難読化して第三者による解析を困難にする
  2. 不要なコードを削除してコード全体を小さくする

どちらがメインになるかは開発者毎に違うでしょうが、そのどちらかが当てはまるのでは無いでしょうか。

Proguradの問題点

Android SDKを一般的(あなたが思う範囲が一般的です。他人の同意を得られるとは限りません)に使っている限り、Proguardはとても良いパートナーです。

ですが、例えば次のような使い方をした場合、Proguardは我々にケンカを売ってきます。

  1. リフレクションを利用してクラスやメソッドやフィールドを参照した場合
  2. JNIを利用してnativeコードを呼び出した場合

リフレクションは使わない人は全く使わないJavaの機能ですが、簡単に説明すると「クラス名」「メソッド名」「フィールド名」を文字列として参照し、制御する方法です。

JNIはC/C++で記述された関数を「文字列」としてランタイムで検索し、呼び出します。

Proguardのかかったコードは、次の点で致命的な致命傷を受けます。

  • クラス名を変えられてしまう
    • それによって、リフレクションを取得するために指定した「文字列」と「実際のクラス名」に違いが生じてしまう
    • 例えば、"com.eaglesakura.Hello"というクラスが、Proguardを通すことで"com.eaglesakura.a"という短いクラス名に変わってしまいます。
      プログラム中で"com.eaglesakura.Hello"クラスを文字列で検索しても見つからなくなってしまい、ランタイムエラーが発生します。
  • NDK側でのみ参照しているクラスやメソッドが削除されてしまう
    • Proguardは利用されていないメソッド等をチェックし、コードからまるまる削除してしまいます。
    • そのため、NDKでしか使っていないクラスやメソッドがあった場合、「不要なクラス」と判断されて削除されてしまう恐れがあります。

今回問題になった所

個人的に作っているiOS/Android同時コーディング用のライブラリでは、JavaのクラスにAnnotationをつけることでJava側のコードを呼び出すC++コードを自動出力しています。

@JCClassはコンバータに「変換対象クラス」であることを知らせるための目印で、@JCMethodは「変換対象メソッド」であることを示すための目印です。

Java側コード
クラスに"JCClass"、メソッドに"JCMethod"のannotationをつけています。

@JCClass
class   Hoge {
    @JCMethod
    static void hello() {
        System.out.println("hello world!!");
    }
}
生成されるC++コード(Hello.cpp抜粋)
Helloクラスが自動的に作成されます。

const ::jc::charactor* Hello::CLASS_SIGNATURE = "com/eaglesakura/Hello";
static jclass class_Hello = NULL;
static jmethodID methods_Hello[1];
static void initialize_Hello() {
    // loaded !
    if (class_Hello) {
        return;
    }

    CALL_JNIENV();

    // load class object
    class_Hello = env->FindClass(Hello::CLASS_SIGNATURE);
    class_Hello = (jclass)::ndk::change_globalref(env, class_Hello);

    // load methods
    {
        methods_Hello[0] = ::ndk::JniWrapper::loadMethod(class_Hello, "hello", "()V", true);
    }
}

void Hello::hello() {
    CALL_JNIENV();
    initialize_Hello();
    env->CallStaticVoidMethod(class_Hello, methods_Hello[0]);
}

C++側のコードを見ると、"com/eaglesakura/Hello"というクラス名・パッケージ名を示したシグネチャや、"loadMethod(class_Hello, "hello", "()V", true);"というJavaのメソッド検索部分が生成されているのがわかります。

Proguardをかけてしまうと、クラス名の崩壊によってこれらの文字列では呼び出せなくなってしまいます。 また、hello()がJava側で参照されていない場合、最適化と称してまるっと削除されている恐れすらあります。

しかも呼び出しは「文字列」で行なっている都合上、コンパイル時に見つけることは出来ず、実行時にしかわかりません。

タチの悪いことに、Android JUnitはDebug状態で作成されてProguardされていないため、Jenkinsの自動ビルドでは発見することが困難です。

問題解決のために

ここからが本題です。
上記の問題を解決するためにはProguardでクラス名やメソッド名が変わらないようにしなければなりません。

幸運にも今回問題になったライブラリの場合だと「自動出力のために必ず特定のannotationをつける」必要があったため、どうにか「特定のannotationの付いたクラスやメソッドをProguard対象から外す」というタイトルのようなことが無いかと資料を漁ってみました。

@sys1yagiさんのブログにAnnotation指定でProguard回避する記述があったので、それを元に次の条件でProguardを回避する設定を追加しました。

  • @JCClassのAnnotationがついたクラスはリネームされない
  • @JCMethodがついたメソッドはリネームされない
  • @JCMethodがついたメソッドは最適化で削除されない
今回proguard.cfgに追加した部分

# jointcoding
# keep annotations
-keep class com.eaglesakura.lib.jc.annotation.jnimake.**
-keep @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class *
-keep @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod class *

# keep Java class names
-keepnames @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class *
-keep class * implements @com.eaglesakura.lib.jc.annotation.jnimake.JCClass *

# keep Java unused methods
-keepclassmembers @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class * {
    @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod *;
}

# keep Java method names
-keepclassmembernames class * {
    @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod *;
}

解説

次の3行でAnnotation自体をProguardから守ります。

-keep class com.eaglesakura.lib.jc.annotation.jnimake.**
-keep @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class *
-keep @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod class *

次の2行で、@JCClassのついたクラスの名称変更を回避します。

-keepnames @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class *
-keep class * implements @com.eaglesakura.lib.jc.annotation.jnimake.JCClass *

最後に、@JCMethodのついたメソッドを名称変更・最適による削除対象から外します。

# keep Java unused methods
-keepclassmembers @com.eaglesakura.lib.jc.annotation.jnimake.JCClass class * {
    @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod *;
}

# keep Java method names
-keepclassmembernames class * {
    @com.eaglesakura.lib.jc.annotation.jnimake.JCMethod *;
}

まとめ

Proguardは欲しい条件でちゃんとやってくれそうだけど、指定するための条件付けを見つけるまでが苦労するよね(´・ω・`)

GLTextureViewを公開しました

Android 4.0(API Level 14)からTextureViewというViewが登場し、OpenGL ESの応用範囲が格段に広くなりましたが、GLSurfaceViewに相当する補助クラスが未だに登場していません。

Google的には必要ないという考えかもしれませんが、SurfaceViewよりもTextureViewでOpenGL ESを扱ったほうが非常に楽なため、俺俺GLTextureViewクラスを公開しました。

GLTextureView(github)

詳細はREADME.mdにも記述されています。

GLSurfaceViewとの相違点

使い方はGLSurfaceViewとほぼ変わりありません。 ですが、おおまかに次の点が異なります。

  • リソース解放用にonSurfaceDestroy()を追加した
  • requestRender()の設計が異なる
    • GLSurfaceViewの場合 -> レンダリングスレッドへpostされるため、非同期実行される
    • GLTextureViewの場合 -> その場でレンダリングと画面への反映が行われる
  • 初期化用のメソッド名が異なる
    • 基本的にEnumを定義し、それをメソッドへ渡すようにしています

サンプルコード

例えば、GLTextureViewでOpenGL ES 2.0を初期化し、レンダリングスレッドを作成するコードは次のようになります。


public class SimpleGL20Activity extends Activity implements Renderer {
    static final String TAG = SimpleGL20Activity.class.getSimpleName();

    GLTextureView glTextureView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        glTextureView = new GLTextureView(this);

        // Setup GLTextureView
        {
            glTextureView.setVersion(GLESVersion.OpenGLES20); // set OpenGL Version
            //            glTextureView.setSurfaceSpec(SurfaceColorSpec.RGBA8, true, false); // Default RGBA8 depth(true) stencil(false)
            //            glTextureView.setRenderingThreadType(RenderingThreadType.BackgroundThread); // Default BackgroundThread
            glTextureView.setRenderer(this);
        }
        setContentView(glTextureView);
    }

    @Override
    protected void onPause() {
        glTextureView.onPause();
        super.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        glTextureView.onResume();
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d(TAG, "onSurfaceCreated");
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d(TAG, String.format("onSurfaceChanged(%d x %d)", width, height));
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClearColor(0, (float) Math.random(), (float) Math.random(), 1.0f);
        GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
    }

    @Override
    public void onSurfaceDestroyed(GL10 gl) {
        Log.d(TAG, String.format("onSurfaceDestroyed"));
    }

}

日経ソフトウエア 2013年 04月号

日経ソフトウエア 2013年 04月号

初めてのOpenGL ES

初めてのOpenGL ES