CMakeでemscriptenアプリを作る

前回のXboxOneに引き続き、今度は同じコードをEmscriptenでビルドしてみた。EmscriptenC/C++コードをJavaScriptやasm.jsにコンパイルするコンパイラで、最近はIntel SSE intrinsics => SIMD.jsの変換をサポートする( https://kripken.github.io/emscripten-site/docs/porting/simd.html )などかなり高機能になってきている。
前回同様プラットフォーム移植層にはSDL2を採用し、アプリケーションも単なるSDL2アプリケーションということになる。

emconfigureでのプロジェクト生成

CMakeプロジェクトをEmscripten向けにビルドするために必要なのは大きく2つで、

  1. Emscripten SDKをセットアップする
  2. Emscripten command prompt上でemconfigure cmakeする

Emscripten SDKは公式サイトからダウンロード( https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html )できる。
ビルドそのものには特に難しいポイントはなく、Emscripten command prompt上で単に

emconfigure cmake -G Ninja c:\path\to\here
emconfigure cmake -G Ninja c:\path\to\here

2度emconfigureを実行すれば良い。どうも正しくtoolchain fileが渡らないことが有るらしく、手元の環境では2度やらないと正しくconfigureさせることができなかった。
emconfigureコマンドはconfigureスクリプトのためのwrapperで、適当な環境変数等を設定した状態でスクリプトを実行してくれる。また、emconfigureにはCMakeの特別対応が入っており、conifugureスクリプトとしてcmakeを指定すると、emconfigureは自動的にPlatformを指定する。
ここでは明示的にNinjaをgeneratorとして指定している。ninjaは別途インストールする必要があるが、makeよりは圧倒的に高速なので好都合と言える。開発には基本的にVisualStudioを使いたいが、まだVisualStudioでのビルドやデバッグには成功していない。

SDL_config.hの捏造

実はSDL2自体もCMakeのプロジェクトファイルが提供されているため、単にemconfigureに掛けるだけでビルドが行える。ただ、SDL2のCMakeLists.txtは構成管理上の不都合が多い(ファイルをGLOBで指定している、subproject化を配慮していない等)ので、今回はSDL2のビルドも自前のCMakeLists.txt内で行っている。
SDL2では、GNU autoconfやCMakeでのビルドを想定しているターゲットではSDL_config.hが提供されない。このため、自前のSDL_config.hを用意する必要があった。

このSDL_config.hはemconfigureに掛けたものをコピペしている。どっちにせよサポートしているlibc関数が増減することはほぼ無いので。。(将来pthreadのサポートは入るかもしれないけど https://kripken.github.io/emscripten-site/docs/porting/pthreads.html 現状はFirefox/Chromeのみでデフォルト無効)

HTMLの出力

Emscriptenコンパイラドライバであるemccは出力拡張子として.htmlを設定すると、生成したJavaScriptをロードして実行するためのHTMLも一緒に出力してくれる。この機能を使用するには、CMake側からSUFFIXプロパティを設定すれば良い。

    add_library(grpcore0 STATIC
        ${sdl2srcs}
        mysdlmain.c)

    add_executable(grpcore0_html
        pkg/emscripten/emmain.c)
    target_link_libraries(grpcore0_html
        grpcore0)
    set_target_properties(grpcore0_html
        PROPERTIES
        SUFFIX ".html")

これを忘れると、出力されるexecutableは単なる.jsになり、ブラウザ上での確認は別途HTMLが必要となる。

Main loopの明示的な指定

Emscripten上のアプリケーションは、描画ループをシステム(ブラウザ)側に呼ばせる必要がある。ブラウザ上のアプリケーションではシステムがCPUを占拠するわけにはいかないので、ブラウザが明示的にfps limitを掛ける。
Main loopの設定はEmscriptenのランタイム関数 emscripten_set_main_loop で行える。

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

/* snip */

#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop_arg(loop, renderer, 0, 1);
#else
    for(;;){
        loop(renderer);
    }
#endif

他に同じようなアーキテクチャを採用しているシステムとしてはiOS(やOSX)が有るが、SDL2上のiOSはさらに別の考察を採用していて、今のところSDL2はこの部分の抽象化を提供していない。
ちなみに、Androidを含む他のプラットフォームは、vsyncを取るなどの方法で自分でthrottleする必要がある。

Canvasサイズの設定

Emscriptenの描画エリアであるCanvasEmscriptenAPIで設定できる。

#ifdef __EMSCRIPTEN__
    emscripten_set_canvas_size(1280, 720);
#endif

これはあまり正確な方法でなく、かつ、自動追従するのが望ましい。ただ、ゲーム実行中の描画領域変更はそれなりの考察が必要なのでまだちゃんと考えていない。