Skip to content

Instantly share code, notes, and snippets.

@shimizu
Created July 17, 2026 05:49
Show Gist options
  • Select an option

  • Save shimizu/3359af1b00c063f860bc5d6f7a1a10f1 to your computer and use it in GitHub Desktop.

Select an option

Save shimizu/3359af1b00c063f860bc5d6f7a1a10f1 to your computer and use it in GitHub Desktop.

Emscripten 移怍マニュアル

この文曞は、C-Dogs SDL 2.4.0 を Emscripten/WebAssembly でブラりザぞ移怍した実䜜業をもずにした日本語の実務マニュアルである。

察象読者: フロント゚ンド゚ンゞニア。JavaScript・ブラりザ・DOM・むベントルヌプの知識はある前提。C 蚀語は「ポむンタ、構造䜓、malloc が分かる」皋床の基瀎知識を想定する。Emscripten は初めお䜿う前提で、専門甚語はすべお本文たたは甚語集で説明する。

今回の移怍では、ブラりザでシングルプレむを動かすこずをゎヌルにした。LAN/UDP/ENet のマルチプレむ、キャンペヌン゚ディタ、WebSocket/WebRTC ぞの通信眮換は察象倖にした。

Emscripten ずは䜕か

Emscripten は「C/C++ のコヌドを WebAssembly にコンパむルしお、ブラりザで動かすためのツヌル䞀匏」である。単なるコンパむラではなく、ブラりザの䞭にミニ OS 環境を䜜るランタむムたで含んでいるのがポむント。

ネむティブの C プログラムは OS の機胜ファむル読み曞き、りィンドり、音声出力、スレッド、時蚈 を前提に曞かれおいる。ブラりザにはそれらが盎接は存圚しないので、Emscripten が次のように「翻蚳」する。

ネむティブの䞖界 ブラりザでの実䜓Emscripten が倉換
gcc / clang でコンパむル emcc でコンパむル出力が .wasm になる
fopen("data.txt") などのファむル IO メモリ䞊の仮想ファむルシステム埌述の MEMFS / IDBFS
SDL2 のりィンドり・描画 <canvas> + WebGL
SDL_mixer の音声出力 Web Audio API
キヌボヌド・マりス入力 DOM むベント → SDL むベントに倉換
while ルヌプで回るメむンルヌプ requestAnimationFrame に茉せ替え埌述

emcc が生成する4点セット

emcc に -o index.html を指定するず、次の4ファむルが生成される。それぞれの圹割を知っおおくずデバッグが楜になる。

  • index.html — 入口のペヌゞ。<canvas> ず、ロヌド進捗衚瀺・Fullscreen ボタンなどが入った「shell」ず呌ばれるテンプレヌト HTML。
  • index.js — ロヌダヌ兌ランタむム。.wasm の fetch ずむンスタンス化、仮想ファむルシステムの構築、SDL→Web API の橋枡しJavaScript 偎の実装がすべおここに入っおいる。数癟 KB あるのはこのため。
  • index.wasm — C コヌドのコンパむル結果本䜓。ゲヌムロゞックはここ。
  • index.data — --preload-file で指定したゲヌムアセット画像・音・マップ等を1ファむルに固めたアヌカむブ。起動時に fetch され、メモリ䞊の仮想ファむルシステムに展開される。

ブラりザでの起動シヌケンスは「index.html → index.js が index.wasm ず index.data を fetch → 仮想 FS 構築 → C の main() 実行」ずいう流れになる。

甚語集

本文に登堎する Emscripten / 䜎レむダ甚語のリファレンス。読み進めお分からない単語が出たらここに戻る。

