2009-08-22

近況

WEB+DB PRESS の連載はなんとか打ち切りにならず続行中. 読んでもらえているとのフィードバックもいくつかいただき, ありがたいことでございます. 今回は CSS 周辺を読んでみました. ようやくブラウザらしくなってきたので興味のある方はごらんください.

記事の準備と称してコードを読む週末副業も今のところ続行中. 遠くに行くのがだんだん億劫になってきたので, 近所の安コーヒー屋に通っている. ひきこもり体質の改善はならず.

連載はこのあとレイアウト, レンダリングと続く予定. その準備にレンダリングまわりのコードを読んでいたら, 連載で書くにはややしんどいけれど WebKit らしいコードをみかけた. 今日はその話を書いてみたい.

Core Animation - Safari の隠し味

CSS に CSS 3D Transforms Module という 策定中の標準があり, WebKit はこれを実装している.

私はビジュアルな美的感覚を欠いているので, この機能の有難味をよくわかっていなかった. (標準の存在も, 派手なデモ を見て驚くまで 気にとめてすらいなかったことを白状しておく.) 一方でビジュアルな美的感覚にうるさい Apple はこの機能に熱心だ. この標準にしたって, 見てのとおり Apple の開発者が書いている.

ビジュアル音痴なりに類推すると, Apple が 3D Transforms で実現したいのは Cover Flow や ニョッキリ Dock に代表される Apple ぽい UI なのだと思う. Apple はこれらの派手な UI を実現する Mac OS の下地づくりを地道に進めており, 一般のアプリケーション開発者向けにも 多くの API を公開している. そうしたミタメ API の一つが Core Animation. 最近の Safari/WebKit はこの Core Animation にレンダリング機構を統合し, ウェブアプリケーションの派手化を推し進めている...というのが本日の趣旨でございます.

この話は何人かに喋ってみたんだけれどいまいちピンときてもらえなかったので, 自分の口頭雑談能力に見切りをつけてここに書いてみることにした.

ディスプレイリスト+ツリー

例のごとく復習から.

誤解を恐れつつ平たくいうなら, Core Animation は Flash みたいなものだと思えばいい. もう少し正確にいうと, Flash Player によく似た ディスプレイリスト+ツリー のレンダリングモデルを Mac OS と Objective-C の世界に持ち込んだものだと言える.

ディスプレイリスト+ツリーのレンダリングモデルでは, ひとまとまりの描画単位をノードに割り当て, それらのノードをつなげてツリーを作る. Flash Player では DisplayObjectSprite クラス, Core Animation では CALayer クラスがツリーのノードをあらわしている.

プログラマは各ノードの描画コールバックに自分の描画コードをセットする. そうしたアプリケーションレベルの描画コードが実行されると, 各ノードのディスプレイリストが更新され描画コマンドが蓄積される. まだ画面は更新しない.

Flash Player や Core Animation のランタイムは, 適当なタイミング(アニメーションのフレームや, ウィンドウの再描画)で ツリーをトラバースし, 各ノードのディスプレイリストを実行する. 実際の画面はこのとき更新される.

ディスプレイリストを評価する際には, 透明度や回転, スケーリングなどの特性を適用することができる. つまりノード単位で透明度を変えたり, 回転拡大縮小などの 3D 効果を加えることができる.

素朴なディスプレイリスト+ツリーは, だいたいこんなかんじで動く.

ハードウェアの支援

ディスプレイリスト+ツリーのモデルは GPU との親和性が高く. 性能上の利点がある. たとえばディスプレイリストの描画コマンドを GPU 側に置き, CPU 時間を浪費せずに描画ができるかもしれない. また写真など描画に使う素材画像をテクスチャとして VRAM に置けば, バスへの負担は下がるだろう. ノードの中身を変えない再描画はディスプレイリストを評価するだけで済む. アプリケーションの描画コードは経由しないぶん速くなる. たとえば各アルバム画像をノードに割り振り Cover Flow 風にページをめくるアニメーションは, 典型的な "ノードの中身を変えない再描画" になるだろう.

