Superdry Memorandom :-p

旧「superdry memorandum :-D」です

RenderscriptについてAndroid Developer Blogをよんでみた(その2)

Android Developer BlogRenderscript Part2を読み途中で地震にあいました。やっと落ち着いてきたのでとりいそぎ更新します。いつものとおりてきとう翻訳なので訳に変なところがありましたらご指摘をいただければなと。

Renderscript Introduction*1で、この技術について簡潔に述べましたが、今回は「演算」をよりクローズアップして説明します。Renderscriptで「演算」とは、単一もしくは複数の異なるプロセッサのどちらで実行させてもいいように、DalvikコードからRenderscriptコードへデータ処理を委譲することを意味します。

Renderscriptの設計目標

Renderscriptは3つの目標があります。

(1)移植性

アプリのコードは、根本的に異なったハードウェアでも、全てのデバイスにアクセスできることが必要です。ARM系では現在、VFPやNEONの有無、様々なレジスタカウンタなどによっていくつかのバリエーションがあります*2。また、ARM系以外のx86に似ている他のCPUアーキテクチャや、いくつかのGPUアーキテクチャ、さらに多くのDPUアーキテクチャがあります。

(2)パフォーマンス

第2の目標として、移植性の制約下でできるかぎりのパフォーマンスを得ることが必要です。Renderscriptが開発者に広く普及し理解されるためには、既に確立されてる手法よりはるかによいパフォーマンスを実現する必要があります。

(3)つかいやすさ

第3の目標として可能な限り簡単につかえるよう開発することです。可能なところは自動化することで、グルーコードを避け、他の開発者に負荷をかけることを避けるようにする必要があります。

これら3つの目標の実現を考えたところはいくつかの設計上のトレードオフと直面しました。このトレードオフとは、DalvikやNDKのような端末上の既存の手法からRenderscriptを分離することです。本来だと、これらは異なった課題を解決するための異なったツールとして考えられるべきです。

Renderscriptの設計背景

まず最初の選択は、何の言語で作るようにすればいいかということでした。ここではほとんど無制限なオプションを持つ言語が候補となります。C、C++シェーディング言語を候補としました。そのなかで、まずシェーディング言語は不採用としました。シーングラフのようなグラフィカルなアプリのためにはデータ構造を操る必要があるためです。使い勝手のためにポインタと再帰は削るべきでないと考えました。一方でC++は非常に望ましかったのですが、移植性の問題にぶつかりました。C++は高度なため非CPUハードウェア上で実行するのが非常に困難です。最終的にRenderscriptはC99をベースにすることを選択しました。他の候補と同等のパフォーマンスであり、開発者によく理解されており、多様なハードウェア上で動作するのに問題がないからです。

次の設計上の課題はワークフローでした。 特にソースコード機械語へ変換する方法に注目しました。オプションを調査し、実際にRenderscriptの開発の間2つの異なった解決法を実装しました。まず、(EclairやGingerbreadなど)古いバージョンにてCのコードをデバイス上で機械語に変換する全ての方法を試しました。この方法だとアプリがソースを動的生成できるなどいくつかのすばらしい特徴がありましたが、使いやすさに問題があると判明しました。アプリをコンパイル、インストールし実行したあとで、文法誤り(syntax error)が判明するのはとても苦痛です。またデバイスでCPU性能が良くない場合、本来行われるべきstatic分析と最適化範囲が制限されるという問題もありました。

次に私たちはLLVMに切り替えて、スクリプトClangの改良版を使用しているホスト上でコンパイル/分析されるモデルへ設計変更しました。この方法だと高いレベルでの最適化が実行でき、その後LLVMビットコードを吐き出します。中間ビットコードから機械語への変換はデバイス上で行われます(追加された端末固有の最適化に依存する)。

最後の課題は、スレッドの実行です。パフォーマンスと移植性のトレードオフが問題となりました。既存の方法だと特定のハードウェアプラットフォーム毎にアプリのチューニングが必要になりますが、これは開発者が十分な知識がないと難しいです。また、未発表のハードウェアに対するチューニングとか負担も大きいです。しかし移植性の問題を解決しようとすると、ラインタイム上でのチューニング負荷のかわりにパフォーマンスのピーク時の平均値が大きくなります。そのため移植性を優先とし、ランタイムにこの負担をかけることを選択しました。

ランタイムにスレッド実行管理させる方法を選択した場合、副次的効果として、スクリプトをどこに実行するかに関して動的に決定をすることができます。 例えばあるハードがポインタと再帰をサポートしてない場合、別のポインタと再帰をサポートするハードウェアで演算させることができます。開発者にはこのあたりはいじらせず必要最小限なAPIを提供するだけという選択もありましたが、ランタイムにスクリプトを分析させる方法を選びました。このことによって開発者にこれら機能をサポートするハードウェアをフル利用できるようになります。常に後方互換性を完全に備えたCPUがあるためです。最終的に開発者はよいアプリを書くことに集中でき、ハードウェアメーカーはフル機能搭載の効果的なハードウェアを製造することができます。たとえ新機能が追加されてもアプリのコードを変更せずにアプリはその恩恵を受けることが可能になるでしょう。

使いやすさはRenderscriptの設計を左右する主要な要因でした。既存の演算プラットフォームやグラフィックプラットフォームのほとんどは、高性能なコードからコアアプリケーションコードへ結びつけるための複雑なグルーコードを必要とします。このコードは一般的に、バグを生み出しやすく書くのも苦痛です。しかし、ホストとなるRenderscriptコンパイラで実行するstatic分析はこの問題を解決するのに有用です。ユーザスクリプト毎にDalvikのグルークラスを生成します。そしてそのグルークラスとそのアクセッサの名前はスクリプトの内容に由来します。このことによりDalvikからのスクリプトの使用は非常に簡単になります。

例:アプリケーション側の実装

これらの3つの課題をふまえて、簡単な演算アプリを見ていきましょう。今回はとても基本的な例として、標準の android.graphics.Bitmap オブジェクトをとって2番目のbitmapにモノクロに変換してコピーするスクリプトを実行するとします。スクリプト自体をみるまえにまずスクリプトを呼び出すアプリケーションコードを見て行きましょう。下記のソースはHelloComputeというSDKのサンプルから引用しています。

    private Bitmap mBitmapIn;
    private Bitmap mBitmapOut;
    private RenderScript mRS;
    private Allocation mInAllocation;
    private Allocation mOutAllocation;
    private ScriptC_mono mScript;

    private void createScript() {
        mRS = RenderScript.create(this);

        mInAllocation = Allocation.createFromBitmap(mRS, mBitmapIn,
                                                    Allocation.MipmapControl.MIPMAP_NONE,
                                                    Allocation.USAGE_SCRIPT);
        mOutAllocation = Allocation.createTyped(mRS, mInAllocation.getType());

        mScript = new ScriptC_mono(mRS, getResources(), R.raw.mono);

        mScript.set_gIn(mInAllocation);
        mScript.set_gOut(mOutAllocation);
        mScript.set_gScript(mScript);
        mScript.invoke_filter();
        mOutAllocation.copyTo(mBitmapOut);
    }

ここで、既に作成されている2つのBitmapは同じサイズで同じフォーマットである前提です。

Renderscriptアプリケーションでまず必要となるものはContextです。 これは、Renderscriptオブジェクトを作成したり管理するのに使用されます。1行目でmRSというRenderscriptオブジェクトを生成しています。アプリがこのRenderscript自身やこのRenderscriptから作成された他のオブジェクトを使用する間はmRSは必要となります。

次の二つの関数はBitmapからアロケーションを計算し作成する関数です。Renderscriptには自身のメモリアロケータを持ちます。なぜなら、メモリは、潜在的マルチプロセッサによって共有されておりもし可能なら1つ以上のメモリスペースに存在するためです。アロケーションが作成する場合、システムが正しいメモリの種類を判断して選択するかもしれないので、潜在的な場合の処理について列挙型で定義する必要があります(第3引数、ここではAllocation.MipmapControl.MIPMAP_NONE)。

1つ目の関数createFromBitmap()は、マッチするRenderscriptのアロケーションオブジェクトを生成してそこへbitmapのコンテンツをコピーします。アロケーションはRenderscriptで使用されるメモリの基本単位です。2つ目の関数createTyped()では、1番目のアロケーション(mInAllocation)と同じ構造の2番目のアロケーション(mOutAllocation)を生成します。構造はgetType()で取得できます。Renderscriptタイプはアロケーションの構造で定義します。今回の例では、Renderscriptタイプは高さ、幅、bitmapのフォーマットから構成されてます。

次の行では、「mono.rs」と名付けられたスクリプトをロードしています。 R.raw.mono によってスクリプトを特定しています。スクリプトはアプリケーションのAPKファイルのなかにrawデータとして配置します。自動的に生成されたグルークラスの名前は、ScriptC_monoであることを注意してください。

次の3行は、グルークラスのなかで生成されたメソッドを使って、スクリプトのプロパティを設定しています。

これで全ての設定は終わりました。invoke_filter()という関数は実際にはいくつかの働きをしています。invoke_filter()はスクリプト内のfilter()関数をコールします。もし関数がパラメータを持っていた場合、ここで渡されます。invoke_filter()は非同期で実行されるため値は返されません。

最後の行の関数でスクリプトの演算結果をbitmapをコピーします。スクリプトを確実に実行させるためにビルドインの同期コードが必要となります。

例:スクリプト側の実装

ここでは上記の例の中で実行されているmono.rsの中に配置されるRenderscriptについて説明します。

#pragma version(1)
#pragma rs java_package_name(com.android.example.hellocompute)

rs_allocation gIn;
rs_allocation gOut;
rs_script gScript;

const static float3 gMonoMult = {0.299f, 0.587f, 0.114f};

void root(const uchar4 *v_in, uchar4 *v_out, const void *usrData, uint32_t x, uint32_t y) {
    float4 f4 = rsUnpackColor8888(*v_in);

    float3 mono = dot(f4.rgb, gMonoMult);
    *v_out = rsPackColorTo8888(mono);
}

void filter() {
    rsForEach(gScript, gIn, gOut, 0);
}

1行目はネイティブのRenderscript APIのリビジョンを定義しています。2行目はスクリプトから生成されるリフレクトコードで使うために、リフレクトコード内で定義すべきパッケージを指定します。

次の3つのグローバル変数は、アプリケーション側のコードで定義したグローバル変数と関連しています。4つ目のグローバル変数はstaticとして宣言されているのでリフレクトされません。非staticなグローバル定数はリフレクトはされますが、単にリフレクトされるgetメソッドを生成するだけです。これはスクリプトとアプリケーション側のコードの間で共通の定数の使いたい場合に使用します。

root()関数は、Renderscript固有の関数で、概念的にはCでいうmain()のようなものです。ランタイムによってスクリプトが実行されるとき、この関数がよばれます。この例の場合、パラメータは、アロケーションへ入出力されるピクセルデータとなります。次のパラメータのユーザポインタは処理が実行されるアロケーションのアドレスの範囲内で渡すことができます。今回の例ではこれらパラメータは無視しています。

root関数の中に3行を解説します。まず1行目については、これは渡されたアロケーションから色情報をRGBA_8888を取り出し、これを4次ベクトルのfloatとして値を書き出しています。2行目は、入力されたピクセルデータをモノクロ用の定数gMonoMultで計算しグレイレベルを取得しようとしており、そのためにビルトインされた数学関数dotを使用しています。dot関数は、単体のfloat一つだけ返す処理時間で、x,y,z要素の各値が単純にコピーされたfloat3を使えることに留意してください。そして3行目で、float3を32ビットの画素へつめ直すビルドイン関数を使います。これはオーバロードされた関数の一例で、RGB(float3)をとるかRGBA(float4)のデータをとるかによってrsPackColorTo8888の種類が複数あります。もしalpha値が提供されない場合、オーバロードされた関数ではalpha値を1.0fと推測します。

filter()関数は変換を行うためものでアプリケーション側のコードからよばれます。アロケーションの各要素の計算を単純に駆動させるものです。この第1パラメータは駆動すべきスクリプトを指定すると、指定したスクリプトのroot関数が起動されます。第2、第3のパラメータは入力データと出力データのアロケーションです。最後のパラメータは、root関数へ追加情報を渡したいときに使用するユーザデータのポインターです。

forEachはマルチプロセッサーを持つ端末でマルチスレッドを実行する関数です。将来的にforEachは1つのプロセッサからもう1つへ制御権を渡すような機能追加を実施する予定です。そうなると将来的に、今回の例でいうところのfilter()がCPU上で実行でき、root関数はGPUDSP上で実行できるようになります。

最後に

Renderscriptの設計背景とその使用方法について、この記事によって理解が深まることを願っています。