読者です 読者をやめる 読者になる 読者になる

Optimizing graphics rendering in Unity games 読んだ

Optimizing graphics rendering in Unity games のメモ

https://unity3d.com/jp/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games?playlist=44069

Unity 描画パイプライン

毎フレーム、CPUは以下を実行する 1. シーンにある全オブジェクトをチェックし、レンダリングすべきかどうか判定する。 - view frustum に入ってないオブジェクトを除外する - 他 2. レンダリング対象のオブジェクトのレンダリング情報を収集し、ソートする。 - MeshやMaterial - このフェイズで、特定の状況に当てはまると batchingが行われる 3. CPUは、「batch」と呼ばれるデータを作成し、draw call コマンドを作成。

Draw call のステップは以下に分別される。

  • Set pass call: CPUがGPUに render state を送信する。これを Set pass call と呼ぶ。Set pass call は、初めてのメッシュを描画するときや、render stat (たぶんMaterialの変数とか頂点データ)が変化わったときに送信されるコマンド。つまり実質、GPUへデータを転送しているのはこれ。
  • Draw mesh: CPUがGPUへ、 Set pass call で定義された内容を描画する命令をGPUへ送る
  • 複数パスでの描画必要な場合、都度 Set pass call、Draw call がそれぞれ走る

Set pass call 、Draw mesh、これらをひっくるめて Draw callと呼ぶ。 フレームデバッガなどでは分かれて表示されているときは、実質、GPUへデータを転送しているのは Unityの言うところのSet pass call。CPU負荷があるのもSet pass call。

if our game is CPU bound

CPU バウンドな処理をしらべるとき、まずはどのタスクが原因なのかを特定することが重要。 なぜなら、Unityの描画はマルチスレッドで動作しようとするため、重いスレッドで動いているボトルネックをピンポイントで潰さないと効果がないから。

たとえば、culling operationが遅いとき、Set pass callとか別のスレッドで動いているタスクを減らしても効果がない。

また、ハードウェアによってスレッドの本数や割り当てに違いがあるので、実機で確認する必要がある。

Unityには3種のスレッドの使いわけをする - main Thread - 1本 - 基本的にはゲームロジックが実行される - render thread → - 環境によってはマルチスレッドで動作 - マルチスレッドレンダリングは複雑かつハードウェア依存。 - GPUにコマンドを転送する特別なスレッド - worker thread - culling or mesh skinning その他のタスクを実行する - どのタスクがどのワーカスレッドを実行するかは場合による - たとえば、ハードウェアが複数のスレッドを持っていた場合、ワーカースレッドはその分多く生成される - そのため、実機でプロファイリングすることが重要

(裏技)Graphics jobs でマルチスレッド化

Player Settings の、 「Graphics jobs」にチェックを入れると、Unityがメインスレッドでやっているrender loop をworker thread へ逃がすことができる。 つまり、Camera.render 自体が並列になり、めちゃ速くなるということ。

※ただし まだ実験的な機能 なので注意

CPUの描画ボトルネックのみつけかた

CPUバウンドになる最も一般的なボトルネックは、GPUへのコマンド送信。 このタスクは render threadで実行されると考えて良い。(PS4 とかでは worker threadで実行されるらしい)

最も重い処理は、Set pass call。Set pass callを減らすことはCPU負荷軽減に多くの場合に効果が見込める。 送信するデータの量や数を減らすことも効果がある。別々の render stateなオブジェクトはできるだけ減らす。

SetPass Call 回数の軽減になるテクニックは以下↓

  • レンダリングするオブジェクトを減らす
    • 例) カメラの farClipPlane を調整することで、範囲を調整し、カリングされるオブジェクトを増やす
    • 例) カメラの layerCullDistance を設定することで、レイアごとにカリングされる距離を調整することができる
    • 例) Occlusion cullingをつかう
  • 1つのオブジェクトのレンダリング回数を減らす
    • 例) シェーダのパスを減らす。シャドウマップ、dynamic light 他

バッチング

CPUの世界で同一のオブジェクトを収集し、同一のものとしてまとめてGPUに送る

  • 基本的 に同一のMaterial、(同一のMaterialプロパティ、シェーダ変数、テクスチャ) でないとバッチングされない
  • ほぼ同じMaterialで、テクスチャだけが違う、ていうときは、1つのでかいテキスチャに結合して無理矢理Materialを1つにするというテクニックがある
  • スクリプトで Renderer.material のプロパティを書き換えると、 内部ではMaterialが複製されている。sharedMaterialをつかうと、バッチングされているMaterialが対象になる

関連する参考情報や、関連するテクニック - Unity Manual - Optimizing Unity UI - Unite Bangkok 2015 - GPU Instanting - Texture atlas

カリング/バッチングで逆に遅くなるケース

  • カリングはコスト高。カメラの数 x オブジェクトの数 のオーダーで負荷がかかる
  • 使わないカメラはdisableに。使わないRendererもdisableに。
  • バッチングは、逆に負荷になることもあるので要注意

Skinned Meshes

ボーンアニメーション。 スキンメッシュの描画関係の処理は メインスレッド or ワーカースレッド。これもハードウェアに依る。 Skinned Meshes のレンダリングはコスト高。