ノードに半透明表示の指定をもつ描画も, GPU 支援を受けるディプレイリスト+ツリーに分がある. ノードが半透明指定を持つとき, そのノードの描画結果は 一旦独立したオフスクリーンのビットマップに描画され, そのビットマップに透明度を掛けて最終的な画面に合成する. そのビットマップが VRAM 上にあれば, 画面の合成は GPU を頼れる.

ビットマップ+ツリー - Flash Player の場合

さて, 上で説明した素朴なモデルは GPU への要求が高く, また空間効率も良くはない. そうした問題を回避するため, 現実の実装は何らかの工夫をしているだろう. Flash Player の例を考えてみよう.

先に ディスプレイリスト+ツリー のモデルは Flash Player に似ていると書いた. Core Animation と異り, 従来の Flash Player は ディスプレリスト+ツリーを CPU 側だけで実現していた.

Flash Player のバージョン 10 は DisplayObject の 3D 変換をサポートし, 同時に GPU の積極的な活用をはじめた. つまり Flash Player はディスプレイリスト+ツリーを GPU 側に推し進めた...と 思いたいところだけれど, どうも事情は違うらしい.

中の人によると, Flash Player が使う GPU 支援の範囲は画像合成の計算に限られているようだ. つまりディスプレイストや素材画像は主記憶に置いて CPU で評価する. そして描画結果のビットマップを VRAM に転送, GPU で合成するのだろう. 主記憶 -> VRAM の転送コストは気になる(上の blog にも "速くなるとは限らない" とある)けれど, ノード (DisplayObject) 毎の描画結果を使い回せるなら 3D 変換などの描画効果は GPU で速くなる. また リリースノート に見られる bitmap cache 云々の記述や上の blog にある描画結果の乱れに関する注意書きから, 描画結果を使い回す工夫をしている様子もある.

今日の主役は Core Animation と GPU 支援なので, ノードを描画したビットマップだけを VRAM 側に置く Flash Player 10 のモデルは ビットマップ+ツリー と呼んでおこう. 既存のコンテンツが多く, また実行するハードウェアを選り好みもできない事情があるから, Adobe はソフトウェア側に倒した折衷案としてビットマップ+ツリーを選んだのだと思う. 当面は GPU を頼らないと腹をくくったのか, CPU のコア数増加に合わせた描画の並列化を進めている (とリリースノートに書いてあった).

ゼロ+ツリーからディスプレイリスト+ツリーへの移行 - Cocoa の場合

Mac OS X 10.5 から導入された新しい API である Core Animation に下位互換性の問題はない. 対象ハードウェアも最新のものが相手だから性能に妥協する必要もない. Core Animation が先に示したベタなディスプレイリスト+ツリーのモデルに近いのは, そんな背景がある. (ただし Quartz の描画機能すべてを GPU だけで描画できるわけではないらしい. Mac OS Internals によると, いくつかの複雑な描画はソフトウェアに戻って行うという.)

Core Animation は独立した API からのみ利用できるわけではなく, GUI フレームワークの Cocoa にも統合されている. 従来の Cocoa はウィンドウ内にあるウィジェット(ビュー)のツリーに特別な資源を割り付けない, いわば ゼロ+ツリー の描画モデルだった. 今はそれが Core Animation に統合され, NSView の特別なメソッドを呼び出すと Cocoa のビューツリーに Core Animation のツリーが割り付けられる. 具体的には該当ビューをルートとするビュー部分木の各ノードに レイヤツリーのノードが与えられる. つまりそのサブツリーはディスプレイリスト+ツリーモデルに切り替わる. Apple はこの仕組みを Layer Backed View と呼んでいる. (紹介資料.) Layer Backed View のウィジェットには回転やドロップシャドウなど Core Animation 由来のエフェクトを適用することができる. Core Animation との統合を通じて既存のアプリケーションもミタメをよくしろということなんだろうね.

追記: この節の内容は間違っていました. 記事末尾を参照ください.

Safari/WebKit と Core Animation の統合

うっかり復習が長びいてしまった. ようやく本題.

今年二月頃, Apple の開発者は accelerated compositing と銘打って Core Animation を WebKit に統合した. (Bug 23360 - Make repainting compositing-aware for accelerated compositing.) コンパイルオプションで #if USE(ACCELERATED_COMPOSITING) を有効にすると, WebKit の描画は Core Animation に統合されたパスに入る.

