エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

Android

Android NDK r8でビルドしたバイナリがAndroid6で動作しない

Android NDK r8でビルドした.soが、Android6でDllNotFoundExceptionになることがあります。logcatを見てみると、hoge.so: has text relocations、というエラーが発生しています。

調査したところ、Android 6.0 Marshmallow (API23)はテキスト再配置をサポートしておらず、この機能を持つライブラリは実行できないようです。

暫定対処としては、targetSDKVersionを23ではなく、22にするとよいようです。

抜本的には、Android NDK r10eでのリビルドを推奨なようです。

Unity 5.5.2のAndroidにおいてApplication.Quitで例外が起きる

Unity 5.5.2のAndroidでアプリケーションからApplication.Quitを呼ぶと、Galaxyなど特定の機種において、libcで例外が起き、アプリケーションが停止されましたと表示されます。Unity 5.6では起きません。

対策としては、Application.Quitではなく、moveTaskToBackを呼ぶとよいようです。

#if UNITY_ANDROID
if (Input.GetKeyDown(KeyCode.Escape)){
	AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").
	GetStatic("currentActivity");
	activity.Call("moveTaskToBack" , true);
	return;
}
#endif

海外のフォーラムでは、StartCoroutineで描画終了を待てば改善するとありましたが、試してみても改善しませんでした。

Android 4.4と5.0でGIFの透明度が失われる

Android版のメトセライズデストラクタで、キャラの背景が黒く表示されると報告を受けて調査したところ、Android 4.4と5.0のGIFの読み込みでアルファチャンネルが失われるようです。

Android 4.4 KitKat: GIFs lose transparencyによると、KitkatからSkImageDecoder_libgif.cppが新しいバージョンのgiflibを使うようになったために、違うifdefのルートを通るようになり、問題が発生しているようです。Android 5.0でも同様の問題があります。

修正パッチもコミットされましたが、Skiaにパッチを投稿してくれと言われて、リジェクトされているようです。Skiaに投稿されたパッチもまだマージされていないですね。

PNGだと問題は起きませんので、可能であればGIFを使わないという運用がよさそうです。他、ワークアラウンドとしては、android-gifviewのJAVAで書かれたGifDecoderを使うという手もあるようです。

以下のように、OSのバージョンがKitKat以上の場合は、GifDecoderを使うようにすると、速度的には問題なく動作しました。

            final int KITKAT_SDK_INT=19;
            if(name.indexOf(".gif")!=-1 && android.os.Build.VERSION.SDK_INT >= KITKAT_SDK_INT){
                GifDecoder gd = new GifDecoder();
                gd.read(buf);
                bmp=gd.getBitmap();
            }else{
                bmp = BitmapFactory.decodeStream(buf,null,options);	//ARGB8888 decode
            }

android-ndk-r10cでNEONコードがビルドできない問題への対処

Android NDK r10cでarm64-v8aのビルドに対応しましたが、NEONを有効にしている場合、次のようなエラーが出ます。


Android NDK: NEON support is only possible for armeabi-v7a ABI its variant armeabi-v7a-hard and x86 ABI


このエラーを回避するには、/library/android-ndk-r10c/build/core/build-binary.mk:283を以下のように修正する必要があります。


ifeq ($(filter $(TARGET_ARCH_ABI), armeabi-v7a armeabi-v7a-hard arm64-v8a x86),)


また、Android NDK r8dからstlportでも例外が使えるようになっているので、例外を使わないようにハックしている場合はシンボルの衝突が発生する可能性があります。以下で定義されているbad_allocと衝突しているので、例外部分は、gabi++を呼ぶ実装のようです。


library/android-ndk-r10c/sources/cxx-stl/stlport/stlport/../../gabi++/include/typeinfo


Android NDK ネイティブプログラミングによると、GABI++は将来的にサポート予定となっている例外機構が利用できるSTLport+RTTIというライブラリの基礎とするために開発されたそうなので、計画通りの実装のようです。

後、Android NDK r10から、NEONを自動的にSSEに変換する機能が搭載されたようです。インテル製のarm_neon.hをincludeするだけで、自動的に命令を変換してくれます。x86 AndroidでのNEONエミュレーションに続き、インテルすごいです。

ME176CにME173XのUSBドライバを入れる

x86 Androidの検証用に購入したME176CのUSBドライバが公開される気配がありません。これでは開発に使えないので、いろいろ調べていたところ、2chにME173Xのドライバが使えるとありました。試してみたところ、問題なく動作しましたので、お困りの方は、こちらを利用されるとよいかと思います。

x86 AndroidはARMエミュレータが超高速で、エミュレーションにもかかわらずCortex A9 1.2GHzと同じくらいの速度を叩き出すので、一見の価値があります。NEONをばりばり使ったarmv7バイナリしか含んでいないアプリが普通に高速に動いてびっくりしました。NEONもSSEでエミュレートしてそうな速度です。

ワイヤレスでAndroidに自炊データを転送する

PCからAndroidに自炊データを転送する場合に、USBケーブルで接続するのは面倒です。できればワイヤレスで転送したい。そんな時に便利なアプリがonairです。

onairを使うと、AndroidをFTPサーバとして動作させることができます。

IMG_0204
onairを起動するとこんな画面になるので、右上のアイコンをタップしてLinuxモードにした後、中央の電源ボタンを押します。