以下、改善策。 - 現在SkinnedMeshRendererを使用している全てのオブジェクトに対して、本当に使うべきか再検討する - SkinnedMeshRendererが刺さっているけど、実際にはMeshRendererで事足りるオブジェクトがいないかチェック - 特定の一回しかアニメーションしないようなオブジェクトは、アニメーションが終わったら SkinnedMeshRenderrerをMeshRendererに差しかえる。( SkinnedMeshRenderer.BakeMesh という機能がある - 限られたプラットフォームでは、実験的に GPU skinning がサポートされている

メインスレッドの処理は、描画タスクとは無関係

CPUバウンドだからといって、メインスレッドの処理を最適化しても無意味。 なぜならスレッドが別だから。

if our game is GPU bound

GPUの足をひっぱるのは大抵は fill rate。 特にモバイルはGPUに見合わない解像度があるので問題が大きくなる。

memory bandwidth と、頂点の処理も同様に重要ではある。

Fill rate

Fill rate とは、1秒あたりにGPUが描画しなければいけないピクセル数のこと。

Fill rateが問題になっているかどうかは、 Player Settingsで、Screen Resolutionを減らしてみて、前後のGPU timeを比較することで確認できる

Fill rate問題の改善方法は以下 - フレグメントシェーダを最適化する - オーバードロー(同じピクセルを何度も描画すること)を減らす。でかいオブジェクトが描画された後、その上にさらにでかいオブジェクトが描画されるようなとき、2つぶんのFill rateになる。render queue を適切に管理して、フラグメントシェーダの前にカリングする等が対策 - ImageEffects はFill rateがめっちゃ上がる。ImageEffectsを2つ以上使う場合は倍々でFill rateが上がる

Memory bandwidth

GPUは専用のメモリを持っている。このメモリの read/writeが性能の限界になることがある。テクスチャがあまりにもでかすぎるとか。 Memory bandwidthが問題になっているかどうかは、Quality Settingsからテクスチャの解像度を落としてみて、前後の GPU timeを見比べることで確認できる

Vertex processing

RxSwift つかってる部分の引き継ぎしてる

前回 に続き、担当してたアプリについて考えていたこと書き記してる。

とくに RxSwift がなぜ良いのか? ということは、説明がすこしむつかしいので、すこし丁寧に書くチャレンジをしてみている。冒頭のところ公開してみる。

--

Rxは何を解決できるか?

クライアントサイドのアプリケーションへの入力は、タイミングがまったく予測できず、おまけに、並列であちらこちらに到着します

イメージ図)

入力の種類 \ 時間 0.166秒 0.33秒 0.5秒 0.66秒
 ひとさし指の動き タップ! スワイプ開始!
 おにいさん指の動き タップ!
 HTTPレスポンス その1 到着!
 JSONのデコード 完了!
 写真の読み込み 完了!
 HTTPレスポンス その2 到着!
 PNGのデコード 完了!

これらバラバラのタイミングで連続して発火する入力を漏らさず受け取り、整合性を取っていかなければいけない、この辺りにクライアントサイドの(設計面での)難しさがあります。

iOS は、アプリが起動してから落ちるまで、1秒に60回くらいのハイペースで休まず画面を描きかえ続けています。

f:id:hadashia:20160825154943p:plain

(1秒に60フレーム)

そのため、我々アプリ層のプログラマは、画面に出力すべき内容の変更を検出し次第、即、それを反映させる責任があります。画面は、プログラムの処理が終わるのを一切待ってはくれず、こうしている間にも、随時、描き直されまくられているのです。

まとめると、入力は並行してバラバラにくるし、出力は即反映させないといけない、ということです。 これは、入力 → (処理) → 出力、というごく単純なシーケンスを考えるだけでは、全体を捉えることがむつかしいです。なにかすごいパラダイムの助けを借りて、人間に理解できる脳内モデルを導入したいところ。

ところで、なぜ入力は、並行してやってくるのでしょうか? それは、iOSがしている1秒に60回の画面描き換えをブロックしてはいけないからです。

UIを書き換えているスレッドは、1/60s = 1.66ms 以内に画面を書き換え終わらなければいけないため、このスレッドで時間のかかる処理はできません。I/O待ちは当然非同期、CPUバウンドな処理は別スレッド、周りを見渡せば、アプリ層そのものが非同期だらけです。アプリ層そのものが複雑なのです。

ご存知のとおり、非同期処理が多くなると、上から下に順番に処理を書いているぶんには簡単だったことも、途端に難しくなります。

たとえば、

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす
  • エラーがどこで発生しようが、いつも同じ位置で処理する
  • 処理と処理の順番を指定する
  • 不要になった処理を途中で止める

もちろん、どれもこれも、同期的に上から下に処理が流れるだけであれば単純なことなのですが、非同期がからむと途端に難しくなります。それに、iOS はマルチスレッドの実行ができるから、並列処理特有の問題もあります。変数のデータ競合を避けるとか。

これがクライアントサイドの設計の本質的に難解な点です。ドメインロジックとプレゼンテーションレイヤを分離するとか、UIとデータをバインドするとかでは、秩序を齎すに十分ではないことがわかるとおもいます。

このアプリでは これに対応するため Rx をつかいました。じつを言うと、Rx なら、上の問題を解決することが簡単にできます。

  • 複数の入力データをくみあわせて、ひとつの出力をはじきだす → Observableを合成するだけ
  • すべてのエラーを、適切なタイミングで出す → onError にエラーが伝搬してくる
  • 複数の処理を適切に待ち合わせる → スケジューラを使えば解決
  • 不要になった処理のキャンセル → Disposableを定義してけば自動でキャンセルされる
  • race condition を避ける → イミュータブルなデータと副作用のない関数の操作がメインなので問題になりにくい。必要なときは、スケジューラでスレッドを指定する

このリストをみれば、Rx と、Rxの亜種との違いがわかると思います。 Rxは、言語仕様によるサポートを一切必要とせず、それどころかどんな言語にもまったく同じ超強力かつ型安全なインターフェイスを提供することに成功しているのですが、なかでも、Disposable 、スケジューラ などの実用的な周辺機能があるおかげで、非同期の難しさに立ち向かうことができます。これらは、みんなの力で言語をまたいで移植され、いきいきと活動し続けています。

変更可能性(ミュータビリティ)

複数の非同期処理 の整合性をとる、これを素朴に実装すると、おびただしい数の状態変数が生まれることになります。

非同期処理によって、あちらこちらに点在するコールバック関数が呼ばれるわけですが、これらを連携させるには、コールバック関数の外側のスコープに変数をつくって、読み書きするしかないからです。

だけど考えてみてください。これは、原理的にやってることがグローバル変数と一緒です。どこからどの変数が更新されるかを保証することも仮定することもできないので、デバッグが非常に困難です。

もしも、変数の内容がどこかで書き換わることがなく、関数に副作用がないとしたら、状況によって挙動が変わるということがなくなるので、とても見通しがよくなります。