WebKit は内部にページ単位の描画用ツリー(=Render Tree)を持っており, 従来の描画ではこのツリーをトラバースする. 詳しい話はそのうち連載に書くから省略するとして, 大ざっぱにこんな感じだと思えばいい:

// クラス名などは架空のものです

void Document::paint(GraphicsContext* ctx)
{
  m_rootNode->paint(ctx);
}

void RenderNode::paint(GraphicsContext* ctx)
{
  this->paintThisNode(ctx);
  for each (c in this->children)
    c->repaint(ctx);
}

このとき GraphicsContext は WebKit の利用者が与える. ウィンドウの場合もあるし, オフスクリーンの場合もある.

Document::paint() はページ全体を引数の GraphicsContext に描画する. 当然, 各 RenderNode はオフスクリーンやディスプレイリストといった描画資源を持たない. そういう意味で WebKit は ゼロ+ツリー モデルに従っている. GraphicsContext のように描画を抽象化したオブジェクトを渡すこの切り口は, 描画を伴うライブラリの定番と言って良い.

さて, どうすればゼロ+ツリーの WebKit をディスプレイリスト+ツリーの Core Graphics に 統合できるのか, 一見するとあまり自明でない. WebKit を Cocoa, RenderNode を NSView に見たてた上で, Layer Backed View に倣い RenderNode(仮名)インスタンス毎に CALayer を割り当てる方法も考えられる. けれど RenderNode(仮名) のインスタンス数はおよそ DOM ノードの数に比例する. その全てに CALayer を割り当てるのはオーバーヘッドが大きい. もうすこし疎な粒度で割り付けたい.

WebKit におけるレイヤ

Apple にとっては好都合なことに, WebKit も内部にレイヤ構造 RenderLayer を持っていた. accelerated compositing 拡張は Core Animation の CALayer を RenderLayer 毎に割り付けている.

この RenderLayer クラスは昔からあるもので, Core Animation とは関係がない. RenderLayer はもともと, RenderNode::repaint() のウソコードで示したような深さ 優先のトラバースで実現できない描画を行うために追加された. Render Tree 上で表現されていない描画順や暗黙の親子関係を表現するために, WebKit は RenderLayer のツリーを作る. 各 RenderLayer は, 通常のトラバースで描画できる Render Tree のサブツリーをあらわしている. 木の中に木があるのはちょっとややこしいけれど, ブラウザのコードではよくある話.

ひとつの RenderLayer が受け持つ各サブツリーの中は通常のトラバース順で描画され, RenderNode(仮) が責任を持つ. サブツリー間の描画順は RenderLayer が決める.

定義はこんなの:

class RenderLayer : public ScrollbarClient {
public:
  ...
    // 描画メソッド
    void paint(GraphicsContext*, const IntRect& damageRect, ...);
  ...
private:
  ...
    // 対応元の Render Tree ノード - 描画単位となるサブツリーのルート
    RenderBoxModelObject* m_renderer;
    // ツリーの枝
    RenderLayer* m_parent;
    RenderLayer* m_previous;
    RenderLayer* m_next;
    RenderLayer* m_first;
    RenderLayer* m_last;
  ...
};

Render Tree と RenderLayer

さて, RenderLayer はどんな単位でインスタンス化されるのだろう. "通常のトラバースで描画" できるのは, どういうサブツリーなのだろうか. 一旦 Core Animation から離れ, RenderLayer のコードを眺めてみよう.

各 Render Tree ノードに対し RenderLayer のインスタンス化を判断する RenderBoxModelObject::requiresLayer() メソッドはこう定義されている:

...
class RenderBoxModelObject : public RenderObject {
    ...
    virtual bool requiresLayer() const { return isRoot() || isPositioned() || isRelPositioned() ||
                                                isTransparent() || hasOverflowClip() || hasTransform() ||
                                                hasMask() || hasReflection(); }
    ...
};
...

ツリーのルート isRoot(), 透明度つきのノード isTransparent(), 描画のクリッピングが必要なノード hasOverflowClip() と hasMask(), 件の transform CSS プロパティを持つもの hasTransform() などに RenderLayer が作られるとわかる.