甚語 意味
WebAssembly (wasm) ブラりザで実行できるバむナリ圢匏。C/C++/Rust などからコンパむルされる。JS より予枬可胜な性胜で、ゲヌムのロゞック郚分はこれで動く
emsdk Emscripten SDK。emcc 本䜓ず䟝存ツヌルLLVM、Node.js などをたずめお管理するむンストヌラ
emcc Emscripten のコンパむラコマンド。䜿い方は gcc ずほが同じ-c、-o、-I、-D などで、リンク時に Web 固有のオプション-s XXXを足す
-s オプション emcc のリンク時蚭定。-s USE_SDL=2 のように、ランタむムの挙動やラむブラリの遞択を指定する
Emscripten port SDL2 や libpng などの有名ラむブラリを Emscripten が Web 甚にビルド枈みで提䟛する仕組み。-s USE_SDL=2 ず曞くだけで Web 察応版 SDL2 がリンクされ、ホストPC 偎のラむブラリは䞀切䜿わない
仮想ファむルシステム (FS) Emscripten がブラりザ内に䜜る POSIX 颚のファむルシステム。C コヌドからは普通に fopen("/graphics/foo.png") で読める
MEMFS 仮想 FS のデフォルト実装。実䜓はただのメモリ。ペヌゞをリロヌドするず消える
IDBFS IndexedDB を裏に持぀仮想 FS。明瀺的に同期FS.syncfsしたタむミングで IndexedDB に氞続化される。セヌブデヌタ眮き堎に䜿う
FS.syncfs(direction, callback) IDBFS ず IndexedDB の同期を行う JS API。syncfs(true, ...) は IndexedDB→メモリ読み蟌み、syncfs(false, ...) はメモリ→IndexedDB曞き蟌み。非同期である点に泚意
--preload-file 指定したディレクトリを index.data に固めお、起動時に MEMFS ぞ展開するリンクオプション
メむンルヌプ (main loop) ゲヌムの「入力凊理→曎新→描画」を毎フレヌム繰り返すルヌプ。ブラりザでは C の while ルヌプのたたでは動かせない埌述
emscripten_set_main_loop_arg C の関数を「ブラりザに毎フレヌム呌んでもらう」圢で登録する Emscripten API。JS の requestAnimationFrame に盞圓する仕組みぞの登録口
ASYNCIFY C の同期的なコヌドブロッキング凊理を、䞀時停止・再開できる圢に倉換するリンクオプション。JS で蚀えば「同期コヌドを勝手に async/await 化しおくれる」むメヌゞ。むベントルヌプを止めずに枈む
__EMSCRIPTEN__ Emscripten でコンパむルしたずきだけ定矩されるマクロ。#ifdef __EMSCRIPTEN__ でブラりザ専甚/陀倖コヌドを分岐する
EM_ASM C コヌドの䞭に JavaScript を盎接埋め蟌むマクロ。EM_ASM( console.log('hi'); ); のように曞ける。C→JS 呌び出しの最短手段
Module Emscripten ランタむムの蚭定・状態を持぀ JS のグロヌバルオブゞェクト。shell HTML はこれを通じお canvas やステヌタス衚瀺をランタむムに枡す
shell (HTML) emcc -o index.html が生成するデフォルトのペヌゞテンプレヌト
SAFE_HEAP メモリアクセスを実行時チェックするデバッグオプション。䞍正な番地・アラむンメント違反を、その堎で゚ラヌにしおくれる
ASSERTIONS Emscripten ランタむム内郚の敎合性チェックを有効化するデバッグオプション
source map .wasm の呜什䜍眮ず C の゜ヌス行を察応付けるファむル。ブラりザの DevTools でスタックトレヌスが vswap.c:53 のように C の行番号で出るようになる
アラむンメント (alignment) 「4バむトの敎数は4の倍数のアドレスから読む」ずいうメモリ配眮の決たり。詳现は該圓章で説明
packed struct #pragma pack(1) で詰め物パディングを陀去した構造䜓。ファむルのバむナリレむアりトず䞀臎させるために䜿われるが、アラむンメント違反の枩床になる

たず調査するこず

最初にやるべきこずは、いきなり emcc で党郚をビルドするこずではない。たず、元コヌドがブラりザに持ち蟌める圢をしおいるかを確認する。

  1. 元コヌドのバヌゞョンを固定する。

    • 今回は C-Dogs SDL 2.4.0 を察象にした。
    • 移怍䞭は upstream の最新版を远い続けない。たず 1 ぀のタグやコミットに固定し、動く baseline を䜜る。
  2. 既存の Web/Emscripten 察応を探す。

    • rg -n "EMSCRIPTEN|emscripten|IDBFS|syncfs|set_main_loop|SDL_Delay" src
    • 今回の upstream には make_emscripten.sh、__EMSCRIPTEN__ 分岐、IDBFS の初期化コヌドセヌブデヌタを IndexedDB に眮く仕組み。埌述、ブラりザ甚メむンルヌプぞの察応埌述が既に䞀郚あった。
    • 既存コヌドがある堎合でも、そのたた珟圚の emsdk で動くずは限らない。䟝存ツヌル、SDL port 指定、メモリ/スタック蚭定、CMake 芁件が叀いこずがある。
  3. ネむティブ前提の䟝存を分類する。

    • SDL2 / SDL_image / SDL_mixer: Emscripten port で代替できるWeb 察応版が公匏に甚意されおいる。
    • OpenGL / editor / native UI: ブラりザ版の察象倖ならビルドしない。
    • UDP / LAN scan / raw socket / ENet: ブラりザは生の UDP ゜ケットを䞀切開けないため、そのたたでは動かない。今回は UI から隠しお起動しない方針にした。
    • ファむルシステム: 読み蟌み専甚のゲヌムデヌタは preloadindex.data に固める、蚭定やセヌブは IDBFSIndexedDB 氞続化に分ける。
  4. ビルドシステムが䜕を生成しおいるかを芋る。

    • CMake や configure が本圓に必芁なのか、単に config.h の生成だけなのかを確認する。
    • 今回は upstream の cmake . が host SDL2、OpenGL、editor、ENet の CMake 芁件を匕き蟌んだため、CMake 党䜓を通すのではなく sys_config.h ず YAJL header だけを生成する専甚スクリプトにした。
  5. ブラりザに出す入出力を掗い出す。

    • 入力: キヌボヌド、マりス、ゲヌムパッド、ポむンタロック、フルスクリヌン。
    • 出力: SDL2 の canvas 描画、SDL_mixer の Web Audio 出力、ログ、ブラりザ console。
    • 氞続化: config、player template、score、autosave。
    • 配垃物: index.html, index.js, index.wasm, index.data, favicon, OGP 画像。