android_onair
次に、PCのFTPソフトで、Androidの画面の右下に出ているURLに接続します。ユーザIDはonair、パスワードは右下の4桁の数字です。後は、普通にFTPで転送すればOKです。

OnAirのダウンロードはGooglePlayから行えます。

-----------------------
2012年10月6日追記:Android4.0だとonairのアドレスが正常に取得できないので、DroidNASの方が便利です。

JavaScriptに追加されたTypedArrayの速度比較

いつの間にかJavaScriptにTypedArrayなるものが追加されていたので速度を比較してみました。今まで、array=new Array(1000)とかしていたのを、array=new Int32Array(1000)とするだけで使えます。

WebGL向けに開発されたということで、普通のArrayよりもパフォーマンスが向上するのでは?という期待感から計測してみました。

計測コードを実行する

一番高速なものを青、特別遅いものを赤で表記しています。単位はmsecです。

.

.

ArrayFloat32ArrayUint8ArrayInt16ArrayInt32Array

.

Chrome19確保047122243

.

変数書き込み251485858692

.

int定数書き込み3153515877

.

float定数書き込み397475257159

.

読み込み4448525994

.

Safari5確保030122445

.

変数書き込み720652820819823

.

int定数書き込み180645801783822

.

float定数書き込み136723103710201008

.

読み込み203896827821815

.

iOS5(NewIpad)確保025142169

.

変数書き込み40502094251324882489

.

int定数書き込み13422107257325562525

.

float定数書き込み8832099299730032978

.

読み込み11074053372237323440

.

Android4確保111101

.

変数書き込み9124701585658405901

.

int定数書き込み167110597659526007

.

float定数書き込み171107731172908430

.

読み込み340186879388489152

.

.



これを見て分かるのは以下の三点です。
・iOSは基本的にArrayを使うのがよい
・AndroidはFloat32Arrayを使うのがよい
・AndroidのIntArray系は50倍遅くて使い物にならない

PC系では以下の二点です。
・ChromeのIntArrayはそんなに悪くない
・SafariのIntArrayは4倍遅いぐらい

JavaScriptのボトルネックは間違いなくメモリアクセスの遅さで、ハッシュではないTypedArrayであれば劇的に高速になってもっと面白いことができるようになる!と思ったのですが、世の中そんなに甘くはありませんでした。速くなるどころか、普通に遅くなります。

iOSでは無難にArrayを使いましょう。特に画像フィルタのようにライトよりもリードの方が多いアプリの場合、4倍くらい速度差が出そうです。ちなみにCanvasのgetPixelDataメソッドの戻り値はUint8Arrayのようですので、場合によってはArrayに変換してもメリットが出るかもしれません。

iOSとAndroidとPCのJavaScriptの処理時間の比較と高速化ノウハウ

2048*2048[px]の画像に対してJavaScriptでフィルタをかけることを想定して、2048*2048*4回の基本演算の所要時間と、2048*2048[px]のCanvasに対するgetImagePixelsとputImagePixelsの速度を計測しました。テストプログラムとテストコードは以下です。

テストプログラム

結果は次の通り。

.

.

計測項目iOS5(NewIpad)Android4(Nexus)Safari5(core i7)Chrome19(core i7)

.

基本演算a++386118482

.

a+b(int)4411044825

.

a+b(int)(|0)6721324925

.

a+b(float)479747825

.

a*b5211039331

.

a/b69339512370

.

a<<b411674943

.

配列array.lengthでループ205234113338

.

array.lengthをローカル変数にコピーしてループ11113538938

.

ループ内で関数呼び出し194935010645

.

CanvasgetImageData51120353167

.

putImageData431955116

.

単位はmsec


ChromeはSafari5よりも基本演算の性能が2倍以上高くなっています。しかし、getImagePixels/putImagePixelsでは、逆にSafari5の方が3倍以上高速です。Chromeはベンチマークでよく使われる基本演算に特化して最適化されており、ベンチマークの少ないCanvas系の最適化はあまり行われていない印象です。

加算と乗算については、Safari5を除いて、速度差はありません。命令のクロック数の差よりも、JavaScriptのコストの差の方が大きいため、乗算を、加算やシフトに置換してもメリットは少なそうです。

floatで演算するよりも、|0をしてintの空間にしてから計算した方が速いという話もありましたが、計測結果を見る限り、|0のコストの方が高いように思われます。

ループ条件の最適化については、AndroidとChromeの場合はfor(var i=0;i<a.length;i++)と書いてもcnt=a.length; for(var i=0;i<cnt;i++)と書いても、速度差は無いようです。しかし、iOSやSafariの場合は、依然として2倍程度の差がありますので、特にiOS向けに開発する場合は、ループ条件のローカル変数へのコピーは必須のようです。また、iOSはループ内での関数呼び出しのコストが、他よりも高いですね。

getImageDataとputImageDataには非対称性があるようです。getImageDataよりもputImageDataの方が10倍も高速です。getImageDataは意外と重いみたいなので、使いすぎに注意ですね。

とりあえずiOSにおける高速化のポイントをまとめると、以下のような感じです。

・配列の.lengthをローカル変数にコピーしてからループすると2倍高速になる
・関数はできるだけインライン展開する
・getImageDataはputImageDataの10倍遅い
・乗算をシフトや加算に置換してもあまり速くならない
・|0でintにキャストして演算すると逆に遅くなる