リアクティブプログラミングは、そういった考え方をもたらしてくれます。中間の状態を変数で管理するという必要がなくなるのです。 イメージは、事前にたくさんのイベント同士の関連をすべて一箇所で宣言する、それだけです。 イベントとイベントの関係を事前に宣言しておけるので、たくさんのイベント間で1つの変数を読み書きする必要がありません。

書き換わることがない変数には、いくつか優れている点があります * マルチスレッド間でデータ競合を引き起こすことがありえない。安心して別スレッドへ渡せる * 参照を共有する必要がないので、データは値型でよくなる。すると参照のコピーが走らなくなるので、Swift の ARC によってリファレンスカウントを管理する必要がなくなる。余計なバイトコードが生成されなくなるし、参照を数える必要がなくなり、パフォーマンスがよい。(コピーの時間が無視できないほどの巨大なデータはこのかぎりではない)

GUI プログラミングで、 関数型言語のムーブメントが盛り上がっているのは、このような複雑な状態管理を解決できるアプローチになりうるからです。

関数の副作用を廃して、始点の入力さえあれば、中間のデータを保持することをかんがえなくてもすべてを宣言できる、この考え方は、非同期だらけのなにかを書くのに非常にマッチします。Swift が関数型の影響を受けているのは、この目的と無関係ではないでしょう。

サーバサイドプログラミングとの違い

結果が変わり次第、随時出力を求められるというのは、サーバサイドにはあまり見られない制約です。

HTTPなサーバサイドのプログラミングを考えると、アプリ層への入力は、「リクエスト」という単一の入力だけがあり、相手に対して応答するのも一度だけ。しかもその間、相手を待たせておくことができます。

入力 → (何か処理) → 出力

サーバサイドは、アプリケーション層自体よりも、むしろより低レイヤなDB層の設計のほうが大変だったり、アプリ層そのものでなく、アプリ自体を並列で並べるアーキテチャ面などに難しさがある傾向があります。

ネイティブアプリケーションは、オーソドックスなWebページとは根本的に難しさが違います。アプリ層の中そのものが非同期だらけです。そのため、クライアントサイドではRxなど、アプリ層で別のパラダイムを導入するムーブメントがあるます。

Rx 入門

iOSアプリの引き継ぎしている

理由あって、仕事でやっていたiOSアプリの開発を抜けることになった。

新しい担当者は、いつかあちこち変更したい気持ちになと思うので、もちろんどんどんやってもらって構わないけど、変更していいのか確信に至れなかったときのため、現状なぜこうなっているのかの理由を書き残しておくことにした。きっと、ちかい将来に役に立つときもあるとおもう。

内容は異論もあると思うけど、せっかくなので、汎用的な部分を、ここにもすこし公開していくことにした。

--

Storyboardは善良

このアプリでは、Storyboard で設定できることは、可能なかぎりStoryboardでやっています。

普段、Webサービス開発をしていると、HTML/CSS とか、人間も機械も読み書き可能なテキストを編集していく、この Linux的なノリによって UIを組み立てることに慣れているかもしれません。これは、どんな環境でも好きなエディタで開け、競合してもマージでき、オープンソースと相性が良いやりかたであることは間違いありません。

しかしテキスト編集による開発だけがすべてではありません。iOSのコードはビルドに時間のかかる言語でできていますし、LL言語のランタイムを組み込んで動作を制御するレイヤーみたいなものもありません。UIを表現したXMLファイル(xib)も実行時にバイナリにコンパイルされ、基本的には文字をなにか書くと再ビルドに時間がかかると考えた方が良いです。

それに、ネイティブアプリケーションは、標準化されたUIコンポーネントを並べるだけじゃなくて、なんでもできるし、ドキュメント化できない触り心地みたいなものも大事です。あるとき「動いているものを目視しないことには良いのか悪いのか判断しかねまっす」といった部分がたくさんあることに気がつきます。とくにそのような箇所では、できるだけコードをビルドせずにリアルタイムに直感的に変更が確認できる部分が多いワークフローが大事だと考えていました。

つまり Storyboard は iOSの良心なんです!

極端に言うと、UIコンポーネントの位置を調整したいとき、コードを編集してビルド開始して10秒待つ、を100回繰り返すより、リアルタイムに変更が即座に反映されるStoryboard 上でマウスを100回ポチポチするほうが 1000倍速いです。いかにビルドをせずに変更を確認できるかということを本来は考えていくべきだったと思います。

とくに、Autolayoutには注意です。Autolayoutをコードによって設定するとき、それはそれはちょっとした独自言語のようなデザインになっていて、実行時でないと結果が確認できないばかりか、コンパイル時に誤りを検出することもできないのです。Autolayoutをコードで設定するメリットはかなり弱いと思います。そんなわけでできるだけ Storyboard に寄せよう。

コードから動的にレイアウトを変更する必要に迫られたときは以下のようなことをしていました。 * Storyboard上で定義した制約を、コードからIBOutletで参照しておき、実行時に制約の値だけを動的に変える。 * 共通部品については、xibで定義して、コードからはめこむようにしたり * 動的に画面遷移先が変わるような場合も、コードからセグエを実行したり、カスタムセグエを定義するようにしていました。

vimさえあればStoryboardなんていらないさ。おれは暗黒VimのDark powerで、vim からiOSシミュレーターを操作する術さえ心得ているんだ」 そこのきみ、さあ今すぐ武器をすてて両手を頭の上に置いて! ていこうしても無駄だよ。きみだって、いつかこんな日がくるってわかっていたはずさ。どんな友情も、終わりがあるからこそ美しいとはおもわないかい?

ライブラリは最期の手段

このアプリ では、外部ライブラリを使うことにとっても慎重な考え方をしていました。

UIコンポーント

とくに、UIコンポーネントそのものが切り出され提供されているライブラリはかなり意識的に使わないようにしていました。 この分野で人気のあるものは、たとえば、画面のド真ん中に読み込み中をあらわす「ぐるぐる」がいっちょまえに表示されるものとかですね。 しかし、このようなものを作るには特別な知識はとくに必要なく、iOS開発者を自認する方々なら30分くらいで書ける程度のものではないでしょうか。 (もちろんインターフェイスの良し悪しや完成度など品質の違いはあれど)