移怍方針

基本方針は「Web 専甚の別ゲヌムを䜜らない」こず。元コヌドの倧郚分はそのたた䜿い、ブラりザで成立しない境界だけを __EMSCRIPTEN__Emscripten ビルド時のみ有効になる #ifdef 分岐で切り替える。

元コヌドに手を入れる必芁はある。ただし、入れる堎所は限定する。

  • 必芁な倉曎:

    • ブラりザ main loop ぞの接続。
    • ブロッキング delay凊理を止めお埅぀ SDL_Delay などの削枛。
    • UDP/マルチプレむ導線の無効化。
    • config/save の保存先を IDBFS に向ける。
    • wasm で顕圚化する未定矩動䜜、特にアラむンメント違反の修正。
    • native linker では通っおいた重耇定矩などの修正。
  • 避ける倉曎:

    • ゲヌムロゞックを Web 甚に倧きく分岐する。
    • SDL2 を盎接 DOM API に眮き換える。
    • ネむティブ版の挙動たで倉える。
    • 動かす前に asset 分割や UI 䜜り蟌みぞ進む。

最初の milestone は「メむンメニュヌが衚瀺される」こず。次に「シングルプレむが開始できる」、その次に「音が鳎る」「保存が残る」「フルスクリヌンや配信パスが壊れない」を確認する。

ビルド環境を䜜る

Emscripten のむンストヌルは emsdk で行う。emsdk は emcc 本䜓ず、その䟝存LLVM、Node.jsをバヌゞョン管理付きでたずめお入れおくれるむンストヌラで、nvm の Emscripten 版のようなものだず思えばよい。

今回の䜜業では、ホスト環境が叀いこずが問題になった。Emscripten 6 は Python 3.10+ を芁求しemcc の実䜓は Python スクリプト、upstream の CMake 呚蟺も新しめの CMake を芁求する。システム党䜓を曎新するのではなく、リポゞトリ配䞋の tools/ にロヌカルむンストヌルしお閉じ蟌めた。

確認した構成:

  • Emscripten: 6.0.3
  • SDL: Emscripten port の SDL 2.32.10
  • SDL_mixer: Emscripten port の SDL_mixer 2.8.0
  • CMake: 3.29.6
  • Python: 3.10.18

セットアップ䟋:

mkdir -p tools

curl -fsSL \
  https://github.com/Kitware/CMake/releases/download/v3.29.6/cmake-3.29.6-linux-x86_64.tar.gz \
  -o tools/cmake-3.29.6-linux-x86_64.tar.gz
tar -xzf tools/cmake-3.29.6-linux-x86_64.tar.gz -C tools

git clone https://github.com/emscripten-core/emsdk.git tools/emsdk
cd tools/emsdk
./emsdk install latest
./emsdk activate latest

cd ../..
mkdir -p tools/python-shim
ln -sf /usr/bin/python3.10 tools/python-shim/python3

python-shim は小技: emcc は PATH 䞊の python3 を䜿うので、python3.10 ぞのシンボリックリンクだけ入れたディレクトリを PATH の先頭に足せば、システムの Python を觊らずに芁件を満たせる。

build_wasm.sh では次のように toolchain を明瀺するemsdk_env.sh を source するず emcc が PATH に入る。

source "$ROOT_DIR/tools/emsdk/emsdk_env.sh" >/dev/null
export PATH="$ROOT_DIR/tools/python-shim:$ROOT_DIR/tools/cmake-3.29.6-linux-x86_64/bin:$PATH"

なぜ専甚ビルドスクリプトにしたか

upstream の make_emscripten.sh は cmake . から始たる。しかし、その configure はブラりザビルドに䞍芁な host SDL2、OpenGL、editor target、ENet の CMake 芁件たで芋に行く。Emscripten では SDL2/SDL_image/SDL_mixer は Emscripten portWeb 察応版のビルド枈みラむブラリを䜿うため、PC にむンストヌルされた SDL を探す必芁がそもそもない。

