Servo で WebVR できるまで (その1)

[“WebVR Coming to Servo: Part 1” の抄訳です]

VR はウェブで普通に使えるようになるべきです。そのためには、私たちの考える未来を実装するための柔軟で高速な処理系が必要です。 Servo はモダンで、ハイパフォーマンスなブラウザエンジンです。 アプリケーションとしての用途はもちろん、組み込み用途でも利用できるように設計されており、 Mozilla が 1 から実装を行なっています。 その技術的な目標とプロジェクトを取り巻くコミュニティと文化に共感したため、 Gecko (Firefox) への実装に並行して、WebVR を Servo に実装することを決めました。

Imanol Fernandez が WebVR API を初めて Rust で実装しました。 彼の実装である rust-webvr はブラウザーから独立していたため、単体でテストや利用ができました。 例えば Rust で実装された OpenGL アプリケーションが、 ブラウザーで利用できる API と同じものを使って VR ヘッドセットをコントロールできるようになります。 その次の目標は、Servo のアーキテクチャとの統合です。そのために、私たちはスクリーンに何かを表示するところから始めました。

Linux と Mac OS X では、Valve と Oculus の提供する SDK を利用できません。 そのため現在デスクトップ向け VR は Windows でしか動作しないものとなっています。 3D コンテンツをスクリーンに表示するために、Imanol は Servo に Windows 向けの WebGL バックエンドの実装を開始しました。 単純に考えて、WGL から始めることとしました。これは他のコンポーネントによって、すでに利用されていたためです。 将来的には Angle を利用したバックエンドの提供も考えています。 これはそれぞれの GPU のパフォーマンスをもっとも引き出せる選択肢となるでしょう。 VR アプリケーションにとって、低い遅延と高いフレームレートは必須の要件です。そのためには、JavaScript で WebGL の関数を読んでからスクリーンにピクセルが表示される前での間に、 何が起きているのかをしっかりと把握しなくてはなりません。 WebGL 関数の呼び出しには、Servo 内の異なるパーツが関係しています:

Servo 内の WebGL 関連モジュール

JavaScript の評価器と、IDL バインディング

Servo は JavaScript の処理系として moz-js を利用しています。これは SpiderMonkey の派生物です。 V8JavaScriptCore と同様、moz-js は JS の評価器としての機能しか持っていません。 WebGL のような API を追加するには、JS からネイティブのクラスや関数へアクセスできるようにする必要があります。 これは仮想マシン (VM) が提供する C/C++ の API を利用して実装されます。 その結果、JS のクラスを定義し、ネイティブと JS の関数を相互に呼び出すことが可能になります。   API を追加するためには、引数のチェックや JS – C++ 間のデータのやりとりなど、 似たようなコードをたくさん書かなくてはなりません。 これに加えて Servo では、C/C++ と Rust の間のバイディングコードも書かなくてはなりません。 このコードは rust-bindgen というツールを使って WebIDL と呼ばれる言語非依存な API 仕様から自動生成されます。 このツールのおかげで API のロジックに集中することができます。

Servo で API を定義する IDL はこちらで公開されています。これには WebGL も含まれます。

WebGL に関連する DOM オブジェクトの 実装

WebIDL バイディングによって Rust と JavaScript を繋げるためのコードは、自動的に生成されました。 その次は、個別の関数やクラスの実装です。実装がないとコンパイルができません。 そのため DOM オブジェクトである WebGLRenderingContext も実装しなくてはなりません。 こちらにその実装があります。 WebGLProgramWebGLShader のような、その他の WebGL クラスの実装も同様です。

これらの DOM オブジェクトは WebGL オブジェクトの状態を表していて、検証のためのルールは全て WebGL の仕様に定められています。 ただし、レンダリングコマンドの処理は全て、WebGL スレッドという別のコンポーネントへ委譲されます。

WebGL スレッド

Servo は並列処理アーキテクチャを取っています。そのため他のコンポーネント同様、WebGL のレンダリングコマンドは他の DOM の描画処理を行うスレッドや JS の評価を行うスレッドとは独立して実行されます。 これはパフォーマンスの向上には好都合でした。なぜなら GL の GPU ドライバーに対する呼び出しが JS の実行をブロックしないためです。 ただし glReadPixelsglGetParameter のような関数をを呼び出した場合は例外です。 これらの関数を呼び出した場合、JS スレッドは WebGL スレッドからのレスポンスを待つためブロックされます。 それでも十分に最適化されてた WebGL アプリにとっては問題にはなりません。

