D3D Shader/OpenGL」カテゴリーアーカイブ

Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari

以前のテストで Chrome の場合だけ極端に遅かった原因が判明しました。
Chrome の WebGL getError() 呼び出しが非常に重かったためです。
getError() 無しのビルドでは 60fps を超える速度となっています。
JavaScript が遅いわけではありませんでした。

Windows 8.1 Chrome 35 (60fps)
emscripten_windows_chrome.png

InternetExplorer でも動くようになったので比較してみます。

Core i7-3615QM / Intel HD Graphics 4000
----------------------------------------------------------
Windows 8.1  Firefox 29         60fps 以上
Windows 8.1  Chrome 35          60fps 以上 (glGetError無し)
Windows 8.1  IE 11           34-60fps

いずれも fps 値が大きい方が高速です。
fps に幅があるものは、前回のテストに従い描画負荷を変更しているため。
「34-60fps」なら一番重い 1. で 34fps 前後、一番軽い 3. で 60fps 以上
出ていることを意味しています。

以下別の PC のテスト

Core i7-2720QM / RADEON HD 6750M
----------------------------------------------------------
Mac OSX 10.9  Firefox 29        60fps 以上
Mac OSX 10.9  Chrome 35         60fps 以上 (glGetError無し)
Mac OSX 10.9  Safari 7          60fps 以上

Windows 8.1   Firefox 29        60fps 以上
Windows 8.1   Chrome 35         60fps 以上 (glGetError無し)
Windows 8.1   IE 11             60fps 以上

AMD Athlon 5350 / GeForce GTX 650
----------------------------------------------------------
Windows 7     Firefox 29        60fps 以上
Windows 7     Chrome 35         60fps 以上 (glGetError無し)
Windows 7     IE 11          50-60fps

Tegra 4 Cortex-A15 / ULP GeForce 72 (Tegra Note 7)
----------------------------------------------------------
Android 4.4   Firefox 29        60fps 以上
Android 4.4   Chrome 35         60fps 以上 (glGetError無し)

↓ Chrome 向け 修正の例: Emscripten から出力された *.js の _glGetError() 内にある
return GLctx.getError() を return 0 に変更。

function _glGetError() {
      // First return any GL error generated by the emscripten library_gl.js interop layer.
      if (GL.lastError) {
        var error = GL.lastError;
        GL.lastError = 0/*GL_NO_ERROR*/;
        return error;
      } else { // If there were none, return the GL error from the browser GL context.
        //return GLctx.getError();  // ← 削除
	return	0;
      }
    }

Chrome 以外では glGetError() (WebGL の getError()) があってもなくても
速度には影響がないようです。
IE/Safari 共にかなり高速に動作しています。

これまで IE で動かなかった原因は、オプティマイザが
小さい描画の Index を BYTE Size に変換してしまっていたためでした。

また IE は WebGL Extension の対応度があまり高くないようです。
DXT, float texture はすべてのブラウザが対応していますが、
depth_texture, element_index_uint 未対応は IE だけとなっています。

上の Chrome のキャプチャ画面では depth_texture が用いられています。
↓ IE は depth_texture がないので、Tegra2/3 と同じく ShadowMap に
Color Buffer を使用しています。

Windows 8.1 Internet Explorer 11
emscripten_windows_ie

// IE11
GL_VERSION: WebGL 0.93
GL_RENDERER: Internet Explorer
GL_VENDOR: Microsoft
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_WEBGL_compressed_texture_s3tc
GL_OES_texture_float
GL_OES_texture_float_linear
GL_EXT_texture_filter_anisotropic
GL_OES_standard_derivatives

// Firefox 29
GL_VERSION: WebGL 1.0
GL_RENDERER: Mozilla
GL_VENDOR: Mozilla
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_EXT_texture_filter_anisotropic
GL_OES_element_index_uint
GL_OES_standard_derivatives
GL_OES_texture_float
GL_OES_texture_float_linear
GL_OES_texture_half_float
GL_WEBGL_compressed_texture_s3tc
GL_WEBGL_depth_texture
GL_WEBGL_lose_context
GL_MOZ_WEBGL_lose_context
GL_MOZ_WEBGL_compressed_texture_s3tc
GL_MOZ_WEBGL_depth_texture

// Chrome
GL_VERSION: WebGL 1.0 (OpenGL ES 2.0 Chromium)
GL_RENDERER: WebKit WebGL
GL_VENDOR: WebKit
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_ANGLE_instanced_arrays
GL_EXT_texture_filter_anisotropic
GL_WEBKIT_EXT_texture_filter_anisotropic
GL_OES_element_index_uint
GL_OES_standard_derivatives
GL_OES_texture_float
GL_OES_texture_float_linear
GL_OES_texture_half_float
GL_OES_texture_half_float_linear
GL_OES_vertex_array_object
GL_WEBGL_compressed_texture_s3tc
GL_WEBKIT_WEBGL_compressed_texture_s3tc
GL_WEBGL_depth_texture
GL_WEBKIT_WEBGL_depth_texture
GL_WEBGL_lose_context
GL_WEBKIT_WEBGL_lose_context
GL_WEBGL_debug_renderer_info

// Safari 7
GL_VERSION: WebGL 1.0 (2.1 ATI-1.22.25)
GL_RENDERER: WebKit WebGL
GL_VENDOR: WebKit
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_OES_texture_float
GL_OES_standard_derivatives
GL_WEBKIT_EXT_texture_filter_anisotropic
GL_OES_vertex_array_object
GL_OES_element_index_uint
GL_WEBGL_lose_context
GL_WEBKIT_WEBGL_compressed_texture_s3tc
GL_WEBKIT_WEBGL_depth_texture

HOST GPU によって多少違いがあります。
OS 毎、GPU 毎のより詳しいデータは下記に載せました。

WebGL Extensions

次回: Emscripten C++/OpenGL ES 2.0 (7) Emscripten の OpenGL API と WebGL

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)

● JSライブラリ

JavaScript で書かれたライブラリと直接リンクすることができます。

// lib.js
mergeInto( LibraryManager.library, {
    print: function(x){
        console.log( x );
    }
});
// main.cpp
extern "C" {
    void print( int x );
}

int main()
{
    print( 12345 );	// これ
    return  0;
}

ビルド手順

emcc main.cpp --js-library lib.js -o main.html

C言語のリンケージに含まれるので、extern 宣言するだけで呼び出せます。

Emscription の library*.js を見ると、標準で用意された関数も
JavaScript で書かれていることがわかります。

利点としては Native な JavaScript の方が自由度が高いことが挙げられます。
ただ C/C++ で書いた場合は asm.js 対応コードに変換してくれるため、
どちらが効率がよく動作できるのかは処理内容に依存すると思われます。

● 引数

// lib.js
mergeInto( LibraryManager.library, {
    print_char : function(x){
        console.log( x );
    },
    print_short : function(x){
        console.log( x );
    },
    print_int : function(x){
        console.log( x );
    },
    print_float : function(x){
        console.log( x );
    },
    print_double : function(x){
        console.log( x );
    },
    print_llong : function(x,y){
        console.log( y * 4294967296 + x );
    },
    print_string : function(x){
        console.log( Pointer_stringify(x) );
    }
});
// main.cpp
extern "C" {
    void print_char( char x );
    void print_short( short x );
    void print_int( int x );
    void print_float( float x );
    void print_double( double x );
    void print_llong( long long x );
    void print_string( const char* x );
};

int main()
{
    print_char( 123 );
    print_short( 12345 );
    print_int( 123456789 );
    print_float( 1.2345f );
    print_double( 1.2345678901234 );
    print_llong( 12345678901234ll );
    print_string( "ABCDEFG" );
    return  0;
}
// output
123
12345
123456789
1.2345000505447388
1.2345678901234
12345678901234
ABCDEFG

ポインタを受け取る場合はメモリアクセスが必要です。
型に応じて HEAP8,HEAP16,HEAP32,HEAP64 を用いるか
setValue()/getValue() 命令を使うことができます。
文字列の Object 変換は Pointer_stringify() で可能。

double に収まらない long long (64bit int) 型は、
2個の整数 (low, high) に分解されていることもわかります。

● 戻り値

// lib.js
mergeInto( LibraryManager.library, {
    ret_llong : function(x,y){
	asm.setTempRet0( 1 );	// high
    	return	0>>>0;	// low
    },
});
// main.cpp
#include 

extern "C" {
    long long ret_llong( long long x );
}

int main()
{
    printf( "%lld\n", ret_llong( 0 ) );
    return  0;
}
// output
4294967296

64bit 型は global な戻り値レジスタ tempRet0~ を併用しています。
他の関数呼び出しで破壊される可能性があるので atomic ではない点に注意。

文字列定数など static なポインタを返すのは簡単ではないようです。
メインメモリ空間に配置されていなければならないためで、
static な data 領域はコンパイル時に作られます。
ライブラリ初期化時に HEAP から確保して代用する形になるでしょう。

C言語に返すことが可能な heap memory の確保は、
Module._malloc() / Module._free() を使用します。
C言語の runtime に含まれるので、コンパイル時にこれらの関数への参照がなければ
正しいコードが生成されない点に注意が必要です。

// lib.js
mergeInto( LibraryManager.library, {
    alloc_int : function(size){
    	return	Module._malloc( size * 4 );
    },
    dump_int : function(addr, size){
        for( i= 0 ; i< size ; i++ ){
	    console.log( Module.HEAP32[(addr>>2) + i] ); // memory アクセス
        }
    },
});
// main.cpp
#include 

extern "C" {
    int*  alloc_int( int size );
    void  dump_int( const int* buffer, int size );
}

int main()
{
    int*  buffer= alloc_int( 100 );
    for( int i= 0 ; i< 100 ; i++ ){
        buffer[i]= i * i;
    }
    dump_int( buffer, 100 );
    free( buffer );   // -- (1)
    return  0;
}

↑(1) の free が無ければ C言語で malloc()/free() が生成されないため、
JavaScript 側の Module._malloc() は正しい挙動になりません。

例えば下記のようにアロケータを完全に置き換えようとしても、
C言語から参照がないので _malloc/_free がダミーコードになります。
一見正しく動作しますがメモリリークします。

// lib.js
mergeInto( LibraryManager.library, {
    my_alloc : function(byte_size){
    	return	Module._malloc( byte_size );
    },
    my_free : function(addr){
    	Module._free( addr );
    },
});
// main.cpp
extern "C" {
    void* my_alloc( int byte_size );
    void  my_free( void* addr );
}

int main()
{
    int*  buffer= (int*)my_alloc( 100 * sizeof(int) );
    for( int i= 0 ; i< 100 ; i++ ){
        buffer[i]= i * i;
    }
    my_free( buffer );
    return  0;
}

● メモリマップ

0~7               NULL trap 領域
STATIC_BASE~      data/bss segument (8~)
STACK_BASE~       stack 領域
DYNAMIC_BASE~     heap 領域
~TOTAL_MEMORY-1

NULL アクセスはコンパイラが検出できる場合だけ例外が発生します。
動的な NULL アクセスはデフォルトではエラーになりませんが、
-s SAFE_HEAP=1 を付けることでメモリアクセス時のアドレスチェックが可能です。
その分動作速度は低下します。

stack は前方から後方に向かって確保されています。

文字列定数や static/global に確保したメモリは STATIC エリアに格納します。
初期値を持たない配列も含まれるため、下記のようなケースでは初期化データに
大量の 0 が並ぶことになります。
ファイルサイズが増加するので、HEAP から確保した方が無駄が少なく済むようです。

// main.cpp
int buffer[1024];

/* memory initializer */ allocate([0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
~

次回: Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)

Emscripten のメモリ空間は固定の配列上に確保されており、
デフォルトの USE_TYPED_ARRAYS=2 では TypedArray によって
任意の型でアクセスできるようになっています。
USE_TYPED_ARRAYS の指定により他のメモリモードを用いることもできます。

関数は JavaScript の function がそのまま用いられており、
呼び出しや引数の受け渡しは普通の JavaScript のコードになっていました。
よって明示的な CallStack は存在せず、引数は Stack を消費しません。
Stack は基本的には auto 変数のために用いられています。

可変引数は特別で、stack 上に格納したのち va_list 渡しになっています。
関数の途中では動的に stack pointer は動かないので、可変引数領域も
prologue 時に確保されているようです。

var i=env.STACKTOP|0;
var c=new global.Int32Array(buffer);

function Ra(){
    var a=0,b=0;
    a=i;	// a が frame pointer
    i=i+16|0;	// stack 確保
    b=a;	// b= local 変数 (address)
    ~
    c[b>>2]=12345;	// intアクセス, bへの代入
    ~
    i=a;	// stack pointer の復帰
    return 0
}

関数呼び出しが通常の function なので、関数ポインタは HEAP や STACK と別空間にあります。
callback などの関数では「関数ポインタ用に設けられた関数のテーブル」
に対する index 値が渡されています。
引数など型に応じてテーブルは複数存在するので、関数ポインタを別の関数型に
cast するようなコードは正しく動かないと公式サイトにも書かれています。

var _=env.abort;
var ua=env._emscripten_set_main_loop_arg;

function mb(a){ // NULL pointer 用
    a=a|0;
    _(0)	// env.abort 呼び出し
}

function fb(){	// main()
    ~

    // emscripten_set_main_loop_arg() の呼び出し
    // 最初の引数に、関数ポインタとして 1 ( Qa[1] ) が渡っている
    ua(1,0,0,1);

    i=a;
    return 0
}

// EMSCRIPTEN_END_FUNCS
var Qa=[mb,gb];  // 関数ポインタ用配列 (void (*)(int) 用)

コード中たまに見かける dynCall() が関数ポインタを使った呼び出しを行っています。

あとから気がついたのですが、emcc に –js-opts 0 を指定すると
もっと読みやすい出力が得られるようです。

function _main() {
 var $0 = 0, $ABCDEFG = 0, $vararg_buffer7 = 0, label = 0, sp = 0;
 sp = STACKTOP;
 STACKTOP = STACKTOP + 16|0;
 $vararg_buffer7 = sp;
 $ABCDEFG = sp + 4|0;
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((72|0),($vararg_buffer7|0))|0);
 $0 = (_func01(123)|0);
 HEAP32[$vararg_buffer7>>2] = $0;
 (_printf((8|0),($vararg_buffer7|0))|0);
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((16|0),($vararg_buffer7|0))|0);
 _emscripten_set_main_loop_arg((1|0),($ABCDEFG|0),0,1);
 (_puts((96|0))|0);
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((24|0),($vararg_buffer7|0))|0);
 STACKTOP = sp;
 return 0;
}

可変引数領域が stack 上に確保されていることがわかります。

JavaScript のコードとして動くため、block するような I/O 命令は
本来うまく動かないことになります。
callback が発生しないためで、一旦 event driven のための
メインループに return しなければ完了通知を受け取ることができません。

Emscripten は memory 上に仮想的な File System を作っているため、
ファイルアクセスでは何も問題が起こらず C言語コードのまま動きます。
互換性が高く、アプリケーションを比較的簡単に移植できる要因として
この仮想 FileSystem の果たす役割は非常に大きいのではないかと思います。

emscripten_set_main_loop() は一見その場で block しているように
見えますが、見た目とは全く異なる挙動をしていました。

static void ems_loop( void* arg )
{
   ...
}

int main()
{
    Initialize();
    emscripten_set_main_loop( ems_loop, 0, true );
    Finalize();
    return  0;
}

emscripten_set_main_loop() の最後の引数が ture の場合無限 Loop をシミュレートします。
実行はその場で停止し、以後毎フレーム ems_loop() が呼ばれます。

これだと event loop に return していないように見えるのですが、
JavaScript のコード上では emscripten_set_main_loop() の最後で例外を throw していました。
つまり emscripten_set_main_loop() が呼ばれた時点で main() 関数
そのものが終了してしまいます。
emscripten_set_main_loop() 以後の Finalize() 等のコードが呼ばれることはありません。

では下記のようなコードはどうでしょうか。

class AppModule {
public:
};

static void ems_loop( void* arg )
{
    AppModule* app= reinterpret_cast( arg );
    ~
}

int main()
{
    AppModule  app;
    emscripten_set_main_loop_arg( ems_loop, &app, 0, true );
    return  0;
}

例外はあくまで JavaScript の throw なので、main() が終了しても
AppModule の destructor が呼ばれるわけではありません。

また main() が終了するなら ems_loop() で stack 上の app に対する
不正アクセスが行われてしまうようにも見えますが、こちらも大丈夫でした。
main() の epilogue を通らないので、中で確保した stack 領域は
そのまま保持されたままとなっているようです。

以上より、関数呼び出しは仮想化されていないので、
現在の実行コンテキストを保存したまま一時的に Event Loop に戻るような処理は
やはり簡単には実現できないのではないかと思われます。
Thread で block することを想定した同期 I/O まわりなど。

例えば socket を使った通信は

connect( sock, &addr, sizeof(addr) );
recv( sock, buffer, size, 0 );
~
close( sock );

connect() が必ず EINPROGRESS を返すので、継続するには Event Loop に
戻らなければならないようです。
↓こんな感じで使えないかと思ったのですが簡単にはいきませんでした。

connect( sock, &addr, sizeof(addr) );
emscripten_push_main_loop_blocker( recv_loop, sock );
~
close( sock );

もし CallStack など関数呼び出しも仮想化されているならば、
自前で Context 切換するなど何らかの手段が取れるのではないかと思ったからです。

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)

プロジェクトが巨大な場合メモリ不足で起動できないことがあります。
Android のブラウザではロード中のまま止まってしまうので、
読み込みが遅いだけなのかエラーなのかわからない場合があります。
原因を調べるにはリモートデバッガが便利です。

MDN Android 版 Firefox のリモートデバッグ

(1) Android Firefox
・Menu → 設定 → デベロッパーツール → リモートデバッグ を ON

(2) HOST PC
・adb で接続したあと下記のコマンドを実行

adb forward tcp:6000 tcp:6000

(3) HOST PC Firefox
・Menu → 開発ツール → 開発ツールを表示 → 開発ツールパネルの左上の歯車アイコン → 「リモートデバッガを有効」にチェックを入れる (2014/05/26 追加)
・Menu → 開発ツール → 接続 → [接続] ボタン

(4) Android
・接続許可を求めるダイアログが出るので [OK]

(5) HOST PC Firefox
・どのタブに接続するか選択

↓エラーの例 (Android Firefox)

Successfully compiled asm.js code (total compilation time 3120ms; stored in cache) flview_jsr.js
uncaught exception: out of memory

● Library 化

bc file は merge できるので library として利用できます。

emcc src1.cpp -o src1.bc
emcc src2.cpp -o src2.bc
emcc src1.bc src1.bc -o libfile.bc

Lib 利用時

emcc file.cpp libfile.bc -o file.html

拡張子の対応付

PC          Emscripten
---------------------------------
.exe        .js/.html    実行可能
.obj/.o     .bc          object
.lib/.a     .bc          library

コマンドの対応付け

PC           Emscripten
---------------------------------
cl/clang     emcc -o *.bc
link/ld      emcc -o *.js/*.html
lib/ar       emcc -o *.bc

Emscripten はリンク時に未定義シンボルがあっても実行できます。
未定義の関数を呼び出さない限り動作に支障はないようです。

● データ型とサイズ

32bit, ILP32 です。

bool=1/1       char=1/1      short=2/2    int=4/4     long=4/4
long long=8/8  float=4/4     double=8/8
void*=4/4      intptr_t=4/4  size_t=4/4   off_t=4/4   wchar_t=4/4

(byte size / alignment size)

● EGL と OpenGL ES 2.0

3D の描画は EGL + OpenGL ES 2.0 の組み合わせを用います。
他にも初期化手順を省いてくれる便利なライブラリがいろいろあるようです。
とりあえずできるだけ低レベルと思われる手順を使っています。

enum {
    WINDOW_DEFAULT_WIDTH   =  960,
    WINDOW_DEFAULT_HEIGHT  =  640,
};
emscripten_set_canvas_size( WINDOW_DEFAULT_WIDTH, WINDOW_DEFAULT_HEIGHT );

Canvas サイズを指定したら、あとは EGL の初期化を行うだけ。

EGLDisplay  egl_display= eglGetDisplay( EGL_DEFAULT_DISPLAY );
EGLint	ver0, ver1;
eglInitialize( egl_display, &ver0, &ver1 );
eglBindAPI( EGL_OPENGL_ES_API );
~

● X11

Sample コードを見ていると Xlib の API も使えることがわかります。

XSetWindowAttributes  swa;
Display*  disp= XOpenDisplay( NULL );
Window  root= DefaultRootWindow( disp );
Window  win= XCreateWindow(
    disp, root, 0,0,
    WINDOW_DEFAULT_WIDTH, WINDOW_DEFAULT_HEIGHT, 0,
    CopyFromParent, InputOutput, CopyFromParent, 0, &swa );

内部コードを確認すると、XCreateWindow() で canvas size の設定を行っているだけのようです。
それ以外のコードは特に意味はありませんでした。
emscripten_set_canvas_size() を指定しているなら必要無いでしょう。

● 入力イベント

Xlib の Event API は実装されていないようなので、
Emscripten の callback を利用しています。

#include 
#include 


static EM_BOOL KeyCallback( int event_type, const EmscriptenKeyboardEvent* event, void* user_data )
{
    AppModule*  _This= reinterpret_cast(user_data);
    switch( event_type ){
    case EMSCRIPTEN_EVENT_KEYDOWN:
        _This->SendKeyEvent( event->keyCode, event::ACTION_DOWN );
        break;
    case EMSCRIPTEN_EVENT_KEYUP:
        _This->SendKeyEvent( event->keyCode, event::ACTION_UP );
        break;
    }
    return  TRUE;
}


static EM_BOOL MouseCallback( int event_type, const EmscriptenMouseEvent* event, void* user_data )
{
    AppModule*  _This= reinterpret_cast(user_data);
    int mouse_pos_x= event->canvasX;
    int mouse_pos_y= event->canvasY;
    ~
    return  TRUE;
}

void AppModule::Initialize()
{
    emscripten_set_mousedown_callback( NULL, this, TRUE, MouseCallback );
    emscripten_set_mouseup_callback(   NULL, this, TRUE, MouseCallback );
    emscripten_set_mousemove_callback( NULL, this, TRUE, MouseCallback );
    emscripten_set_wheel_callback(  NULL, this, TRUE, WheelCallback );

    emscripten_set_keydown_callback( NULL, this, TRUE, KeyCallback );
    emscripten_set_keyup_callback(   NULL, this, TRUE, KeyCallback );

    emscripten_set_touchstart_callback(  NULL, this, TRUE, TouchCallback );
    emscripten_set_touchend_callback(    NULL, this, TRUE, TouchCallback );
    emscripten_set_touchmove_callback(   NULL, this, TRUE, TouchCallback );

    emscripten_set_gamepadconnected_callback( this, TRUE, GamepadCallback );
    emscripten_set_gamepaddisconnected_callback( this, TRUE, GamepadCallback );
}

Mouse, Wheel, Keyboard, Gamepad は PC 上で確認。
Android のブラウザ上では Touch Event が有効でした。

Firefox の場合は EmscriptenMouseEvent の buttons に押されているボタンの
状態が入るので、MouseMove でも buttons を見るだけで判断できます。
Chrome では buttons が常に 0 だったので、ボタンが押されているかどうかは
MouseDown/MouseUp 時に自分で保持しておく必要があります。

● 残っている問題等

pthread が動作していないため、それに関連する Module をポーリングなど
他の手段に置き換える必要が生じています。
スレッドを使用していないため Atomic 系のコードも未実装です。

サウンド API は OpenAL が利用できるので、
OSX/iOS や Linux とコードを共通化できそうです。
ビルドは通っているのですがスレッドの関係もありまだテストできていません。
Socket API も実験中です。

次回: Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)

前回 Cortex-A15 の動作だけ極端に低速で少々疑問が残る結果となりました。
RAM 1MB の端末でも動くようになったのでさらに調べてみました。

Tegra4 Cortex-A15   Firefox     60fps 以上  (Tegra Note 7)
Tegra3 Cortex-A9    Firefox     20~47fps   (Nexus 7 2012)

今回の結果を見る限り、Cortex-A15 でも Krait 並に高速に動作していることがわかります。
また Cortex-A9 も予想よりも速い印象です。

上の両機種とも画面解像度が低く 1280×800 dot です。
前回速度が出なかった Nexus 10 は 2560×1600 と非常に解像度が高いので、
ブラウザが何らかの解像度に応じた CPU 転送を行っている可能性もあります。
なお内部のレンダリング解像度は 960×640 固定です。

以下詳細データ

1. 960x640 63万Tri+Animation

Tegra4 Cortex-A15   Firefox     60fps 以上   (Tegra Note 7)
Tegra3 Cortex-A9    Firefox     20fps 前後   (Nexus 7 2012)

2. 960x640 63万Triangles

Tegra4 Cortex-A15   Firefox     60fps 以上   (Tegra Note 7)
Tegra3 Cortex-A9    Firefox     25fps 前後   (Nexus 7 2012)

3. 960x640 6万Triangles

Tegra4 Cortex-A15   Firefox     60fps 以上   (Tegra Note 7)
Tegra4 Cortex-A15   Chrome      12fps 以上   (Tegra Note 7)
Tegra3 Cortex-A9    Firefox     47fps 前後   (Nexus 7 2012)
Tegra3 Cortex-A9    Chrome       8fps 前後   (Nexus 7 2012)
K3V2   Cortex-A9    Firefox       描画崩れ   (dtab)
K3V2   Cortex-A9    Chrome        動作せず   (dtab)

・fps が大きい方が高速

Tegra3 は JavaScript の動作にはまだ余裕があるものの、
明らかに GPU 性能が足りずに速度が落ちているようです。

K3V2 は Vivante GC4000 による描画が崩れて正しく表示されませんでした。
ログを見る限りアプリケーション自体は動いているようです。

● Emscripten のビルド

Emscripten のビルドは gcc/clang の代わりに emcc コマンドを用います。
obj (.o) の代わりが .bc (bitcode) です。

emcc file1.cpp -o file1.bc

実際に実行できるよう JavaScript に変換 (ld/link) するには下記のようにします。

emcc file1.bc -o file1.js

または

emcc file1.bc -o file1.html

実行形式は js だけの場合と、html 含めたブラウザ向けの 2種類あります。

ローカルで実行する場合は node コマンド (Node.js) を使うことが可能で、
この場合は js だけで OK です。
WebGL の描画や preload 指定した追加ファイルを読み込む場合は html で出力し、
Web サーバー経由でブラウザから読み込みます。

実行形式   実行環境   バンドル
-----------------------------------------------------
 .js       node       --embed-file
 .html     Browser    --emded-file or --preload-file

● 実行手順の例

ローカルで実行する場合 (node)

emcc file1.cpp -o file1.bc
emcc file2.cpp -o file2.bc
emcc file1.bc file2.bc -o file.js

node file.js

ローカルサーバーを経由する場合 (Browser)

emcc file1.cpp -o file1.bc
emcc file2.cpp -o file2.bc
emcc file1.bc file2.bc -o file.html

python -m SimpleHTTPServer 8888

ブラウザで http://localhost:8888/file.html を開く

● ファイルシステム

Emscripten の実行環境は仮想 FileSystem を持っており、
内部的に Unix FileSystem をエミュレートします。
実行時に読み込むファイル群は、一旦この仮想 FileSystem 内に読み込んでおく必要があります。

必要なファイルを実行ファイルに埋め込む方法は下記 2種類あります。

--preload-file    外部ファイル(*.data)にアーカイブし起動時に読み込む
--embed-file      js に埋め込む

–embed-file の例

emcc file1.cpp -o file1.bc
emcc file2.cpp -o file2.bc
emcc file1.bc file2.bc -o file.js --embed-file root/shaders

↑この場合 root/shaders フォルダ以下のファイルをすべて js の中に埋め込みます。
仮想 FileSystem 内のパスも /root/shaders/* とみなします。

もし実際のデータ置き場と、仮想 FileSystem 内のパスが異なる場合は
‘@’ マークを使います。

emcc file1.cpp -o file1.bc
emcc file2.cpp -o file2.bc
emcc file1.bc file2.bc -o file.js --embed-file root/shaders@/data/shaders

↑ ‘@’ 以降が内部の仮想パスの指定になります。
実際のデータは root/shaders 以下に存在しており、
実行時は /data/shaders にあるとみなされます。

–embed-file は間違いが少なく node でも利用できて便利なのですが、
ファイル数が増えるとビルドが極端に遅くなる問題があります。
バンドルが 200MB くらいあるプロジェクトではコマンドが返ってきませんでした。

–preload-file の場合はデータを外部ファイルにまとめるため、
データ容量が増えても比較的時間がかからずにビルドできます。

emcc file1.cpp -o file1.bc
emcc file2.cpp -o file2.bc
emcc file1.bc file2.bc -o file.html --embed-file root/shaders@/data/shaders

python -m SimpleHTTPServer 8888

ブラウザで http://localhost:8888/file.html を開く

どちらにせよ、これらの方法はブラウザを開いたあと長いロードが入るので
その間何もできなくなります。
現実的には、動的な逐次読み込みを実現する必要があるでしょう。

またメモリ上の仮想 FileSystem はデータの保存ができません。
必要ならば Indexed Databased を FileSystem に mount して
利用することができるようです。

● Inline JavaScript

仮想マシンが JavaScript となる Emscripten では、Inline Assembler ではなく
Inline JavaScript を利用することができます。

#include 
~

int main()
{
    EM_ASM(
       ~
       JavaScript code
    );

    return  0;
}

EM_ASM() の中に JavaScript code を記述します。
仮想ファイルシステムへの動的な読み込みなど色々コードを埋め込むことができるようです。

● Main Loop

Emscripten では callback を使って Game の Main Loop の代わりにします。
例えば AppModule::Render() を毎フレーム呼び出したい場合は下記のような使い方になります。

#include 

class AppModule {
public:
   void Render();
};

// 毎フレーム呼ばれる
static void ems_loop( void* arg )
{
   AppModule*  iApp= reinterpret_cast( arg );
   iApp->Render();
}

int main()
{
    AppModule* iApp= new AppModule();

    // callback の登録
    emscripten_set_main_loop_arg( ems_loop, iApp, 0, TRUE );

    delete iApp;
    return  0;
}

● HeapSize

実行環境のメモリが足りずに、アプリケーションを起動できないことがあります。
下記のようにアプリケーションが使用する HEAP SIZE を指定することができます。

emcc  file.cpp -o file.bc
emcc  file.bc -o file.html  -s TOTAL_MEMORY=134217728

あまり大きな値を指定し過ぎると、今度はブラウザ側のメモリが足りず
Out of Memory でロードが止まってしまうことがあるようです。

● まとめ

・Android Cortex-A15/A9 + Firefox は遅くなかった
・Emscripten のビルドと実行手順
・仮想ファイルシステムとバンドル
・MainLoop と HeapSize

次回 : Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