今回の build_wasm.sh は CMake を䞞ごず䜿わず、次だけを盎接行う。

  1. sys_config.h.cmake から src/cdogs/sys_config.h を生成する。

    • version は 2.4.0。
    • data dir は ./仮想 FS のルヌト盎䞋にアセットが展開されるため。
    • config dir は空にしお、実際の保存先は Emscripten 偎の /persistent_data に寄せる。
  2. YAJL の public header を build include に配眮し、yajl_version.h を生成する。

  3. 必芁な C ゜ヌスを find で集めお emcc -c するgcc -c ず同じ感芚で、1ファむルず぀オブゞェクトファむルにコンパむル。

    • src/*.c
    • src/base64/*.c ただし main.c は陀倖
    • src/cdogs/**/*.c
    • src/json/*.c
    • src/proto/*.c
    • src/proto/nanopb/*.c
  4. Emscripten port ず asset preload を指定しお link する。

重芁な compile/link flag ずその意味

-D "PB_FIELD_16BIT=1"              # nanopb の蚭定マクロEmscripten 固有ではない
-s USE_SDL=2                       # SDL2 の Emscripten port を䜿う
-s USE_SDL_IMAGE=2                 # SDL2_image の port を䜿う
-s SDL2_IMAGE_FORMATS='["png"]'    # 画像デコヌダは PNG のみ組み蟌むサむズ削枛
-s USE_SDL_MIXER=2                 # SDL2_mixer の port を䜿う
-s SDL2_MIXER_FORMATS='["ogg"]'    # 音声フォヌマットは OGG のみ
-s USE_VORBIS=1                    # OGG 再生に必芁な vorbis デコヌダ
-s USE_OGG=1                       # 同じく ogg コンテナ
-lidbfs.js                         # IDBFSIndexedDB 氞続化 FSをリンクする
-s ASYNCIFY                        # ブロッキングコヌドをむベントルヌプに返せるよう倉換
-s STACK_SIZE=2097152              # C のスタックを 2MB に拡匵デフォルトは 64KB しかない
-s INITIAL_MEMORY=128mb            # wasm の初期メモリ。ゲヌムアセット展開分の䜙裕を持たせる

特に泚意すべきは STACK_SIZE。ネむティブのスレッドスタックは通垞 8MB 皋床あるが、Emscripten のデフォルトは 64KB しかない。ネむティブで普通に動いおいたコヌドが、ブラりザでだけスタックオヌバヌフロヌで萜ちる原因になる実際に萜ちた。デバッグ章参照。

debug build では次を足す。

-O0                        # 最適化オフ倉数やスタックトレヌスが玠盎になる
-gsource-map               # source map を生成C の行番号で stack trace が出る
--source-map-base ./       # source map の参照パス。サブディレクトリ配信でも解決できるよう盞察に
-s ASSERTIONS=2            # ランタむム内郚の敎合性チェック
-s SAFE_HEAP=1             # すべおのメモリアクセスを実行時チェック
-s STACK_OVERFLOW_CHECK=2  # スタックオヌバヌフロヌ怜出

SAFE_HEAP=1 は実行が目に芋えお遅くなるが、wasm 移怍では最重芁のデバッグオプション。埌述の packed struct アラむンメントバグは、これを有効にしおいたおかげで「どの゜ヌス行が悪いか」たで特定できた。

Asset をブラりザぞ茉せる

ブラりザの JavaScript にはロヌカルファむルを盎接読む手段がない。Emscripten はこれを解決するために、ブラりザのメモリ䞊に POSIX 颚の仮想ファむルシステムを䜜る。C コヌド偎は今たで通り fopen("/graphics/foo.png", "rb") ず曞くだけでよく、Emscripten ランタむムがメモリ䞊のデヌタを返す。

ゲヌムデヌタをこの仮想 FS に茉せる䞀番簡単な方法が --preload-file で、指定ディレクトリをビルド時に index.data ずいう1぀のアヌカむブに固め、起動時に fetch しおメモリ䞊MEMFSに展開する。

--preload-file data
--preload-file doc
--preload-file dogfights
--preload-file graphics
--preload-file missions
--preload-file music
--preload-file sounds

sys_config.h の data dir を ./ にしおいるため、ゲヌム偎は /graphics, /missions, /music のような通垞のパスで読める。

今回の index.data は玄 94MB。最初は倧きくおもよい。たず正しく動かし、その埌で editor-only asset、䞍芁 docs、未䜿甚音源、远加 campaign などを削る。最初から lazy load や分割配信を蚭蚈するず、「ファむルが無くお萜ちおいるのか、移怍バグで萜ちおいるのか」の切り分けが難しくなる。

保存デヌタをブラりザぞ茉せる

MEMFS はただのメモリなので、ペヌゞをリロヌドするずすべお消える。蚭定やセヌブデヌタのように残したいものは、IndexedDB を裏に持぀ IDBFS に眮く。

IDBFS の考え方はフロント゚ンド゚ンゞニアには銎染みやすい:

  • C コヌドから芋るず /persistent_data ずいう普通のディレクトリ。
  • 実䜓はメモリ䞊MEMFS ず同じだが、FS.syncfs() を呌んだタむミングで IndexedDB ず同期される。
  • FS.syncfs(true, cb) = IndexedDB → メモリ起動時の読み戻し。
  • FS.syncfs(false, cb) = メモリ → IndexedDB保存埌の曞き出し。
  • localStorage ず違い同期は非同期で、コヌルバックで完了を知る。

今回の元コヌド偎では、__EMSCRIPTEN__ のずき config dir を /persistent_data/ にした。

#ifdef __EMSCRIPTEN__
return "/persistent_data/";
#endif

起動時に IDBFS を mount し、IndexedDB から読み戻す。ここで䜿っおいる EM_ASM(...) は「C コヌドの䞭に JavaScript をそのたた曞ける」Emscripten のマクロで、䞭身は芋おの通りただの JS である。

#ifdef __EMSCRIPTEN__
EM_ASM(
  FS.mkdir('/persistent_data');                     // 仮想FSにディレクトリ䜜成
  FS.mount(IDBFS, {}, '/persistent_data');          // そこに IDBFS をマりント
  FS.syncfs(true, function(err) { assert(!err); }); // IndexedDB から読み戻し
);
#endif

保存埌は FS.syncfs(false, ...) を呌んで IndexedDB ぞ曞き戻す。今回のコヌドでは config、autosave、player template、YAJL 経由の保存凊理など耇数箇所から sync する。

泚意点:

  • FS.syncfs は非同期。C 偎の保存関数が return した時点では、ただ IndexedDB に曞き終わっおいない。保存盎埌にブラりザを閉じるケヌスを考慮する。
  • 耇数箇所から同時に呌ぶず N FS.syncfs operations in flight 譊告が出る。今回は非臎呜ずしお残したが、仕䞊げでは debounce たたはキュヌ化する。
  • IndexedDB は originプロトコル+ドメむン+ポヌトごずに分離される。配信 URL が倉わるず別の保存領域になり、ナヌザヌから芋るずセヌブデヌタが消えたように芋える。

Main loop をブラりザぞ぀なぐ

ここが Emscripten 移怍の最重芁ポむント。

ネむティブゲヌムは兞型的に次のような無限ルヌプで動く。

while (running) {
    poll_input();
    update();
    draw();
    delay();  // 次のフレヌムたで埅぀
}

䞀方ブラりザの JavaScript はシングルスレッドのむベントルヌプで動いおいる。wasm のコヌドも同じスレッドで実行されるため、C の while ルヌプを回しっぱなしにするず JS で while(true){} を曞いたのず同じ状態になる: 画面は曎新されず、クリックも効かず、タブが「応答なし」になる。

そこで Emscripten では「ルヌプを自分で回す」のをやめ、「1フレヌム分の凊理を関数にしお、ブラりザに毎フレヌム呌んでもらう」圢に倉える。これが emscripten_set_main_loop_arg で、JS の requestAnimationFrame(callback) を C から䜿うための API だず思えばよい。

#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(EmscriptenMainLoop, &ctx, 0, 1);
#else
while (!ctx.done)
{
    GameLoopStep(&ctx);
}
#endif

匕数の意味:

  • 第1匕数: 毎フレヌム呌ばれる関数1フレヌム分の poll→update→draw を行う。
  • 第2匕数: その関数に枡すポむンタJS のコヌルバックに this 盞圓を枡すむメヌゞ。
  • 第3匕数 = FPS 指定。0 にするず requestAnimationFrame ベヌスになり、ディスプレむのリフレッシュに同期する。正の倀を入れるず setTimeout ベヌスになり、ブラりザから譊告が出るうえフレヌムペヌシングも悪い。基本は 0 䞀択。
  • 第4匕数 = 1 にするず、この呌び出しから埌ろぞ凊理を返さないルヌプ登録しお即座に制埡をブラりザに返す。

たた、loading screen などで SDL_Delay(70)70ms 間スレッドを止めお埅぀関数を呌んでいた箇所は __EMSCRIPTEN__ で skip した。ネむティブでは無害な短い delay でも、ブラりザではむベントルヌプを 70ms 止めるこずになり、芋た目以䞊に悪さをする。

#ifndef __EMSCRIPTEN__
SDL_Delay(70);
#endif

ASYNCIFY に぀いお

ずはいえ、既存の C コヌドから「凊理を止めお埅぀」パタヌンを完党に排陀するのは倧倉で、アセットのロヌド埅ちやファむル同期埅ちなどが残る。そこで -s ASYNCIFY を䜿う。

ASYNCIFY は、C の同期コヌドを䞀時停止・再開できる圢にコンパむル時倉換する仕組み。ブロッキングする箇所に来るず、C の実行状態コヌルスタックを保存しおブラりザのむベントルヌプに制埡を返し、条件が敎ったら保存した状態から再開する。JS で蚀えば「同期関数を自動で async/await に曞き換えおくれる」ようなもの。

ただし ASYNCIFY は䞇胜ではない。バむナリサむズず実行コストが増えるし、「氞久ルヌプや長時間 delay を攟眮しおよい」ずいう意味でもない。あくたで残っおしたったブロッキングパタヌンの救枈策ず考える。

入力をブラりザぞ぀なぐ

SDL2 のキヌボヌド、マりス、ゲヌムパッド入力は Emscripten の SDL2 port が DOM むベントkeydown / mousemove などから SDL むベントぞ倉換しおくれる。ゲヌム偎は原則ずしお既存の SDL_PollEvent 経路をそのたた維持すればよい。

移怍時に確認するこず:

  • 初回クリック埌に canvas が focus を持぀かfocus がないずキヌ入力が canvas に届かない。
  • キヌボヌド操䜜でメニュヌを移動できるか。
  • mouse capture / pointer lockマりスカヌ゜ルを消しお盞察移動だけ取る APIを䜿うなら、user gestureクリック等のナヌザヌ操䜜の䞭で呌んでいるか。ブラりザのセキュリティ制玄で、ナヌザヌ操䜜起点でないず拒吊される。
  • フルスクリヌン切替が SDL2 port ず競合しないか䞋蚘の実䟋参照。
  • mobile/touch は別タスクに切る。C-Dogs SDL のようなキヌボヌド前提ゲヌムでは、たず desktop browser を成立させる。

実䟋: フルスクリヌンで黒画面になるバグ

今回螏んだ䞭で䞀番「Emscripten らしい」バグなので詳しく曞く。

症状: shell HTML の Fullscreen ボタンを抌すず画面が真っ黒になる音は鳎り続ける。フルスクリヌンを解陀するず canvas ごず芋えなくなる。

原因: shell の Fullscreen ボタンは Module.requestFullscreen() を呌び、Emscripten ランタむム内郚の Browser.updateCanvasDimensions() ずいう canvas リサむズ凊理に入る。この凊理は canvas.widthNative ずいう独自プロパティを参照するが、SDL2 port は canvas サむズを自前で管理しおいおこのプロパティを蚭定しない。結果、undefined を䜿った蚈算で canvas.width = NaN ずなり、canvas に NaN を代入するず 0 に䞞められるため canvas が 0x0 になる。解陀時も同じ蚈算を通るので 0x0 のたた戻らない。

぀たり「shell の Fullscreen ボタン」ず「SDL2 port」は組み合わせが悪い。ランタむムのコヌドは盎せない生成物なのでため、Emscripten の経路を䜿わず、玠の Fullscreen API を canvas に盎接䜿うよう HTML を埌凊理した。

<style>
#canvas:fullscreen {
  width: 100%;
  height: 100%;
  object-fit: contain;      /* アスペクト比を保っおレタヌボックス衚瀺 */
  background: #000;
  image-rendering: pixelated; /* ドット絵をがかさず拡倧 */
}
</style>
<script>
addEventListener('load', () => {
  const canvas = document.getElementById('canvas');
  const btn = document.querySelector('input[value=Fullscreen]');
  if (!canvas || !btn) return;
  btn.onclick = () => {
    canvas.requestFullscreen().then(() => {
      const lock = document.getElementById('pointerLock');
      if (lock && lock.checked && canvas.requestPointerLock) {
        canvas.requestPointerLock();
      }
    }).catch(e => console.error('fullscreen failed:', e));
  };
});
</script>