Servo の WebGL スレッドは DOM からの WebGL メッセージを受け取り、処理するように実装されています。 今の所、2つの描画パスを利用できます。どちらを利用するかは Servo の初期化方法によって決まります:

  • Webrender: WebGL スレッドは GL 関数呼び出しを Webrenderer コンポジタースレッドへ送出します。最近 Servo の標準の描画パスとなりました
  • rust-zure: 描画の抽象化レイヤーです。以前は標準の描画パスでした。グラフィックススライブラリーである Skia を内部的に利用し、スレッド内で GL 関数の処理を行います

Webrender は最適化された WebGL の描画パスを持っています。 それは 共有の OpenGL テクスチャを使ってコンポジションを行います。 一方 Skia は readPixels を用いてテクスチャを共有するため、その WebGL 描画パスは最適化されていません。

Servo と Rust のプロセス間通信

WebGL DOM は GL 描画メッセージを異なるスレッドを送出します。 Rust の標準ライブラリではスレッド間の通信には非同期のチャンネルを利用しますが、 Servo はマルチプロセス処理を向上させるため、独自に実装した ipc チャンネルを利用します。

チャンネルと Rust の強力な列挙型を組み合わせることにより、プロセス間通信はエレガントに実装できました。 WebGL スレッドで利用される WebGL コマンドは全て webnrenderer_traits に実装されています。 このコンポーネントは WebRender と Servo で共有されています。

OpenGL の呼び出し

Webrender チャンネルは GL コマンドを受け取り、OpenGL の関数を呼び出します。 ただし、この設計は将来変更されるかもしれません。その理由は GL コマンドのバッファリング機構の実装が計画されているためです。 Webrender は 1 つの GPU プロセスで、全ての WebGL コンテキストの処理を行います。

実際に GL コールを行う前にやらなければならないことがあります。 それはプラットフォームごとに異なる実際の関数の呼び出しです。 OpenGL ドライバーを呼ぶことになるかもしれませんし、 DirectX バックエンドを利用するために Angle のような OpenGL ラッパーを呼ぶことになるかもしれません。 もしくは OSMesa を利用するかもしれません。 Windows のようなプラットフォームでは、異なる OpenGL 実装を DLL として実行時にロードすることさえ可能です。 その DLL は OS によって提供されたものかもしれませんし、GPU ドライバー由来の最適化されたものかもしれません。

これらのラッパーを動かすためには、OpenGL のシンボルを動的にロードしなくてはなりません。 Servo は gleam ライブラリを利用して、同じ OpenGL シンボルを全てのコンポーネントで共有しています。 gleam は内部的に gl_generator を利用します。これは Rust コミュニティではよく使われるユーティリティで、 OpenGL の関数ポインターのローダーとバインディング機能を提供します。

これでついに OpenGL の関数呼び出しがドライバーまで到達しました。

Servo のコンポジションにおける WebGL アクセラレーション

これまで WebIDL のバインディング、DOM の実装、WebGL コマンドのマルチスレッド化、そして実際の GL 呼び出しについて述べてきました。 ここでは、WebGL canvas がメインウィンドウの描画コンテキストへ、どのように組み合わされるかを説明します。

Servo はメインウィンドウを GLutin を利用して作成します。 これはクロスプラットフォームウィンドウやメインの描画コンテキストの作成を助け、 入力やイベントの受付を簡単にするライブラリです。

WebGL コンテキストはヘッドレスなオフスクリーンコンテキストとして実装されています。 GL コマンドは テクスチャアタッチメントされた FBO を用いてテクスチャへの描画を行います。 WebGL の描画コマンドはマルチプラットフォームなものですが、ヘッドレスの GL コンテキスト作成はそうではありません。 プラットフォーム間の差異を隠蔽するために、Servo は rust-offscreen-rendering-context を利用します。 このライブラリは、ヘッドレスコンテキストの作成を抽象化し、EGL、CGL、WGL、OSMesa といった多様な API をサポートします。

Servo のコンポジターは Web ページの全ての要素の描画を管理するコンポーネントで、 GPU レンダリングには Webrenderer を利用しています。 WebGLContext は独自の描画コンテキストを持ちますが、コンポジターレイヤーとも描画コンテキスト共有されています。 共有が効率的に行えるのは、共有されたテクスチャのおかげです。