透明度つきの Render Tree ノードは RenderLayer の必要性を端的に表している. 透明度つきのノードをルートとするサブツリーは, まず独立したビットマップに描画される. それからビットマップに透明度をかけて全体の描画結果に合成される. RenderLayer は, 独立して描画したいこのようなサブツリーをあらわす.

isRelPositioned() や isPositioned() もわかりやすい. "position:relative" や "position:absolute" のブロックは通常のフローレイアウトと独立したレイアウトが行われ, 従って描画の順番も変化する. z-index プロパティで明示的に描画順を指定されることもある. これらのノードを含む Render Tree が深さ優先トラバースで描画できないのは明らかだ. これら特別な position を持つノードのサブツリーも RenderLayer になる. RenderLayer は子のレイヤを z 順にソートして描画する.

4 年前の requiresLayer() を 覗いてみると, いま示した 合成の都合による一時的なオフスクリーンバッファの確保レイアウトの都合による描画順の制御 という RenderLayer の意図が, よりはっきり現れていた.

...
bool RenderObject::requiresLayer()
{
    return isRoot() || isPositioned() || isRelPositioned() || style()->opacity() < 1.0f ||
           hasOverflowClip();
}
...

RenderLayer から CALayer へ

Apple は, この RenderLayer を中心に Core Animation を統合した.

accelerated compositing を有効にした RenderLayer には, 統合に伴う各種面倒を押し込んだ RenderLayerBacking オブジェクトが追加される

...
class RenderLayer : public ScrollbarClient {
public:
   ...
#if USE(ACCELERATED_COMPOSITING)
    OwnPtr<RenderLayerBacking> m_backing; // これ
#endif
   ...
};
..

RenderLayerBacking は CALayer のラッパオブジェクト GraphicsLayer をもつ.

// rendering/RenderLayerBacking.h
...
class RenderLayerBacking : public GraphicsLayerClient {
public:
    ....
    // GraphicsLayer がいくつもある事情はコメントを参照.
    OwnPtr<GraphicsLayer> m_ancestorClippingLayer; // only used if we are clipped by an ancestor which is not a stacking context
    OwnPtr<GraphicsLayer> m_graphicsLayer;
    OwnPtr<GraphicsLayer> m_foregroundLayer;       // only used in cases where we need to draw the foreground separately
    OwnPtr<GraphicsLayer> m_clippingLayer;         // only used if we have clipping on a stacking context, with compositing children
    OwnPtr<GraphicsLayer> m_maskLayer;             // only used if we have a mask
};

GraphicsLayer はいちおう環境非依存ということになっており, 実体である mac 版のサブクラス GraphicsLayerCA が Core Animation 由来の CALayer オブジェクトを持っている.

// GraphicsLayerCA.h
class GraphicsLayerCA : public GraphicsLayer {
public:
    RetainPtr<WebLayer> m_layer; // WebLayer は CALayer のサブクラス
    RetainPtr<WebLayer> m_transformLayer;
    RetainPtr<CALayer> m_contentsLayer;
};

GraphicsLayerCA が持つ 3 つの CALayer ファミリのインスタンス は m_transformLayer -> m_layer -> m_contentsLayer という親子関係をつくっている. たぶん Safari 側の都合で設定したい属性と WebKit 側からの属性をうまく混ぜるための トリックだと思うけれど, 詳細はよくわからず.

とにかく, こうして RenderLayer と CALayer, WebKit と Core Animation は繋がっている. CALayer のツリー構造は, GraphicsLayer を介して RenderLayer のツリー構造と同期される. ツリー構造の保守は RenderLayerCompositor が行う. RenderLayer はツリー構造が変更される様々なタイミングで RenderLayerCompositor に 構造の変更を通知し, RenderLayerCompositor が GraphicsLayer を調整する.

例:

void RenderLayer::addChild(RenderLayer* child, RenderLayer* beforeChild)
{
....
#if USE(ACCELERATED_COMPOSITING)
    compositor()->layerWasAdded(this, child);
#endif
}