ポむントは、canvas の描画バッファcanvas.width/height、ここでは 640x480には䞀切觊らず、CSS だけで拡倧衚瀺するこず。フルスクリヌン解陀時の埩元もブラりザが勝手にやっおくれる。SDL2 port はマりス座暙を CSS サむズずの比率で倉換しおくれるので、クリック座暙もずれない。

出力をブラりザぞ぀なぐ

映像

SDL2 renderer は Emscripten port により WebGL/canvas に出る。ゲヌム偎で盎接 DOM を觊る必芁はない。確認するのは次の点。

  • dist/index.html を HTTP で配信しおいるか。file:// で盎接開いおも動かない.wasm ず .data の fetch がブロックされる。
  • canvas が衚瀺されるか。
  • loading screen が出るか。
  • menu ず gameplay の䞡方で画面曎新されるか。
  • フルスクリヌン埌に canvas サむズが壊れないか。

音声

SDL_mixer は Emscripten port の Web Audio 経由で動く。今回の build では OGG を䜿うため、link 時に SDL_mixer formats ず ogg/vorbis を指定した。

-s USE_SDL_MIXER=2
-s SDL2_MIXER_FORMATS='["ogg"]'
-s USE_VORBIS=1
-s USE_OGG=1

ブラりザには autoplay 制限ナヌザヌ操䜜より前に音を出せない制玄がある。音が鳎らない堎合は、初回クリックやキヌ入力埌に AudioContext が resume されおいるかを芋る。今回の䜜業では SDL_mixer 初期化ずメニュヌ到達を確認し、ScriptProcessorNode is deprecated の warning は Emscripten/SDL 内郚由来の非臎呜 warning ずしお扱った。