こうした使いまわしUIコンポーネントは、UIKit のカスタマイズ方法がわからない片手間の開発者や、デザイナがいないプロジェクトなどにとっては有益だと思いますし、実際助かる人が多いはずだと思いますが、目的を品質の高いUIに絞って考えたとき、UIコンポーネントはプロダクトのデザインとしてふさわしいものを一から考えることがより良い方法であると考えています。

Web開発に慣れていると、仕様がある部分だったり、皆の欲しいものが一致しているレイヤについては、積極的に品質の高いオープンソースライブラリを当てはめていくといった考え方でうまくいった経験を多くしていると思いますが、フロントエンドのレイヤーに下りてくると、そのバランスがもっと微妙になってきます。(もっともっと下りていくとこの道はデザイナにつながっていることを想像してください)

UIコンポーネントライブラリの大半は、設定によって柔軟に挙動を変えるためのコードでできています。そういうものをとっぱらってコアな部分だけをとりだすと、ほらみてください。せいぜい 10-50行くらいでしょ。ほら。ほら。 使いたい動きやUIのライブラリをみつけた際は、しくみをざっと読んでエッセンスだけとりこんで4秒くらいで実装しましょう。君にはそれができるはずです。そのほうが柔軟にプロダクトにフィットするものができ、挙動のカスタマイズ、見た目のカスタマイズも簡単で、負債にもなりません。 世の中のすてきなアプリをみて、フロントエンドはそれくらいアプリごとに作りたいものに開きがあると知りましょう。

HTTP

この話は、 UIコンポーネントに限った話ではありません。プロダクトごとにスタイルが違う要素に関しては注意です。

人気のあるものところでは、HTTPクライアントの使いやすいインターフェイスを提供する Alamofire もかなり意識的に使っていません。 こういう類のものは、必ずしも使えないというわけではないですが、標準ライブラリと比べてパラダイムが何も変わっていない。ただのコールバックベースです。より設計に気をつかっているプロジェクトでは、Rx を採用していたり、野良Promise的なものを採用していたり、非同期処理そのものをオブジェクトとして表現する統一的な方法を導入するはずで、 HTTP も同じスタイルに統合していくにあたって、 Alamofire を導入するメリットがありません。

browserify から rollup.js に乗り換えてる

仕事でやってるブラウザjsのビルドには、長らくbrowserifyを使っていましたが、さいきんrollupに乗り換えました。

module bundler ツールたち

browserify、rollup、どちらも主眼に置いているのは、モジュール分割して書いたソースコードの依存解決です。必要なjsソースファイルをすべて辿り、展開して、まとめた1つのjsファイルをつくる、それが主な仕事です。 こうしたツールはmodule bundlerとか呼ばれています。

module bundler が必要な理由は、現状のブラウザではモジュールとして書かかれたjsの依存解決をすることができないからです。実現方法もまだ議論中みたいです。

また、大量のjsファイルをブラウザが読み込ませることは、パフォーマンス上のオーバーヘッドが無視できないという問題も相変わらずあり、現状では手元でせっせと依存解決して1つにまとめたjsをこしらえてから本番環境に置く、というワークフローが普通です。 module bundler は、このワークフローの面倒をまとめて見てくれる存在です。

開発中に書いたソースを本番用jsファイルをつくるには、モジュールの依存解決だけではありません。こんな変換もしたいです。

  • トランスパイル
    • es2015など新しい仕様のjsをブラウザで動く文法のjs変換 → babel、etc..
    • TypeScriptなどAltJS言語をブラウザで動くjsに変換 → TypeScript、etc..
  • 本番用ファイルを短縮、難読化してファイルサイズを減らす
  • ソースマップの出力

などなど。

この点、browserifyやrollup、あるいはwebpackもそうですが、プラグインを入れるだけで上記のような工程を簡単に追加できます。

module bundler は、人間が読み書きするjsを入力にとり、あれこれした後、最終的な成果物の実行用jsを吐くところまでに責任を持っているので、その間どのタイミングでどんな工程をはさむべきかを知っています。 そのためmodule bundler を使っていれば、パイプラインの順番に細かく気を配ったり、ビルドのためにスクリプトを書く必要もほぼなく、宣言するだけで、やりたいことが実現できます。最終的な出力とソースファイルを紐づける必要のある、ソースマップなどの機能も特別なことをしなくても利用できます。

gulpなどのjs製タスクランナーでパイプラインを組み立てる場合、自分でビルドのパイプラインをかなり詳細に設定することが可能ですが、それが必要になることは本当にごく稀だと思います。 よほど気合を入れてタスクを実装しない限り、 module bundler のパイプラインのほうが優れているし簡単です。コマンドラインからも利用できるので、 僕は コマンドを Makefile とか package.json とかに書いておいて使ってます。

CommonJS モジュール と ES モジュール

rollup は browserify や web pack より後発の module bundler で、 ESモジュールをそのまま解釈できることが大きな特徴です。

js の モジュール分割の仕様で、今後主流になるのは、es2015で標準化された ESモジュールだと思われます。

標準化されたのは node.js がだいぶ普及した後だったので、node.js では 別の仕様、CommonJS モジュールが採用され普及しています。 略して CJS モジュール です。

人気のある module bundler ツールの1つである browserify も、そもそもは CJSモジュールを ブラウザ js でもサポートする、というふれこみのツールでした。 ブラウザ js では、今後は 標準化された ESモジュールが使われていくことになると思われ、node.js 側でも、ESモジュールを利用できるようにするための議論がされているようです。 近い将来は、node モジュールは、CJS、ES 両方の仕様を提供するようになっていく模様ですが、もっと将来は、CJSモジュールはレガシーなものになっていくのではないでしょうか。

公開されている npm package も、最近のものは、ESモジュールが配布されていたりします。たとえば RxJS5 なんかは、ESモジュールファイルとして配布されるパッケージと、CommonJSモジュールとして配布されるパッケージが両方提供されていたりします。

rollup.js は、ES モジュール 形式で 配布されている npm パッケージ を、直接解釈することができます。それがbrowserify との大きな違いです。