このように若干なげやり感ただようコードが RenderLayer の各所に埋め込まれている. 実際の同期コードは遅延とか色々ややこしいことをやってそうなので割愛させていただきます.

なお, 全ての RenderNode(仮) に RenderLayer が作られるわけではないように, 全ての RenderLayer に RenderLayerBacking が作られるわけでもない. RenderLayerBacking を持つ RenderLayer は self painting layer と呼ばれ, 以下のような条件を満たしている.

....
bool RenderLayer::isSelfPaintingLayer() const
{
    return !isNormalFlowOnly() || renderer()->hasReflection() ||
           renderer()->hasMask() || renderer()->isTableRow() || renderer()->isVideo();
}
....

normal flow でない (特別な position プロパティを持ち, transform プロパティを持つかもしれない)ノード, <video> 要素のノード, などが該当している. そのほか mask とか reflection は webkit 拡張. opacity が考慮されないところ, table の行が入っているところは意外.

描画シーケンスの変更

ゼロ+ツリーからディスプレイリスト+ツリーへの変更は, 描画コードの実行順序にも大きく影響する. 従来の WebKit は, ページを表示しているウィンドウ (Mac OS だと NSView) からの要求に応じてページ全体を再描画していた. 描画時は再描画範囲をあらわす矩形が与えられるから実際に全画面を書き直すわけではないけれど, Render Tree をルートからトラバースして個々の RenderNode を描画しようとするのは確かだ. Mac OS での描画結果は NSView の GraphicsContext に書き込まれる.

accelerated compositing パッチでは, 描画を RenderLayer 単位で行う. つまり RenderLayer 毎に割り付けられた CALayer に描画を行う. 描画は CALayer から要求される.

結果として, accelerated compositing を有効にした WebKit は従来の描画パスで何も描画しない.

void RenderLayer::paintLayer(RenderLayer* rootLayer, GraphicsContext* p,
                             ...)
{
#if USE(ACCELERATED_COMPOSITING)
    if (isComposited()) {
        // The updatingControlTints() painting pass goes through compositing layers,
        // but we need to ensure that we don't cache clip rects computed with the wrong root in this case.
        if (p->updatingControlTints())
            paintFlags |= PaintLayerTemporaryClipRects;
        else if (!backing()->paintingGoesToWindow() && !shouldDoSoftwarePaint(this, paintFlags & PaintLayerPaintingReflection)) {
            // If this RenderLayer should paint into its backing, that will be done via RenderLayerBacking::paintIntoLayer().
            return; // CALayer に描画する場合はこのパスに入る.
        }
    }
#endif
    .... ここから先が従来の描画パス
}

実際の描画コードは RenderLayerBacking::paintIntoLayer() にある.

// Share this with RenderLayer::paintLayer, which would have to be educated about GraphicsLayerPaintingPhase?
void RenderLayerBacking::paintIntoLayer(RenderLayer* rootLayer, GraphicsContext* context,
                                        ...)
{
    ...
}

冒頭のコメントから少しは罪の意識が伺える, 気もする.

条件によっては従来の描画パスも使われるのがややこしい. "DRY じゃない" などと甘ったれたことを言わないのが WebKit 流とはいえ, 描画バグの原因究明は大変に違いない. 読んでいるだけで肝を冷やした.

各ポートへの影響

Qt や Gtk, Cairo, Chromium(Skia) など, WebKit にはいくつかのポートがある. おそらく accelerated compositing に一番肝を冷やしたのは, 各ポートの開発者ではないかと思う.

Core Animation や Flash に相当する ディスプレイリスト+ツリー(またはビットマップ+ツリー)のオブジェクトモデルを持つ描画処理系は, それほど多くない. GPU 支援を合わると尚更敷居は高くなる. 同じ WebKit なのに Safari の CSS 3D Transform だけがガンガン動き, 自分のポートではコマ落ちしてしまう...なんてことになったら, 顧客や上司にいびられやしないか.

調べてみると, Qt は一応 OpenGL に統合されているようす. OpenGL の描画とウィジェットを合成する チュートリアル があった.