全体的に、iOSの処理時間は、PCで実装した処理時間の10倍程度遅いと見積もっておけばよさそうです。

Android NDKでStaticLibraryにデバッグ情報を出力しないようにする

Andoid NDKにおいて、SharedLibraryの場合は自動的にstripされてデバッグ情報が消去されるのですが、StaticLibraryの場合はreleaseビルドにしても消去されません。これは、Android NDKのビルドシステムが、releaseビルドの場合でも-gオプションを付けてコンパイルしているためです。

実際に、C:\cygwin\android-ndk-r6b\build\coreの中にある、add-application.mkを見てみましょう。


# set release/debug build flags. We always use the -g flag because
# we generate symbol versions of the binaries that are later stripped
# when they are copied to the final project's libs/ directory.
#
ifeq ($(APP_OPTIM),debug)
APP_CFLAGS := -O0 -g $(APP_CFLAGS)
else
APP_CFLAGS := -O2 -DNDEBUG -g $(APP_CFLAGS)
endif



このように、将来のstripを見越して、常に-gが付いていることが分かります。SharedLibraryの場合は確かにstripされるのですが、include $(BUILD_STATIC_LIBRARY)と設定してStaticLibraryでビルドする場合はstripされません。コードパスやデバッグ情報が丸見えです。

ということで、release時は-gを付けないように書き換えて、上書きしてしまいます。すると、例えば300KBだった出力ファイルが59KBになります。

Androidの動画配信アプリまとめ

Androidの動画配信アプリを調査してみたのでまとめです。ざっくり調査しただけなので、間違っていたら教えて下さい。ユニークだと思った所に色をつけています。全てストリーミングによる配信です。全体的にBeeTVがユニークで、一般的なサイトは25分程度のコンテンツを210円程度で販売しているのに対して、BeeTVのみが8分程度のコンテンツを月額課金で販売しています。BeeTVは字幕モードもあったりと、携帯での視聴に特化している印象です。尚、全てのサイトは動画選択画面をWebViewで作っていました。動画の形式はH.264のプログレッシブダウンロードが多いです。FLVはDRMが必要なケースと、PCと同じフォーマットで配信する目的で使われているようです。HTTP以外のプロトコルによるライブストリーミングは使われていないようです。iOSは動画配信などのマーケットアプリを認めていないので、各社Androidに注力している印象です。

アプリ運営コンテンツ種類コンテンツ単価コンテンツ秒数OSCODEC課金方法画質シーク単位備考
MovieGate角川角川系のアニメ/ドラマ210円25分程度2.2以降FLV
(ブラウザ内)
クレジット、電子マネー1種類不明GalaxyS2だと再生が開始できなかった
ビデオマーケットビデオマーケットフジテレビ系のアニメ/ドラマ210円25分程度2.1以降H.264
(プログレッシブDL)
(別アプリ起動)
クレジット、
キャリア課金
3種類
(3G/HSPDA/Wifi)
15sec
ビデオストアTVBankTBS系のドラマ/アニメ210円(アニメ)
315円(ドラマ)
105円でHDオプション
25分程度2.2以降H.264
(プログレッシブDL)
(アプリ内View)
キャリア課金
(Docomoを除く)、
クレジット
1種類30secソフトバンクはWifiのみ
BeeTVAvex/Docomoドラマ/海外ドラマ/コミック月額315円8分程度2.1以降
(一部2.2以降)
H.264
(プログレッシブDL)
(アプリ内View)、FLV
(海外ドラマ)
キャリア課金
(SPモード)
2種類
(3G/HSPDA)
5secコミックに音声を付けて動画化した独自コンテンツあり、電車内などの音が聞けない環境用に字幕モードあり
MF+ISAOドラマ/アニメ210円25分程度2.1以降H.264
(プログレッシブDL)
(別アプリ起動)
キャリア課金(docomo/au)、
電子マネー、
クレジット
1種類15sec

そろそろマーケットアプリのトップにWebViewを使うのをやめたほうがいいんじゃないか

AndroidのDocomoMarketもビデオマーケットもM-trixMarketもMobageも全てトップ画面をWebViewで作ってHTMLを流し込む作りになっていて、もっさりしすぎているのを何とかするべきではないかと思う今日この頃です。

AndroidのWebViewはスクロールがかくかくしすぎているし、タップしてページ遷移する度に画面全部をレンダリングするので、ユーザビリティが低すぎるのです。また、画像を全てストリーミングで読み込んでくるので、毎回ロードが発生しますし、本来はコンテンツデータだけを取得すればいいところを、HTMLやCSSなども毎回読み込んでこないといけないので、細いモバイルの回線では厳しすぎます。

ということで、ゲームと同じような作りにしてしまって、データベースからHTTPで必要な情報だけを取ってきてレンダリングするのがいいと思います。こうすると、60FPSでぬるぬるスクロールできるし、通信も最低限、UIのリソース画像などはアプリ内のローカルリソースを使えるので回線を圧迫しません。また、描画にOpenGLをばりばり使えるので、今まで見たことのないようなリッチなマーケットアプリも作れます。

理想型なのが、Android4.0のYouTubeアプリです。あれは素晴らしい。さくさくスクロールするし、動画のリストをクリックしてからすぐに次の動画の再生が始まるので、どんどん次の動画を見ていきたくなります。画面内で動画を再生しながら他の動画も探せますし、この快適さはWebViewでは到達できません。



Mobageのngcoreが流行らないのは、MobageアプリをHTMLで作ってしまったからだと思います。HTMLで作ってしまったので、HTMLからngcoreを起動しないといけない。なので、HTMLゲームの方が遷移が無い分、素早く始められてしまって、どうせHTMLゲームしかしないならブラウザ版でよくてアプリを使わないという流れになってしまっています。決して、ブラウザ版の方がよいのではなくて、ngcoreのゲームの起動が面倒すぎる、遅すぎるだけです。逆に、ngcoreでMobageのポータルアプリを作ってしまって、遷移なしにngcoreのゲームが起動すれば、スマートフォン向けのソーシャルゲームはngcoreの方が流行ると思います。そうすれば、スマートフォンらしいソーシャルゲームが見えてくるんじゃないかなと思います。

更新が楽なのは分かるのですが、スマートフォンの良さを活かす意味でも、そろそろWebViewでマーケットアプリを作るのはやめたほうがいいんじゃないかと思う今日この頃です。

AndroidとiOSの開発環境を比較してみた

AndroidとiOSの開発環境を比較してみました。個人的に好きな方に色をつけています。

AndroidiOS解説
開発言語JavaObjectiveCJAVAはリソースの開放をGCに任せられるので楽、iOSは文字列制御だけでも複雑でretainを忘れてメモリリークが発生したりと結構大変
統合開発環境EclipseXcode開発環境は同等ですが、好みでEclipse
エミュレータ遅すぎて使えないかなり優秀Androidのエミュレータは起動だけに数分かかる上にほとんど使えないぐらい重いので実機デバッグが必須、iOSは普通の速度で動いて快適
デバッグ大変容易Androidは機種数が多すぎてメジャーなVDPだけでも4種類あってOSのverもいろいろあるので4機種程度は買わないといけない、iOSは3GSと4で検証すればほぼOK
動作速度遅い速いAndroidはGCとVMがボトルネックで速度があまり出ない、NDKを使えばiOSに近いパフォーマンスを出すことはできるが機種依存が若干不安、iOSはネイティブなので最速
UI設計コードで配置GUIで配置iOSの方がUIは楽に配置できるけどドラッグでオブジェクト間の依存関係をつないだりするのがわかりにくい、Androidは全てプログラムで制御できるのはいいけど狙ったレイアウトにするのがなかなか難しい、ゲームを作る分にはAndroidの方が楽
アプリの配布制約なし制約ありAndroidはapkファイルを配るだけでOK、iOSは事前に登録した機種にしかインストールできない上に3ヶ月ごとにプロファイルを更新してリビルドしなければ開発中のアプリは使えなくなるためアプリを自由に配布することができない、せめて1年にして欲しいです
プラットフォームアプリの開発制約なし制約ありアプリマーケットや電子書籍マーケットなど、プラットフォーム系のアプリはAndroidでは自由に作れますが、iOSでは審査で落とされることが多いです
アプリの売上まだ市場規模が小さい市場規模が大きいiOSの方が3倍程度売上が大きいです


全体的に作りやすいのはAndroidです。初めてのアプリ開発はAndroidがオススメ。ただ、アプリの売上はiOSの方が大きいのが悩ましいところですね。個人的には、Androidでプラットフォーム型のアプリを作るのが将来性があるかなと思ってます。

後、GUI系はWindowsのVisualStudioが圧倒的に作りやすいので、将来的にWindows8とWindowsPhone7が統合されると、結構いいアプリが出てくるんじゃないかなと思っています。

AndroidのNDKでNEONを使った場合にJavaの浮動小数変数が破壊される問題への対策

AndroidのNDKでNEONを使っていて、Javaのdoubleと混載した場合に、doubleの変数が破壊されたので調べてみました。まず、NEONのレジスタのd8-d15がFPUと共有されていて、このレジスタの退避ルールはcallee savedで、関数の呼びだされ側、つまりNDKのコード側で退避しなければならないようです。


I’ve only used assembly programming so far, so I will generally describe techniques under assembly terms, but everything should be equally applicable when programming with intrinsics, just with a different syntax. There is one important thing, however, which is applicable only when you program in assembly (you don’t have to worry about that when using intrinsics): make sure you save and restore d8 to d15 if you use them in your function, as the ABI specifies that these registers must be preserved by function calls. If you don’t, it may work fine for a while, until you realize with horror that the floating-point variables in the calling functions become corrupted, and all hell breaks loose. So make sure these registers are saved and restored if they are used.
引用:Introduction to NEON on iPhone


上記にはintrinsicで書かれていれば特に気にしなくても問題無い(コンパイラが自動的にケアする)と書かれていて、実際にiOSでは問題が出ないのですが、Android環境では問題が出ていて、オブジェクトファイルを逆アセンブルしてみても退避コードが生成されていないです。(GCCは4.5.3を使っています)

ちなみにVP8では自前で退避しているようです。