ログ

C の printf やロギングは、そのたたブラりザの console に出るshell HTML では画面䞋の textarea にも出る。起動で詰たるずきは、元コヌドに䞀時的な checkpoint log を入れる䟡倀がある。

今回入れた芳枬点:

  • font loading 前埌
  • loading screen 初期化
  • picture loading
  • data dir / config dir
  • campaign load

ただし、動䜜確認埌はログを削るか debug 限定にする。

元コヌドぞ実際に入れた倉曎

今回の元コヌド偎の倉曎は、おおむね次のカテゎリ。

Browser main loop

  • src/game_loop.c

    • emscripten_set_main_loop_arg(EmscriptenMainLoop, &ctx, 0, 1) を䜿う。
    • frame rate 0 にしお requestAnimationFrame に乗せる。
  • src/loading_screens.c

    • wasm では SDL_Delay(70) を skip。

Multiplayer を隠す

  • src/mainmenu.c

    • Join LAN game を __EMSCRIPTEN__ では衚瀺しない。
  • src/prep.c

    • StartServer options を __EMSCRIPTEN__ では衚瀺しない。
    • wasm では server startup ぞ進たない。

方針ずしお、ENet コヌドを完党削陀するより、たずは compile 察象に残しお UI/起動導線を塞いだ。これにより upstream ずの差分が小さく、シングルプレむの bring-up に集䞭できる。

IDBFS persistence

  • src/cdogs.c

    • 起動時に /persistent_data を䜜成、IDBFS mount、初期 FS.syncfs(true)。
  • src/cdogs/files.c

    • Emscripten では config dir を /persistent_data/ にする。
  • src/autosave.c

  • src/cdogs/player_template.c

  • src/cdogs/config_json.c

  • src/cdogs/yajl_utils.c

    • 保存埌に FS.syncfs(false, ...)。

Packed struct / アラむンメント違反の修正

wasm 移怍で䞀番重芁だったバグ矀。たず前提知識から。

アラむンメントずは: CPU がメモリから 4 バむト敎数uint32_tを読むずき、「4 の倍数のアドレスから読む」こずが期埅されおいる。これをアラむンメント敎列ず蚀う。x86 ç³» CPU はズレたアドレスunalignedからの読み出しも黙っお凊理しおくれるが、C 蚀語の仕様䞊は未定矩動䜜であり、WebAssembly + SAFE_HEAP では実行時゚ラヌalignment faultになる。

なぜゲヌムのファむル読み蟌みで起きるか: 叀いゲヌムのバむナリファむル圢匏は 1 バむト単䜍で詰たっおおり、C 偎では #pragma pack(1) でパディングを陀去した構造䜓packed structをファむルのバむト列にそのたた重ねお読むのが垞套手段だった。䟋えば 6 バむトのヘッダの盎埌に uint32_t の配列が続くフォヌマットでは:

ファむル先頭からのオフセット:  0 1 2 3 4 5 | 6 7 8 9 | ...
                              ヘッダ(6byte) | uint32_t | uint32_t ...
                                             ↑ オフセット6 = 4の倍数でない

この「オフセット 6」のアドレスを uint32_t * にキャストしお読むず、x86 では動くが wasm では alignment fault で abort する。

兞型パタヌン:

  • file buffer 䞊の #pragma pack(1) struct を typed pointer ずしお読む。
  • 6 byte header の盎埌を uint32_t * に cast しお読む。
  • packed struct の uint16_t / uint32_t field を盎接参照する。

察策: unaligned の可胜性があるアドレスを盎接 dereferenceポむンタ経由で読むしない。必ず memcpy でアラむンされたロヌカル倉数ぞコピヌしおから䜿う。memcpy はバむト単䜍で動くのでアラむンメントの制玄がない。

static uint32_t ReadU32(const void *p)
{
    uint32_t v;
    memcpy(&v, p, sizeof v);  /* バむト単䜍コピヌなので安党 */
    return v;
}

今回の倉曎箇所:

  • src/cdogs/cwolfmap/cwolfmap.h

    • CWLevelGetWidth
    • CWLevelGetHeight
  • src/cdogs/cwolfmap/cwolfmap.c

    • MapHeadMagic
    • MapHeadPtrAt
    • LevelLenAt
    • LevelOffAt
    • packed level header の field access を accessor 化。
  • src/cdogs/cwolfmap/vswap.c

    • VSWAP chunk offset / length table を aligned allocation に deep-copy。
  • src/cdogs/cwolfmap/audio.c

    • packed uint16_t music value を memcpy helper で読む。
  • src/cdogs/map_wolf.c

    • level->header.width/height 盎接参照を CWLevelGetWidth/Height に眮換。

これは wasm 専甚 hack ではなく、C ずしお正しい修正。strict alignment の他 platform にも効く。

Linker で出た重耇定矩

  • src/cdogs/cwolfmap/wad/waderrno.h
  • src/cdogs/cwolfmap/wad/waderrno.c

header に int waderrno; があり、耇数の .c ファむルで同じ倉数が定矩される状態だった。ネむティブの linker はこれを「common symbol」ずしお黙認するこずがあるが、Emscripten の linker では duplicate symbol ゚ラヌになる。

修正は C の䜜法どおり: header は extern int waderrno;宣蚀のみにし、.c ファむル1぀に実䜓定矩を眮く。

Debug の進め方

移怍䞭は release build で粘らない。たず debug build を䜿う。

WASM_DEBUG=1 ./build_wasm.sh
python3 -m http.server 8081 --directory dist

ブラりザで開く:

http://127.0.0.1:8081/

debug build では source map が生成されるため、DevTools のスタックトレヌスが index.wasm:0x3258 のような wasm オフセットではなく vswap.c:53:24 のような C の゜ヌス行で衚瀺される。この差は決定的に倧きい。

毎回 hard reload する。ブラりザは index.js, index.wasm, index.data を匷く cache する。修正したのに stack trace が叀い、source map が効かない、wasm offset しか出ない、ずいうずきは cache が原因のこずが倚い。

よく出た問題ず察凊:

症状 原因ず察凊
Aborted(stack overflow ...) Emscripten のデフォルトスタック64KBが小さすぎる。-s STACK_SIZE=2097152 にする
Aborted(alignment fault) packed struct / raw file buffer / typed pointer cast を探し、memcpy accessor に眮き換える前章参照
Cannot set timing mode for main loop since a main loop does not exist SDL renderer 初期化䞭の warning。今回の範囲では非臎呜
ScriptProcessorNode is deprecated SDL/Emscripten の Web Audio 実装内郚由来。非臎呜
N FS.syncfs operations in flight 耇数の保存経路から同時に IndexedDB 同期しおいる。非臎呜だが、仕䞊げで debounce する
フルスクリヌンで黒画面、解陀しおも戻らない shell の Fullscreen 経路が SDL2 port ず非互換入力章の実䟋参照。canvas に盎接 requestFullscreen() し、CSS で拡倧する
音が出ない autoplay 制限。初回のナヌザヌ操䜜埌に AudioContext が resume されおいるか確認
修正が反映されない / stack trace が叀い ブラりザキャッシュ。hard reloadCtrl+Shift+Rする

配垃物ず配眮

珟圚の build は dist/ 盎䞋に出す。

dist/index.html
dist/index.js
dist/index.wasm
dist/index.data
dist/index.wasm.map  # debug build のみ
dist/favicon.ico
dist/og.jpg

すべお静的ファむルなので、配信はどの静的ホスティングでもよい今回は GitHub Pages + gh-pages パッケヌゞ。

サブディレクトリ配信https://example.com/game/ のような配眮を想定し、参照は盞察パスにする。Emscripten が生成するロヌダヌはもずもず index.wasm / index.data をスクリプト䜍眮基準の盞察で解決するので、远加で泚意するのは次の2点:

  • debug build の source map は --source-map-base ./ にする/ だずドメむン盎䞋を芋に行く。
  • favicon はブラりザがドメむン盎䞋の /favicon.ico を芋に行きがちなので、生成 HTML に明瀺的な盞察 link を泚入した。
sed -i 's|</title>|</title>\n    <link rel="icon" href="favicon.ico">|' "$DIST_DIR/index.html"

OGP の og:image は逆に、SNS のクロヌラヌが盞察パスを解決しないため、公開 URL の絶察 URL を䜿う。

䜜業順序の掚奚

  1. upstream の察象 tag/commit を固定する。
  2. rg で既存の Emscripten 察応、filesystem、delay、network、SDL 初期化を調査する。
  3. host toolchain の叀さを確認する。
  4. emsdk、Python 3.10+、CMake をロヌカルに甚意する。
  5. 既存の Emscripten build script があればその倱敗ログを取る。
  6. CMake/configure が本圓に必芁か分解する。
  7. wasm 専甚の最小 build script を䜜る。
  8. asset はたず党郚 preload しお、missing file 問題を消す。
  9. WASM_DEBUG=1 で source map、SAFE_HEAP、stack check を有効にする。
  10. メむンメニュヌ到達たで盎す。
  11. シングルプレむ開始たで盎す。
  12. 音声初期化ず BGM/SE を確認する。
  13. config/save を IDBFS に茉せ、reload 埌に残るか確認する。
  14. fullscreen、resize、subdirectory hosting を確認する。
  15. 䞍芁ログ削陀、syncfs の debounce、asset 削枛を埌工皋で行う。

完了条件

最䜎限の完了条件:

  • ./build_wasm.sh で dist/ が生成される。
  • HTTP server 経由で dist/index.html が起動する。
  • canvas に SDL2 video が衚瀺される。
  • main menu に到達する。
  • multiplayer の join/host 導線が Web 版に出ない。
  • Quick Play たたは暙準 campaign を開始できる。
  • SDL_mixer が初期化され、効果音たたは BGM が再生できる。
  • config や player template が reload 埌も残る。
  • debug build で source map が効く。
  • 既知制限ず build 手順が文曞化されおいる。

参考リンク

今回の root リポゞトリでは、詳しい珟状メモは docs/wasm-port.md英語、構成図は docs/architecture.md英語、実際の build 手順は build_wasm.sh にある。この文曞は、それらをもずに移怍担圓者が最初から同じ道を蟿れるように、前提知識・調査順・刀断基準・元コヌドぞの介入範囲を日本語でたずめたもの。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment