OpenGL ES 2.0 Adreno 205 と discard 命令の問題

GPU Adreno 205 は Fragment Shader で discard 命令を使うと
フリーズし、OS ごと再起動することがあるようです。
コメントchototsu さんより情報を頂きました。
ありがとうございました。

試したらあっさり再現してしまいました。
ドライバレベルで停止しているらしく発生すると何もできなくなります。
デバッガで強制的にプロセスを削除すると OS ごと再起動します。
またはシェーダー実行直後に何もしなくても OS がリブートします。

試した機種
Xperia Ray SO-03C Android 2.3.3
Qualcomm MSM8255 1.0GHz Adreno 205

Adreno 200/220 では発生しません。205 だけです。
ただ 205 でもフリーズしないケースもあったので、条件を変えて
症状を調べてみました。

結論としては
FragmentShader で Texture を使用している場合に、
discard 命令より後で Texture フェッチが発生すると固まります。

Alpha Test や Clip Plane 等で discard を使う場合は
十分注意した方がよさそうです。

以下検証した結果

●Texture を使っていないシェーダーの場合

(1) 無条件 discard は固まらない
(2) varying 依存 discard も固まらない

// (1)  問題なし
precision mediump float;
void main()
{
    discard;
}
// (2)  問題なし
precision mediump float;
varying vec2 vTexcoord;
void main()
{
    if( vTexcoord.x < 0.5 ){
        discard;
    }
    gl_FragColor= vec4(1.0, 1.0, 0.0, 1.0);
}

●Texture を使っているシェーダーの場合

(3) 無条件 discard はどこに挿入しても固まる。
(4) texture 依存 discard は shader の一番最後なら固まらない。
  ・すべての texture 読み込み命令のあとなら固まらない。
(5) varying 依存 discard も (4) と同じだが、texture フェッチの
  前でも固まらない場合があった。

// (3)  freeze
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    // discard; ここでも同じ
    gl_FragColor= texture2D( ColorMap, vTexcoord );
    discard;
}
// (4) - A  問題なし
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    lowp vec4  color= texture2D( ColorMap, vTexcoord );
    if( color.w < 0.7 ){
        discard;
    }
    gl_FragColor= color;
}
// (4) - B  freeze
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    lowp vec4  color= texture2D( ColorMap, vTexcoord );
    if( color.w < 0.7 ){
        discard;
    }
    color+= texture2D( ColorMap, vTexcoord + vec2(0.1,0.1) );
    gl_FragColor= color;
}
// (4) - C  問題なし
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    lowp vec4  color= texture2D( ColorMap, vTexcoord );
    color+= texture2D( ColorMap, vTexcoord + vec2(0.1,0.1) );
    if( color.w < 0.7 ){
        discard;
    }
    gl_FragColor= color;
}
// (5) - A  freeze
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    if( vTexcoord.x < 0.5 ){
        discard;
    }
    gl_FragColor= texture2D( ColorMap, vTexcoord );
}
// (5) - B  問題なし
precision mediump float;
varying vec2 vTexcoord;
uniform lowp sampler2D ColorMap;
void main()
{
    gl_FragColor= texture2D( ColorMap, vTexcoord );
    if( vTexcoord.x < 0.5 ){
        discard;
    }
}

テクスチャ命令との順番が関係しています。(3),(5) に関しては
例外もありますが discard が Texture と依存関係が無いので、
コンパイラによって命令の実行順が変更されたためだと考えられます。

対策としては discard を使わないか、すべてのテクスチャ命令のあと
出来るだけ最後に記述するようにします。

ただ幸いなことに、discard は半透明と同じように TBDR の PowerVR
と大変相性が悪い命令です。さまざまな GPU への対応を考えると、
おそらくテクスチャを多数読み込むような複雑なシェーダーでは、
discard があまり積極的に用いられていないのではないかと思います。
(Adreno でもあまり推奨されていません。)

また discard の用途は多くが Alpha Test だと思うので、この場合
必ず Texture 命令のあとに用いられます。
上と同じ理由でテクスチャ一枚きりの単純なものが多く、今まで
あまり問題になっていなかったのではないかと考えられます。

関連エントリ
さらに OpenGL ES 2.0 Mobile GPU の速度比較

OpenGL ES 2.0 Adreno 205 と discard 命令の問題」への10件のフィードバック

  1. chototsu

    詳細なテスト有り難うございます。
    discardが無いと表示出来ないモデルは今のところ1つだけですので、単純に削除することにします。

  2. oga 投稿作成者

    (3) は texture2D と discard に依存が無いので、
    texture を発行した後結果を待たずに discard できます。
    pixel が破棄されたあとも texture unit が動作している
    可能性があり、問題が起こっていると考えられます。
    またシェーダーはできるだけ先に texture を発行するよう
    命令の実行順も入れ替わる可能性があります。

    (4)-A は依存関係があります。
    texture2D の結果がなければ discard すべきかどうか
    判断できないので、discard する前に texture unix の
    終了を待っています。
    discard するタイミングでは texture 命令が終わっている
    ことが保証されます。

  3. chototsu

    QUALCOMMから返答があったのですが、
    > In general we discourage the use of the discard statement as it is a very expensive operation.
    遅いから使うなということのようです。
    パフォーマンスではなくてOSごと落ちる事を問題にしているのですが・・・。

    この他にもAdreno200,205,220全部で落ちるプログラムがありログを取ったところこういうものがいっぱい出ていました。

    98, status=0). CPU may be pegged. trying again.
    W/SharedBufferStack( 2927): waitForCondition(LockCondition) timed out (identity=

    そこでググったところこういうのを見つけたのでやってみたところ、とりあえず落ちなくはなったようです。
    http://www.archivum.info/an…(android-developers)-Re-OpenGL-lockups-in-2.2.html
    ただ問題との関連性が不明ですので、本当に正しい対処方法なのかどうかは分かりません。
    パフォーマンスはだいぶ落ちるようです。

    アプリが落ちるだけでしたら大した問題にはならないのですがOSを巻き込んで落ちるのは本当に困ります。
    メーカー様には何とかしてもらいたいものです。

  4. oga 投稿作成者

    いつも報告ありがとうございます。

    1. GPU のハングアップ
    2. GPU ハングアップ時に OS が再起動する

    この2つの問題が一緒になってしまったのかもしれません。
    1. は他の GPU でもあるし、2. は WDT が正しく働いていると
    ということなのでしょうか。
    またこれまで問題になっていなかったことからも、
    discard 自体がほとんど使われていないのも確かだと思います。

    (1)ハードの構造上致命的に遅くなる(PowerVR TBDR等)
    (2)ハードの高速化の仕組みが無効となる(Pre Z Culling,Z圧縮など)
    (3)レンダリングのアルゴリズム上避けたい(Shadow Map,multi pathなど)

    多くの GPU は (2) に当てはまるしプログラマとしては (3) なので
    ハードメーカーもプログラマも利害が一致しています。

    もう一つのハングアップですが、Buffer の動的な書き換えとか
    しているのでしょうか。
    こちらでは今のところその問題は出ておりません。(バッファ更新無し)
    ただ Adreno200 で EGL Config に 565 でなく 8888 を選ぶと
    落ちるのはかなり前から判明しています。

    また最近 shader が複雑になったためか Android 3.x の Tegra2
    だけ落ちるようになりました。
    再起動はしませんがアプリがクラッシュします。
    Android 2.2 の Tegra2 や Tegra3 では動いています。

  5. chototsu

    こちらこそ、いつも非常に役に立つ情報を有り難うございます。
    読んで勉強させていただいております。

    バッファの書き換えですが、表情モーフィングはGPUで行えないためCPU側で行っており、顔の頂点だけ毎フレームGPUに送っています。

    discardについてはユーザーさんにモデルのメッシュを作り直して下さいとは言えないので仕方なく入れました。
    ただdiscardが入っていてもフレームレートは変わらないようです。

    新たなバグですが、今度はPowerVRで引っかかってしまいました。
    どうもPowerVRはunifromの取得を先行して行っているようで、こういうコードでトラブルが発生しました。
    if (weight.x == 1.0) {
    skinMat = m_BoneMatrices[int(index.x)];
    newPos = (skinMat * position);
    newNormal = (skinMat * normal);
    } else if (weight.x == 0.0) {
    skinMat = m_BoneMatrices[int(index.y)];
    newPos = (skinMat * position);
    newNormal = (skinMat * normal);
    } else {

    ここで、weight.x == 1.0(つまり使用ボーンが1本だけ)の場合、本来はelse以下は実行されないのでindex.y に -1がセットされていても問題無いはずです。ところがGalaxy NEXUS、Galaxy Tab SC-01CでOSが不安定になるという現象が発生しました(Galaxy Sでは動作)。
    推測ですがコンパイラの最適化でelse以下が投機的実行されている可能性があります。

    Adreno系はバグは多いのですがドライバーの種類は少ないらしく、どの機種もほぼ同じ動きをします。
    しかしPowerVRは機種ごとで動きが大きく違うので厄介です。
    私は幸いTegra2のバグには遭遇していないのですが、どのようなバグなのか興味があります。

  6. chototsu

    > 2. GPU ハングアップ時に OS が再起動する
    再起動せず、電源ボタン長押しも効かず、バッテリーを抜くしか無くなるケースが多かったようです。
    SharedBufferStackで検索するとこういうケースが数多く報告されており、そのほとんどがAdreno系のようです。
    Adreno系GPUを搭載したスマホのフリーズ問題(写真を撮ったらフリーズした等)が数多く報告されていますが、そのうちの何割かは同じバグを踏んでいるのかもしれません。

    1つ忘れていましたが888で落ちるとはどのようなケースでしょうか?
    私のアプリはRGBA8888を使っていますが、もしかしてそれが原因なのでしょうか。
    8888はQualcomm社のサンプルでも使われていますので、それが原因で落ちるというのは考えにくいのですが。

  7. oga 投稿作成者

    頂点更新はダブルバッファ化とかしているのでしょうか。
    GPU との同期命令で直るということは、GPU リソースの
    アクセスが関連しているかもしれません。

    GPU はスレッドグループで実行するので、動的分岐は
    もともと並列に実行時間がかかります。
    単純な演算だけなら、本当に分岐するよりも全部命令で
    埋めた方がパイプラインの無駄がなく速いと思います。
    最後に条件付き代入するだけなので、レジスタに余裕が
    あればストールが少なくて済みますし、どの GPU でも
    実現できるやりかたです。
    なるほど、Uniform の範囲外アクセスの可能性とかは
    全く頭にありませんでした。
    今後は気を付けてみます。

    ただ未使用の index も 0 を入れて weight は 0 に
    するなど、ちょっとくらい外れても問題がないような
    コードは普段から心がけていました。
    あと浮動小数点値の == 比較は信用していないので、
    例えば weight.x >= 1.0-0.001 等にしています。

    Adreno 200 の 8888 ですがおっしゃられるおりこちらの
    使い方のミスなど他に原因があるかもしれません。
    もう少し関係を調べたいと思います。

  8. chototsu

    頂点データはダブルバッファ化しています。
    確かに、バッファ更新でフリーズしている可能性はあります。
    考えてみると表情データが無いモデル(VBOの更新は無し)と、CPUの負荷の方がGPUより高いモデル(次のフレームの準備が出来た頃にはGPUはidleになっている)は落ちません。
    もしかするとGPUが使用中のVBOに対しglBufferSubDataを呼び出すとデッドロックするのかもしれません。

    スキニングコードの変更は、メッシュあたりのボーンマトリックスの数を減らすために行いました。それによりTegra2では性能改善がみられたので採用したのですが、考えてみると未使用のindexに0をセットしておけばif文はいらないので、その方が速いのかもしれません。
    時間が出来たら試してみます。

    浮動小数点の == ですが、PowerVRでもweight.x == 1.0がtrueになることは確認済みです。
    ただおっしゃるとおりそういう可能性も考えてindexに0を入れるべきでした。

  9. chototsu

    if文を削除してみましたがTegra2で少し速くなったような気もします。ただこの程度ですと誤差の範囲かもしれません。
    考えてみればこのトラブルは「仕様」と言われても仕方ないのかもしれませんね。
    CUDAとかの経験もありますので理解はしていたつもりですが、頭の切り替えが難しいです。

コメントは停止中です。