Regarding to your question on
#if HAVE_ARMV7
void vp8_push_neon(INT64 *store);
void vp8_pop_neon(INT64 *store);
#endif
this code is written for ARM7 NEON. For ARM NEON, d8-d15 are
callee-saved registers. vp8_push_neon() saves d8-d15 values before
going into decoding, and vp8_pop_neon() restores those saved values
after decoding is done. The prevents problem when these registers are
used in user application and their values need to be preserved before
and after the decoding. For example, if you measure time before and
after the decoding for testing performance, the beginning time needs
to be preserved, which is floating-point number and can be in d8-d15.
Therefore, d8-d15 values have to be stored to make it right.
引用:Some questions about VP8 decoder assembly functions


ということで、次のようなコードでNEONを使う関数を囲むことで、レジスタを退避してみました。

#define PUSH_NEON if(check_neon()){asm("vpush {d8-d15}");}
#define POP_NEON if(check_neon()){asm("vpop {d8-d15}");}

結果、JAVA側のdoubleの変数が破壊されなくなったので、やはりレジスタの退避はうまくいってなかったようです。

推測するに、JNIを使うとmain関数が無いため、コンパイラがレジスタ退避コードを入れる場所を上層へ押し込んでいった結果、JNIのトップを最上位と認識できず、入れられなかったのかなと考えています。iOSの場合は、スタティックライブラリなので、コンパイラが自動的に入れられた予感です。

ただ、これだとまだ問題があります。コンパイラがvpushとvpopを認識できずに最適化してしまって、スタックを破壊してしまうことがあります。より安全には、スタックではなくローカル変数にNEONレジスタの内容を退避します。

具体的に、次のようなアセンブラファイルを作ってsave_neon_reg.sで保存します。


@ save callee saved neon registers {d8-d15}

 .equ DO1STROUNDING, 0

  .global push_neon
  .type push_neon, function
  .global pop_neon
  .type pop_neon, function

  @ ARM
  @
  @ PRESERVE8

.text
.p2align 2

_push_neon:
  push_neon: @ PROC
  vst1.i64 {d8, d9, d10, d11}, [r0]!
  vst1.i64 {d12, d13, d14, d15}, [r0]!
  bx lr

  .size push_neon, .-push_neon @ ENDP

_pop_neon:
  pop_neon: @ PROC
  vld1.i64 {d8, d9, d10, d11}, [r0]!
  vld1.i64 {d12, d13, d14, d15}, [r0]!
  bx lr

  .size pop_neon, .-pop_neon @ ENDP

  .section .note.GNU-stack,"",%progbits


Android.mkにsave_neon_reg.sを追加します。


LOCAL_SRC_FILES += \
_save_neon_reg.s.neon \


NEONを使う関数を呼ぶ前後でpush_neonとpop_neonを呼びます。


typedef long long INT64;
const int NEON_REGISTER_SIZE=8;

extern void push_neon(INT64 *store);
extern void pop_neon(INT64 *store);

int is_neon=check_neon_support();

void hoge(void){
 INT64 dx_store_reg[NEON_REGISTER_SIZE];
 if(is_neon){
  push_neon(dx_store_reg);
 }
 NEONを使う処理
 if(is_neon){
  pop_neon (dx_store_reg);
 }
}


これでばっちりです。

AndroidNDKとiOSでライブラリを作る際に不要なシンボルをエクスポートしない方法

AndroidやiOSではライブラリを作ることができます。例えばAndroidではAndroidNDKを使用してC++のソースから.soファイルを作成します。(NDKの使用方法)

NDKでライブラリを作る場合:

コンパイル方法
(1)jniフォルダにC++ソースとmakeファイルを置く
(2)ndk-buildコマンドを実行
(3)libsフォルダに.soが出来る

インタフェース用の.javaを作成
(1)System.loadLibrary("library_name");と記述
(2)C++で記述したものと同じ引数を持つ関数をnative指定で記述


NDKで作ったライブラリを使う場合:

.soと.javaをプロジェクトのlibsフォルダとsrcフォルダにコピー
javaの関数を呼ぶとJava_*というC++の関数が呼ばれる


また、iOSの場合はXcodeの”プロジェクトの新規作成”からStaticLibraryを選択することでスタティックライブラリのプロジェクトを作成することができます。このプロジェクトでコンパイルするとスタティックライブラリである.aファイルが作成されます。ただし、iOSはシミュレータ用にはi386命令、実機用にはarm命令でコンパイルする必要があります。そこで、ビルドターゲットをプロジェクト内で複製し、シミュレータ用とARM用にそれぞれビルド、Lipoで結合してユニバーサルバイナリにします。


lipo -create "DerivedData/test/Build/Products/Release-iphoneos-iphone/test.a" "DerivedData/test/Build/Products/Release-iphonesimulator-simulater/test.a" -output "test.a"


スタティックライブラリを使う場合は、作成したユニバーサルバイナリのtest.aとインタフェースを記述した.hをプロジェクトに追加すればよいです。

しかし、このままだと、Android/iOS共に、関数名や構造体名がだだ漏れになります。試しに.soや.aをテキストエディタで開いてみると、関数名が含まれていることが分かります。このようなライブラリをそのまま配布すると、知られたくない設計情報を解析することが可能になってしまいます。そこで、このような不要なシンボルをエクスポートしない方法を考えます。

Androidの場合は比較的簡単です。gccのビルドオプションに-fvisibility=hiddenを付けると、不要なシンボルはエクスポートされなくなります。エクスポートしたい関数にだけ

 __attribute__((visibility("default")))

を付けます。

 __attribute__((visibility("default"))) JNIEXPORT jint JNICALL Java_abars_test_CreateInstance(JNIEnv *env, jclass obj, jstring filename)