browserify は、CommonJS モジュールをブラウザjs でもサポートすることがそもそもの目的だったので、ESモジュールで配布されているパッケージを直接読むこはできません。babel なんかは、ES モジュールを CJS に変換する 機能も持っているので、そういうツールを組み合わせ、CJSモジュールに変換してからあらためて解釈するという少し回り道なやりかたをとるしかありません。 ただ、babel はデフォルトでは npm パッケージを トランスパイル対象にはしないので、npm パッケージ側でCJS 、ES 両方のモジュール で提供してくれているパターンが多いです。

Tree-shaking

さて、rollup はただ仕様が新しいから気分的にすがすがしいというだけでなく、「吐いたjsファイルの容量が小さくなる」という実用面での大きなメリットがあります。

CommonJS モジュール、 ESモジュール、2つの仕様の違いは、シンタックスだけではなく、ESモジュール のほうがより static で explicit な仕様になってます。そもそも、js の良いところはめちゃめちゃにdynamicなところではあったのですが、近頃はコードベースが巨大になりがちなので、どちらかというと実用面での要請から 仕様が static めいてきてきているようです。ローカルスコープもクロージャもなにからなにまですべて function で表現できた時代は終焉を迎えてしまいました。

ES の import/ export は、式ではなく特別な文です。ソースファイルの トップレベルのスコープにしか置けない決まりになっていいます。このおかげで、 モジュールのソースを実行しなくても、パースすれば依存しているモジュールがわかるというわけです。

rollup は、この特徴を利用していて、依存モジュールを構文解析し、参照されていない オブジェクトや関数はソースコードをみつけて削除してくれます。これは Tree-shaking と呼ばれていて、公式サイト http://rollupjs.org の Webぺ上から試せるデモで、ファイルサイズが減る臨場感を感じることができます。

また、CommonJS では、 var hoge = require(‘hoge’) のように、require結果はその都度、ローカルスコープに変数がつくられるので、 require の数だけ 別の参照がつくられることになりますが、一方、ESモジュールの import によってつくられた 変数は、コード全体で同じ参照をもつという仕様なので、import は一度だけ インライン展開すればそれで済みます。この辺りもコード量減につながっているのかもしれません。

rollupの処理の流れ

次に、現時点で rollup がどんな動作をするのかかんたんにまとめてみます。

プラグインなんもいれてない場合