GTK にんな大層な機能があると聞いたことはない. ただ GTK 由来の Glib を使った Clutter Toolkit は ディスプレイリスト+ツリーの API を提供している. GTK インテグレーションのデモもある. 開発元の OpenedHand は Intel に買収されており, Clutter Toolkit は Moblin のミタメ強化を期待されているのだろう. WebKit+GTK が将来的に WebKit+Clutter に移行してもそれほど不思議はない. (開発は進んでいるらしい.)

Cairo, Skia はベクターグラフィクスの描画ライブラリという出自もあって Core Animation に相当するレイヤはない. Skia にはちょっとした GUI ツールキットが付属しているようだけれど, ちょっとみたかんじゼロ+ツリーの実装だった.

Cairo はさておき Skia ...というか Chromium と Android ポートにとっては少し面倒な事態かもしれない. 性能を売りにしている以上は CSS の 3D Transforms も高速に動かしたいけれど, 現状は Safari+Core Animation のような GPU 支援がない. Skia をチューニングするのか, accelerated compositing に必要なパーツを揃えるのか, 別の路線(並列化とか)で対抗するのか.

各ブラウザの CSS3 実装が出揃った頃, レンダリング性能のベンチマーク競争が始まるだろう. その時 Chromium や(今回の話とは関係ないけど) Mozilla がどんな手を打つのか, 個人的にはとても興味を持っている.

それ以前に CSS3 の派手なエフェクトは流行るのか, という疑問もある. きっと iPhone の Safari 向けにキュンキュン動くウェブアプリが登場して人気を博し, (ついでに開発の面倒な iPhone アプリは下火になり,) 競合の Android や各種ケータイ向けブラウザが追従, デスクトップにもその流れが及ぶ...という展開が自然だけれど, Flash も Silverlight もあるから, どうなるんだろうね.

雑感

個人的には, 複雑な描画の要件とややこしいコードベース持つブラウザを, 近代的で GPU フレンドリな構造に乗せかえたApple (というか Simon Fraser) の手腕に, 肝を冷やしつつ感動した. ブラウザの描画は文字が多くコンテンツの構成も多様で, Flash のように描画中心のモデルに比べて高速化はやりにくいだろうと想像していた. だから JavaScript の性能競争が一段落したあとにやってくるであろうレンダリングの性能競争で どんなアイデアが現れるのか, 外野なりに興味を持ってみていた.

Flash の並列化が念頭にあったので, 何らかの並列化を漠然と想像していたけれど, WebKit のように並列性を考えていないコードでそれをやるのも難儀だろうし, 並列化の粒度も良い切り口がなさそうに見えた.

そんな難題に対し, 彼らは. CSS Transforms と Core Animation を軸に RenderLayer という絶妙な切り口を掴み, ややこしいツリーのジャグリングをこなし, レガシーなレンダリングモデルを一気に近代化した. その力技にびびらずにはおれない.

RenderLayer はもともとの KTHML にはなかったもので, Mozilla 出身の WebKit 開発者 David Hyatt が Gecko のコードベースから ひっこぬいてねじこんだという経緯がある. (だから RenderLayer.h/cpp には Netscape の Copyright があり, MPL が併記されている.) Simon Fraser による実装の背後には David Hyatt との議論なんかも あったんだろうなーなどと想像し, やっぱり最後は Mozilla なのか, と思ったりもした.

まとめ

というわけで WebKit の Core Animation 統合について書いてみました. たぶんぜんぶ読んだ人はいないと思うのでまとめ:


訂正 - 現行の Core Animation は ビットマップ+ツリー

Wikipedia を見ていたら, Quartz の OpenGL アクセラレーションは現行の Mac OS では無効化されているらしく, 確認したところ Core Animation も DisplayList や VBO は使っていなかった. Core Animation は (おそらくソフトウェアで) ラスタライズを行い, それをテクスチャとして VRAM に置く. つまりディスプレイリスト+ツリー ではなく ビットマップ+ツリー の実装だった. 大ウソついてごめんなさい. QuartzGL の話と混同してました...

参考までにウソだと確かめる方法を紹介:

アップロード前にやっとけという指摘にうなだれつつごめんなさい.

ポートへの影響についても, ソフトウェアラスタライザでいいなら Core Animation 相当を用意するのは割と現実的なのかも.