みたいな感じです。

ここまではいいんですが、iOSではこのオプションが使えません。というのも.aはオブジェクトファイルを結合しただけの形式だからです。従って、リンク時に有効な-fvisibilityは無効で、有効な解決策はありません。単純なライブラリであれば、無名名前空間に隠したい関数を全て入れてしまえばよいですが、テンプレートを使っているとうまくいきません。

ということで、結論としてはiOSではシンボルを隠せません。妥当な解決策としては、適当なスクリプトをperlとかで書いて、関数名とクラス名を正規表現で列挙し、ClassNo1とかfunc_no1とか適当なものに置換するぐらいです。

aa3


気合で置換するとこんな感じになります。将来的にはiOSでダイナミックリンクライブラリをサポートして欲しいですね。

GalaxyS2の機種依存情報(DrawTextureExtension)

GalaxyS2のVDPにはARM社のMali-400が使われています。メトセライズデストラクタのユーザから、GalaxyS2で動かないという報告を受けていたので、さっそく、秋葉原のイオシスでGalaxyS2を買ってきました。現金特価49500円、未使用品です。

Mali-400はETC1しかテクスチャ圧縮が使えないというのが有名ですが、メトセラはテクスチャ圧縮を使っていないのに何で動かないのかなと4時間ぐらい戦ってみましたが、原因はDrawTextureExtensionでした。

OpenGL extensions available on different Android devicesでもまとめられているように、Mali-400ではDrawTextureExtensionが使えるというフラグを返します。しかし、実際に使ってみると、他のVDPと違って、例外処理をしないといけないようです。

(1)GL_COLOR_ARRAYの値を参照してしまうので無効化する必要がある
(2)glDrawTexiOESの直後にglTexCoordPointerを再設定する必要がある

具体的に、次のようなコードにする必要があります。

if(m_isMaliVdp){
 gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
}

gl.glColor4f(r,g,b,a);
((GL11)gl).glTexParameteriv(GL10.GL_TEXTURE_2D,GL11Ext.GL_TEXTURE_CROP_RECT_OES,draw_texture_extension_rect,0);
((GL11Ext)gl).glDrawTexiOES((int)(x*m_view_scale_x),(int)(y*m_view_scale_y),0,(int)(width*m_view_scale_x),(int)(height*m_view_scale_y));

if(m_isMaliVdp){
 gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, texCoords);
 gl.glEnableClientState(GL10.GL_COLOR_ARRAY);
}

Maliかどうかは、次のようなコードで判別できます。

String extensions = gl.glGetString(GL10.GL_EXTENSIONS);
String renderer = gl.glGetString(GL10.GL_RENDERER);
m_supportsDrawTextureExtension = extensions.contains("draw_texture");
m_isMaliVdp=renderer.contains("Mali");

こうすると、メトセラも問題無く動くようになりました。

IMG_0451


Tegra2、Adreno200、Adreno205、PowerVRの四つで動いたのでさすがに大丈夫だろうと思っていたら意外な伏兵でした。DrawTextureExtensionはAndroid1.6の頃からの高速化の定石なので、修正しなければならないコードは多そうですね。

Mali-T604なんてのも開発されているんですね。また端末が増えそうです。

AndroidのJavaScript<->OpenGLブリッジの最適化

前回はtexSubImage2Dを高速化したのですが、今回はString.splitを削減してみました。従来は、JavaScriptから投げたディスプレイリストのコマンドをパースする際に単純にdl.split(";")としていたんですが、これをやってしまうと、ものすごい数のStringオブジェクトが新規確保されてしまいます。最近の端末で一番コストの高い処理はメモリ確保なので、これだけでペナルティが随分大きかったのです。


a



どれぐらいペナルティが大きかったかというと、全体の20%をsplit関数だけに費やしてしまうぐらい重かったのです。そこで、ディスプレイリストをパースするクラスを作って、

public int pop_int()
{
if(m_pos>=m_len)
return 0;

// Check for a sign.
int num = 0;
int sign = -1;

final char ch = m_p_dl.charAt( m_pos );
m_pos++;

if ( ch == '-' )
sign = 1;
else
num = '0' - ch;

// Build the number.
while ( m_pos < m_len ){
char c=m_p_dl.charAt( m_pos );
m_pos++;
if(c==';'){
break;
}
num = num*10 + '0' - c;
}
return sign * num;
}

のように、charAtを使用してパースするように変更しました。これで、メモリの新規確保が発生しなくなるので、劇的に速くなります。


aa



最適化結果がこれです。ゲームコアの実行後にあったsplitがごっそり無くなって、バランスのいい処理負荷になっています。これで、オブジェクト数が増えても大分実用的な速度になってきます。

iOSはObjectiveC、AndroidはJAVA、WindowsPhone7はC#と、言語がばらばらすぎるので、ゲームコアをJavaScriptにして、ライブラリ部だけをネイティブで書く実装は今後増えてくるのかなと思っています。その際の高速化のポイントは、
 (1)HttpリクエストではなくStringでまとめて命令をネイティブに渡す
 (2)GLUtilのtexSubImage2Dは縦幅が指定できないので、
   copyPixelToBufferを挟んだ上で、GL10.glTexSubImage2Dを使う
 (3)Stringのsplitは遅すぎるのでメモリ確保の発生しないcharAtベースの自作パーサを使う
の三点になりそうです。

AndroidのtexSubImage2Dで転送サイズを指定する

GLUtils.texSubImage2Dは転送サイズを指定できないという問題があります。ラップされていないGL10.glTexSubImage2Dは転送サイズを指定できるので、GLUtilsを使わずにGL10のglTexSubImage2Dを使うことで高速化できないかを考えてみました。

GLUtilsでは、bmpを与えて、
 GLUtils.texSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, m_texture.image);
のようにテクスチャを上書きします。

GL10.glTexSubImage2Dでは、ByteBufferを引数に与える必要があるため、
 private ByteBuffer m_byte_buffer;
 m_byte_buffer=ByteBuffer.allocate(TEXTURE_SIZE*TEXTURE_SIZE);

 m_byte_buffer.position(0);
 m_texture.image.copyPixelsToBuffer(m_byte_buffer);
として、bmpからByteバッファに転送します。

その後、
 m_byte_buffer.position(0);
 gl.glTexSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, TEXTURE_SIZE,height, GL10.GL_ALPHA,GL10.GL_UNSIGNED_BYTE, m_byte_buffer);
として転送を行います。

GL10ではheightが指定できるのがポイントです。つまり、
 GLUtil:1024x1024のVRAMへの転送
 GL10の場合:1024x1024のCPU RAMへの転送+1024xheightのVRAMの転送
のどちらが早いかという話になります。

一般にVRAMへの転送はかなりコストが大きいため、フォント描画などheightが動的に変わるような用途では、GL10の方を使った方が効率的になるのではないかと考えられます。

ということで、メトセラの街の画面で200回計測して処理時間の平均を算出してみました。

VDPGLUtils.texSubImage2D(1024x1024)copyPixelsToBuffer(1024x1024)GL10.texSubImage2D(1024x1024)GL10.texSubImage2D(1024x128)
Tegra2(Xoom)20.985ms1.815ms8.305ms1.1ms
Adreno200(NexusOne)12.695ms5.67ms15.02ms1.03ms
Adreno205(XperiaArc)3.775ms1.695ms3.415ms0.525ms
powerVR(galaxy)7.815ms2.61ms8.145ms1.255ms


平均的に1024x128程度の転送が発生するのであれば、copyPixelsToBufferのコストを考えたとしても、GL10の方が二倍程度性能が改善します。逆に、1024x1024の転送が発生する場合は、性能が悪化します。GL10.texSubImage2Dは転送サイズに応じてほぼリニアにパフォーマンスが低下していくので、転送高さ512程度でGLUtilsの方が有利になりそうです。ということで、動的にフォントを転送する場合はGL10.texSubImage2Dが良さそうです。

気になるのは、Tegra2だけGLUtilsが遅すぎることです。Adreno200でGLUtilsの方がGL10より良いのはByteBufferの転送コストで大体理解できるのですが、Tegra2は謎ですね。

この最適化によって、現在のJavaScript&Native Bridgeゲームエンジンの1フレームは次のようなパフォーマンスになっています。

a


大分速くなってきました。後はjavascriptから受け取ったコマンドリストのsplitを何とか高速化できればいいんですが。

AndroidのOpenGLの機種依存(2011/07/16更新)

IMG_0423

Androidには、大きく分けて下記の5種類のGPUが使用されているようです。☆マークは調達した端末です。
GPUメーカ機種
Adreno205QualcomXperia Arc☆、IS05☆
Adreno200QualcomNexusOne☆、IS03、HTC desire/EVO 4G
PowerVRImaginationTechGalaxy S☆
Tegra2NvidiaXoom☆
Mali-400ARMGalaxyS2☆

以下、OpenGLで気をつけるべき機種依存情報です。
機種依存情報が見つかり次第、追記していく予定です。
GPU非正方形テクスチャDrawTextureExtension
Adreno205
Adreno200
PowerVR
Tegra2
Mali-400

PowerVRとAdreno200で非正方形のテクスチャを確保しようとすると失敗して真白のテクスチャになります。各辺が2の乗数サイズ以外でもダメです。非正方形の場合はテクスチャ読み込み時に

int max_size=bmp.getWidth();
if(max_size<bmp.getHeight()){
max_size=bmp.getHeight();
}
int s=8;
while(s<max_size){
s<<=1;
}
new_bmp=Bitmap.createBitmap(s,s,Bitmap.Config.ARGB_8888);
canvas=new Canvas(new_bmp);
canvas.drawBitmap(bmp,0,0,null);

のように正方形化してやる必要があります。

Mali-400では、DrawTextureExtensionで例外処理をしてやる必要があります。

Webkitでゲームフレームワークを作ると何が重いか?

最近、ライブラリの最適化を進めているので、Instrumentsでメトセラのプロファイルをかけてみました。

top


まずは最上位モジュールです。render関数が描画関数、stringByEvaluationJavaScriptFromStringがゲームのJavaScriptでの実行、その他がタッチイベントなどの通知関数です。これを見ると、40%が描画、30%がゲームエンジン、10%がイベントで消費されていることが分かります。

GameloopTouchでは6回、GameloopTextfieldでは1回、stringByEvaluationJavaScriptFromStringを呼び出しています。これより、stringByEvaluationJavaScriptFromString一回につき、ゲーム全体で1%程度のロスが生じることが分かります。できるだけstringByEvaluationJavaScriptFromStringの呼び出し回数を減らすことがパフォーマンス向上で重要そうです。