まず、 プラグインなしの場合の rollup がしてくれることは以下です。

  • エントリポイントの js ソースファイルいっこを入力にとる。
  • ソースファイルに import 文 がみつかったら、相対パスからモジュールファイルを探してきて読み込んで展開。1つのファイルにまとめる。
  • もし 対応するモジュールが相対パスから発見できなかったら、無視。(import文がそのままソースに残る
  • その他、オプションによって以下がサポートされている。
    • ソースマップの出力
    • 出力されるjs を、ローカルスコープでラップするか否か、する場合はそのフォーマットの指定。

rollup は、プラグインを使うことで、モジュールを解決する際の検索対象をカスタマイズしたり、各モジュールをトランスパイルしたりといったことができます。

プラグイン: rollup-plugin-node-resolve

  • rollup-plugin-node-resolve を入れると、 node_modules/ ディレクトリ内のパッケージもモジュールの検索対象に含んでくれます。
  • デフォルトでは、 node_modules/hoge などモジュールのルートディレクトリが対象になります。
    • たとえば、 import { a } from ‘hoge’ とした場合、 node_modules/hoge/a.js が対象に。
    • jsnext: true を 設定しておくと、 nodeモジュールの package.jsonjsnext プロパティが設定されているか見に行き、そこに指定されているファイル名をモジュールデフォルトとして扱います。

プラグイン: rollup-plugin-commonjs

  • rollup-plugin-commonjs を入れると、CJS モジュールでの提供しかない npm パッケージ を rollup で読み込めるようになります。
  • 具体的には、 npm モジュール内の 、CJS 記法、exports とかを、 ES の export 文に トランスパイルする、ということをやっているみたいです。
  • この発想、興味深いです。
  • また このプラグインは、 副次的効果として、 index.js という名前のファイルを特別扱いし、 フォルダ内のデフォルトとして扱うようになるという、CJS 由来の 仕様 もサポートされます。
  • 僕は、CJS モジュールでしか提供されていない npm パッケージを明示的に指定して使っています。

プラグイン: rollup-plugin-babel

最終的に ブラウザで動作させる js にするには、まだ実装されていない仕様を 変換 するという工程が必要です。

rollup-plugin-babel を使うことで、各モジュールを1つ1つ babel で変換します。

babel は、サポートしたいES の仕様をpluginによってフレキシブルに指定できるようになっていますが、rollup から使う場合は、 「ESモジュールを CJS モジュールに変換する」というplugin を除外しないといけません。 rollup は ESモジュールをそのまま解釈し、CJSを解釈できないので。

そのため、公式にもあるとおり、 babel-preset-es2015-rollup という preset を指定します。これは、 babel-preset-es2015 から、 babel-plugin-transform-es2015-modules-commonjs を取り除いた ものみたいです。

他にも、rollup の作者による ES の トランスパイラ、buble というものもあり、rollup-plugin-buble というのもあります。

だいたいこのあたりのプラグインを入れておけば、やりたいことが実現できると思います。 他、watch や uglify などのプラグインもあります。

Rust から ImageMagick 呼び出してみる

Rustがかなりかっこいいのですこし触っています。

Rust はまるで、めちゃめちゃ易しくなったc++かのようです。メモリリークやその他の危険が扱いやすくなったし、シンタックスも今風だ。関数型言語由来のパターンマッチやOptional/Eitherのエラー処理などすげーセンス良い機能が搭載された一方、メモリのようすが透明でなんでもできてどこでもリンクできるパワーはc++並みです。

Rustは、c/c++みたく、明示的に指定しないかぎりすべての変数はスタックに実体があるので、参照やヒープを使うときにこそ記号めいたものを書くという世界観です。これは、僕が普段さわっている、オブジェクトが基本ヒープの参照で、ランタイムがGCしてくれるってな世界観とはまったく逆なので、その辺はやはりすこし難解にかんじます。でも慣れると楽しいです。

Rustはサーバ用途としても強そうだし、ネイティブなプラットフォームでつかうライブラリ用途とかにもよさそうです。

FFI

今回は、RustからImageMagickのCのAPIを呼び出す行為をしてみました。

Rustには、Cのライブラリを呼び出す方法が提供されています。

Rustには入門向けのドキュメントがあって、とってもおもしろいです。

https://doc.rust-lang.org/book/

そしてこれを日本語に訳されている方々がおり、大変ありがたいです。m( )m

FFIについてはここに説明がありました。

https://rust-lang-ja.github.io/the-rust-programming-language-ja/1.6/book/ffi.html

Rust から Cのライブラリをリンクするには、rustc コンパイラ-l とか -L とかcコンパイラのノリのオプションをそのまま渡すこともできますが、通常rustプロジェクトのビルドにはCargoを使うことになると思います。

CargoはRustの依存ライブラリ管理&ビルドツールで、標準で添付されています。Cargoは使いやすいし、クロスコンパイルも簡単みたいです。こういうツールが標準であると敷居が下がって嬉しい。

MagickWand とリンクする

ImageMagick には C から標準的な画像処理を簡単にできるようにしたMagickWandというライブラリが提供されています。 これをrust から叩いてみようと思います。

まず、cargo の設定ファイルに、依存ライブラリを追加します。

[package]
name = "sample-rust"
version = "0.1.0"
authors = ["hadashiA <tanuki@example.com>"]
build = "build.rs"

[dependencies]
libc = "0.2.0"

[build-dependencies]
pkg-config = "0.3.0"
  • pkg-config は、その環境にあるcライブラリのパスを探すツール、pkg-configをRustから利用できるモジュールです。
  • libc は、cの型をRustで定義したものが入っているモジュールです。

次に、ビルドスクリプトをちょっとだけ追加します。ビルドする環境にあるcのライブラリの場所は、動的に探す必要があるためです。

プロジェクトルートに、build.rs をつくり、以下のようにします。

extern crate pkg_config;

fn main() {
    pkg_config::probe_library("MagickWand").unwrap();
}

これだけ書いておけば、CargoはMagickWandをみつけてくれます。

このコードがなにをしているかというと、標準出力にビルドオプションを吐くという動作をしています。 僕の環境では上記のコードは以下の文字を吐きました。

cargo:rustc-link-search=native=/usr/local/Cellar/imagemagick/6.9.3-0_1/lib
cargo:rustc-link-lib=MagickWand-6.Q16
cargo:rustc-link-lib=MagickCore-6.Q16

Cargo のドキュメントによると、build.rs の標準出力への書き込みによって、ビルドオプションをコントロールするという仕様のようです。 http://doc.crates.io/build-script.html

bindgen

これで c のライブラリはリンクできますが、Rust からCの関数を叩くには、cの関数宣言をRustの文法で書いてあげる必要があるみたいです。cの .h 拡張子のヘッダファイルみたいなノリです。というかむしろ、cのヘッダファイルからRustの関数定義を生成してくれる bindgen というツールがあります。これをつかってみようとおもいます。

$ cargo install bindgen

bindgen コマンドに、cコンパイラ的なノリのオプションを渡してあげると、指定したcのヘッダファイルをrustコードに変換したものが生成されます。

$ bindgen -I /System/Library/Frameworks/Kernel.framework/Headers -I /usr/local/include -I /usr/local/Cellar/imagemagick/6.9.3-0_1/include/ImageMagick-6/ /usr/local/Cellar/imagemagick/6.9.3-0_1/include/ImageMagick-6/wand/MagickWand.h -o magickwand.rs

Macで実行していたら、stdarg.h がない とか色々エラーがでてしまったんですが、適当にオプションに追加したりしたら動いたみたいです。

生成されたコードはそのままでも動いたのですが、なぜか9000行以上あったりわかりずらかったので、今回は勉強がてら、使いたいインターフェイスの定義だけあらためて手で書いてみました。

extern crate libc;

use libc::*;

pub struct MagickWand {}

pub type MagickBooleanType = c_uint;
pub static MagickFalse: c_uint = 0;
pub static MagickTrue: c_uint = 1;

#[repr(u32)]
pub enum ExceptionType {
    UndefinedException = 0,
    WarningException = 300,
    TypeWarning = 305,
    OptionWarning = 310,
    DelegateWarning = 315,
    MissingDelegateWarning = 320,
    CorruptImageWarning = 325,
    FileOpenWarning = 330,
    BlobWarning = 335,
    StreamWarning = 340,
    CacheWarning = 345,
    CoderWarning = 350,
    FilterWarning = 352,
    ModuleWarning = 355,
    DrawWarning = 360,
    ImageWarning = 365,
    WandWarning = 370,
    RandomWarning = 375,
    XServerWarning = 380,
    MonitorWarning = 385,
    RegistryWarning = 390,
    ConfigureWarning = 395,
    PolicyWarning = 399,
    ErrorException = 400,
    TypeError = 405,
    OptionError = 410,
    DelegateError = 415,
    MissingDelegateError = 420,
    CorruptImageError = 425,
    FileOpenError = 430,
    BlobError = 435,
    StreamError = 440,
    CacheError = 445,
    CoderError = 450,
    FilterError = 452,
    ModuleError = 455,
    DrawError = 460,
    ImageError = 465,
    WandError = 470,
    RandomError = 475,
    XServerError = 480,
    MonitorError = 485,
    RegistryError = 490,
    ConfigureError = 495,
    PolicyError = 499,
    FatalErrorException = 700,
    TypeFatalError = 705,
    OptionFatalError = 710,
    DelegateFatalError = 715,
    MissingDelegateFatalError = 720,
    CorruptImageFatalError = 725,
    FileOpenFatalError = 730,
    BlobFatalError = 735,
    StreamFatalError = 740,
    CacheFatalError = 745,
    CoderFatalError = 750,
    FilterFatalError = 752,
    ModuleFatalError = 755,
    DrawFatalError = 760,
    ImageFatalError = 765,
    WandFatalError = 770,
    RandomFatalError = 775,
    XServerFatalError = 780,
    MonitorFatalError = 785,
    RegistryFatalError = 790,
    ConfigureFatalError = 795,
    PolicyFatalError = 799,
}

#[repr(u32)]
pub enum FilterType {
    UndefinedFilter = 0,
    PointFilter = 1,
    BoxFilter = 2,
    TriangleFilter = 3,
    HermiteFilter = 4,
    HanningFilter = 5,
    HammingFilter = 6,
    BlackmanFilter = 7,
    GaussianFilter = 8,
    QuadraticFilter = 9,
    CubicFilter = 10,
    CatromFilter = 11,
    MitchellFilter = 12,
    JincFilter = 13,
    SincFilter = 14,
    SincFastFilter = 15,
    KaiserFilter = 16,
    WelshFilter = 17,
    ParzenFilter = 18,
    BohmanFilter = 19,
    BartlettFilter = 20,
    LagrangeFilter = 21,
    LanczosFilter = 22,
    LanczosSharpFilter = 23,
    Lanczos2Filter = 24,
    Lanczos2SharpFilter = 25,
    RobidouxFilter = 26,
    RobidouxSharpFilter = 27,
    CosineFilter = 28,
    SplineFilter = 29,
    LanczosRadiusFilter = 30,
    SentinelFilter = 31,
}

extern {
    pub fn MagickWandGenesis();

    pub fn NewMagickWand() -> *mut MagickWand;
    pub fn DestroyMagickWand(wand: *mut MagickWand) -> *mut MagickWand;
    
    // Iteraor

    pub fn MagickGetIteratorIndex(wand: *mut MagickWand) -> ssize_t;
    pub fn MagickSetIteratorIndex(wand: *mut MagickWand, i: ssize_t) -> MagickBooleanType;

    pub fn MagickGetException(wand: *const MagickWand,
                              severity: *mut ExceptionType) -> *mut c_char;
    pub fn MagickReadImageBlob(wand: *mut MagickWand,
                               blob: *const c_void,
                               size: size_t) -> MagickBooleanType;
    pub fn MagickResetIterator(wand: *mut MagickWand);
    pub fn MagickNextImage(wand: *mut MagickWand) -> MagickBooleanType;
    pub fn MagickResizeImage(wand: *mut MagickWand,
                             columns: size_t,
                             rows: size_t,
                             filter: FilterType,
                             arg5: c_double);
    pub fn MagickSetImageFormat(wand: *mut MagickWand, format: *const c_char);
    pub fn MagickGetImageBlob(wand: *mut MagickWand, size: *mut size_t) -> *mut c_uchar;
}

こんなかんじで、実装のない関数宣言を c のヘッダからrust に翻訳したものをつくってあげます。 これの関数宣言を magickwand.rs として保存すると、 mod magickwand; で読み込むことができました。

extern crate libc;

mod magickwand;

use std::fs::File;
use std::io::{Read, Write};
use std::ffi::CString;
use libc::{size_t, c_void};
use magickwand::*;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 3 {
        println!("Usage: {} SRC DST", &args[0]);
        return
    }
    let src = &args[1];
    let dst = &args[2];
    
    let mut blob: Vec<u8> = Vec::new();
    let bytes = File::open(src)
        .and_then(|mut file| file.read_to_end(&mut blob))
        .unwrap();
        
    unsafe {
        // MagickWandGenesis();
        let wand = NewMagickWand();
        let status = MagickReadImageBlob(wand,
                                                     blob.as_ptr() as *const c_void,
                                                     bytes as size_t);
        if status == MagickFalse {
            let mut severity = ExceptionType::UndefinedException;
            let description = MagickGetException(wand, &mut severity);
            let description = CString::from_raw(description);
            println!("{}", description.to_str().unwrap());
            std::process::exit(0);
        }

        MagickSetIteratorIndex(wand, 0);
        MagickResizeImage(wand, 100, 100, FilterType::LanczosFilter, 1.0);
        MagickSetImageFormat(wand, CString::new("PNG").unwrap().as_ptr());

        let mut size: size_t = 0;
        let ptr = MagickGetImageBlob(wand, &mut size);
        let blob = Vec::from_raw_parts(ptr, size, size);

        File::create(dst)
            .and_then(|mut file| file.write_all(blob.as_slice()))
            .unwrap();
    }
}

ただcの関数を呼んでるだけでの、rustらしさのないコードになってしまいましたが、これでコンパイルとおりました。

$ cargo run src.png dest.png

実行してみたところ、MacikWandの活躍によって画像がリサイズされます。

RAII

しかし、上記のように生のc関数をそのまま呼ぶのは、rustの世界ではけっこうつかいずらいです。

unsafeブロックで囲まないといけないし、MacickWandが管理しているメモリをつかいおわったら手で解放する必要があるので、いまいちrustの恩恵に預かれないし、あと見栄えもよくないです。

そこでMagickWandをもうすこしrustでラップしてみます。

rustの値は明示的に指定しない限りスタックに置かれていて、メモリ解放するタイミングが明確なので、c++で頻出のRAIIイディオムでリソース管理することができます。

値型のコンストラクでリソースを取得し、デストラクタで華麗にリソース解放するあれです。

pub struct Wand {
    ptr: *mut magickwand::MagickWand,
}

impl Wand {
    pub fn new() -> Wand {
        let ptr = unsafe {
            magickwand::NewMagickWand()
        };
        Wand { ptr: ptr }
    }
}

impl Drop for Wand {
    fn drop(&mut self) {
        unsafe {
            magickwand::DestroyMagickWand(self.ptr);
        }
    }
}

このようにDropトレイトを実装しておくと、dropメソッドに書いた処理が、変数解放時に呼ばれます。

// たとえば、、
{
  let wand = Wand::new();
} // このスコープを抜けるときに DestroyMagickWand() が呼ばれる

将来は標準のDOM APIを使うのがより身近になると思ってる

最近、標準のDOM APIは別に悪くない、と考えるようになった。 そう考えて劇的に何か変わるかというと、現時点ではライブラリを使うことに慎重になるという気分的なものかもしれない。

気分が変わった結果として、僕は直近のプロジェクトのごくふつうのWebページでは、標準のDOM APIを直接さわる形に変更した。フレームワークは使わずRxJSのみ使っている。結果、パフォーマンスと細かいUIの挙動とコードの透明度が改善された。

標準のDOM APIは、べつに不必要に冗長なところがあるわけではないし、扱っているものが特別プリミティブ過ぎるとも思わない。むしろ、意図しない動作が入りずらく、インターフェイスが明示的にできている点なんかは優れている。 欠点があるとすれば、あらゆるスコープから好きなNodeの書き換えが禁止されてない点、クライアントサイドでのレンダリングのサポートが弱い点、何をするにもまず検索 (querySelector) しないといけない点かな。

しかし、これらを解決するにあたって、今流行しているようなアプローチが、将来に渡って必要とされ続けるかどうかは少し疑問視している。

今流行しているアプローチというのは、標準DOM APIをラップしたレイヤーを介すようなもの。たとえば、DOM要素を常にコレクションとみなして抽象化するjQuery。HTMLテンプレにDSLを埋めて暗黙の動作をさせるもの(vue, angular)。仮想DOMや、仮想DOMにコンパイルされるJSXでのみ記述するものとか。

これまでは、そうしたアプローチが良いと思って、従ってきた。 しかし、最近はあまりそう考えなくなった。

jsが辿ってきた進化の道を一旦引き返し、現代のようにブラウザ互換性にそこまで問題がなくなった状況からもう一度進化をはじめたとき、別のやりかたが見えてくる気がする。

最初期に必要とされたライブラリであるjQuery は、ブラウザ間の互換性を吸収する必要があったからこそ広く使われたことは間違いない。 これは現在はさほど問題にならない。babel や polyfill が必要ではあるけれど、ブラウザのAPIはそのまま使える状況になった。

また、jQueryの、よりショートハンドでDOM操作が記述できてかっこいい、という魅力的な面は、ファッション的な良さの域をそこまで出ていないと思う。 もちろん、コードにおけるファッション的な良さは気分的にはとても重要なことには変わりないけど。

jqueryに限った話ではなく、独自のHTMLテンプレを書くのようなものも、コード量がかなり減ることは間違いない。

ただし、近頃は、「暗黙の動作をしようが何しようがとにかくコードが短く書ける」ことの価値が昔ほど高くなくなってきている。それによって支払う代償が高くつくなら、多少コードの行数の面で不利だとしても、明示的なインターフェイスが正しい。という価値観になってきてないだろうか。

Webプログラミングの仕事をして10年くらいになるのでなんとなくそれはわかる。 証拠としてはすこし弱いが、TypeScriptがけっこう人気になっている点が挙げられる。コード量が多少増えてもコンパイル時に型安全が保証されるメリットが遥かに上回るとされる価値観。最近の静的型な言語は、型推論によってファッション的にそこまで悪くないということがあるけど。

大きく変わったのは、アプリケーションというものが多様化したことだと思う。 プロジェクトごとに、作りたいものの完成図に開きがありすぎるので、それら全ての需要に応える、「皆が欲しかった暗黙の動作」というものがない。フロントエンドになればなるほどこの傾向が強い。だから、これさえ使っていればだいたいうまくいくというフレームワークはない。

僕が想像している将来のjsフレームワークは、標準DOM APIをそのまま使いつつ、コードを構造化してくれるようなもの。

// たとえば、
class HogeComponent extends Component {
  constructor() {
    super()

    // Viewレイヤで、標準DOMElementオブジェクトが安全かつ自動で参照できる    
    this.hogeField.value = 'hoge' 
  }
}

こうしておくと、たとえばデータバインディングのようなものも、ふつうにjsで書けばよく、暗黙のDSLのようなものでifとか書く場合と比較してパフォーマンスが良く、動作も明示的。

// Rx ならこんなかんじで書けますね
observable
  .subscribe(v => {
    this.hogeElement.value = v
  })

考えてみると、他のGUI環境ではこうしたアプローチが当然だ。とにかく標準のViewオブジェクトはラップしてから使う。というjsの価値観はすこし奇妙と言うこともできる。

標準DOM APIをさわるようなViewレイヤをつくることはいくつか反論が考えられる。

  • ユニットテストが書けないのでは?
  • HTMLテンプレの上にさらにViewレイヤができてしまい冗長では? 
  • クライアントサイドレンダリングをきれいに書けないのでは?

ユニットテストについては、上のようなインターフェイスであれば、スタブをつくるのはそこまで難しくない。あるいは、ViewModelレイヤを別でつくってそこを重点的にテストする。

HTMLテンプレの上にさらにViewレイヤができるのはたしかに冗長だが、iOSのViewControllerと同じようなものと思えば慣れそう。

クライアントサイドレンダリングを標準DOM APIがサポートするまでは仮想DOMを併用するのが現時点では良いとおもっている。WebComponentsに期待。

そんなかんじです。

Unity で 等角タイルマップエディタつくってる

vimeo.com

Unityのすばらしいところの1つはやっぱりエディタ。なにせ、Sceneビュー上でコンパイルせずに変数の値をいじることができるし、実行中ですらできる。 ゲームとか、インタラクティブな何かをつくっている最中は、実際に動いてるようすを見ないと良いか悪いか判断できないので、編集して見た目を見るまでのスピードで生産性が変わってくる。コードを変更した後、確認する方法がコンパイルし直すしかないのはちょっと効率が悪い。Unity や c#界隈はその辺がすごく進んでいる気がする。

ななめ見下ろし型の2Dゲームをつくっていて、マップの編集ツールもUnityのエディタを拡張してつくっている。 マップをUnityのシーンビューでいじれると、外部ツールをつかうよりも効率が良いと思ってる。

つくったマップはPrefabで保存して使いまわそうと思っている。なにかしらのフォーマットで保存してC#でデコードするより、UnityネイティブなPrefabで保存しておいたほうが、より低レイヤーなところで読み込みが実装されていると思うのでパフォーマンス的に有利だと予想している。

と、ここまで作って気づいたけど、こういうマップエディタが将来Unity標準で同梱されるらしいので捨てるかも。