renderer


次に描画関数の内部です。意外なことにcomponentsSeparatedByCharactersInSetが12%も消費していて、実描画は17%に過ぎないことが分かります。また、フォントの準備に8%の時間を消費しています。ゲームエンジンでは描画情報を文字列でObjectiveCに渡すのですが、それを区切り文字でパースする部分だけで12%も消費しているようです。したがって、このパースをいかに効率化するかが課題になりそうです。

component


componentsSeparatedByCharactersInSetの内部です。6%は分割した文字列をnewしている部分です。たしかに毎フレーム大量に文字列を新規生成すると重そうです。

render_all


実描画関数の内部です。これまた意外なことに、5.0%が文字列をintに変換する部分で消費されています。OepnGLによる描画の負荷は10%程度のようです。これにフォント描画の8%を足しても、合計18%程度ですね。

ということでまとめ。JavaScriptでゲームエンジンを動かした場合、
・OpenGLによる描画負荷:18%程度
・ゲーム部分:30%程度
・コマンドのパース:22%程度
・JavaScript<>ObjectiveC通信:10%程度
程度の負荷バランスになるようです。これだと、OpenGLを高速化するよりも、その他の部分を見直した方が最適化がかかりそうですね。

とりあえず、stringByEvaluationJavaScriptFromStringの呼び出しを減らす、コマンドにステート記憶を付けてコマンドの総量を減らす、というのが有効そうです。後、NSStringで新規メモリを確保せずにある区間を切り出す方法をご存知の方いたら教えてください。

AndroidのOpenGLで文字を書く

IMG_0410

iPhoneのOpenGLで文字を書くのAndroid版です。例によって、OpenGLにはフォント命令が無いので、自由に文字を書く場合は、テクスチャに文字を書いて、それをポリゴンで描画する必要があります。

基本は、プログラム全体で一度、

 m_texture.image = Bitmap.createBitmap(m_texture.width, m_texture.height, Config.ALPHA_8);

のように文字を書く先のBitmapを確保して、フレーム毎に

 Canvas canvas = new Canvas(m_texture.image);
 Paint paint = new Paint();
 paint.setTextSize(14);
 paint.setARGB(0xff, 0x00, 0x00, 0x00);
 canvas.drawText(line, 0, y+in_block_y+y_unit-4, paint);

とCanvasでBitmapに文字を描画して、

 GLUtils.texSubImage2D(GL10.GL_TEXTURE_2D, 0, 0, 0, m_texture.image);

と文字を書いたBitmapでテクスチャを更新してしまえばよいです。

ただし、一人で悔やむ: AndroidのView.onDrawで、改行する。で書かれているように、自動的に折り返して文字を描画する命令が無いので、paint.breakTextを使って一行ずつに切り分けて、一行ずつ描画する必要があります。

また、drawTextで指定する座標が、左上座標ではなく、文字のベースライン(下線)座標なので、そのままUV値を計算すると文字がずれるので注意です。ベースライン座標から上と下に何ピクセル存在するかは、FontMetricsを使って取得することができます。(参考:テキストの描画(FontMetrics) - Android Wiki*

 FontMetrics fontMetrics = m_paint.getFontMetrics();
 m_font_top_offset = (int)Math.ceil(0 + fontMetrics.top); //ベースライン上ピクセル(負数)
 m_font_bottom_offset = (int)Math.ceil(0 + fontMetrics.bottom); //ベースライン下ピクセル(正数)


ソースコードはhttp://www.abars.biz/blog/FontTexture.javaからどうぞ。

このコードでは、最初に1024x1024 pixelの巨大なテクスチャを確保します。その後、毎フレーム、先行して現在のフレームに必要な文字を全て描画し、texSubImage2Dでテクスチャを更新します。

現在のフレームに必要な文字を全て描画した後は、一般的なオブジェクトを描画しながら、必要なタイミングで必要な文字を描画します。一枚の巨大なテクスチャに文字が格納されているので、次のように、必要な文字に応じてUV座標を変更しながら描画します。

 no=mFontTexture.getTexture();
 sx=mFontTexture.getWidth();
 sy=mFontTexture.getHeight();
 offset=mFontTexture.getOffset();
 GraphicUtil.drawTexture(gl,x,y,sx,sy,no, 0,offset/1024.0f,sx/1024.0f, sy/1024.0f, r,g,b, 1.0f);
 mFontTexture.nextReadPoint();

今回の描画文字に対するuのレンジは0〜sx/1024、vのレンジはoffset/1024〜offset/1024+sy/1024です。

texSubImage2Dで1024x1024ピクセルの画像を転送するのにかかる時間は、XperiaArcで3〜5msec、Xoomで7〜18msecでした。PowerVRよりもTegra2の方が、テクスチャの転送には時間がかかるみたいですね。
Search
Profile

abars

アプリとWEBサービスを開発しています。最近はUnityとGAE/pyが主戦場。

ブラウザ向けMMOのメトセライズデストラクタ、イラストSNSのイラストブック、東証の適時開示情報を検索できるTDnetSearchを開発しています。

かつてエンターブレインのTECH Win誌でATULADOを連載しました。

サイト:ABARS
Twitter:abars
Github:abars

Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING