この記事は Qt Blog の "QML Scene Graph in Master” を翻訳したものです。
執筆: Gunnar Sletta 2011年5月31日
5月の終わりに qtdeclarative-staging.git#master に QML Scene Graph を取り込みました。さぁ、モジュール化された Qt 5 リポジトリ からソースコードを取得して、ハックをはじめましょう。
この基本的な使用方法は qmlscene
を実行することです。もしくは、QSGView を使用して qml ファイルを実行することも可能です。import する際の名前が QtQuick 2.0
にアップグレードされていますので、qml ファイルも書き換えてください。qml ファイルを書き換えていない場合には、qmlscene
は QtQuick 1.0
のファイルを 2.0
としてロードしようとするでしょう。QDeclarativeItem
ベースのプラグインはロードされないでしょう。
QML Scene Graph の紹介のために以下の動画を作成しました。
この動画で使用しているソースコードは こちらから 入手可能です。このデモでは QML プレゼンテーションシステム を使用しています。
コメントで聞かれるよりも前に、なぜ既存のシステムを使わないのかという質問に答えましょう。我々は軽量でコンパクトなモジュールで QML を動作させたいと考えました。シーングラフのコア部分は(クラスのドキュメントも含めて)1万行以下のコードで、我々のユースケース向けに作られています。それ以外は Qt と QML を繋ぐ部分です。書かれたコードは全てのプラットフォームで共通です。既存の技術の上にはこのようなスリムなものを実現することはできませんでした。
免責: この記事の内容を最終的な仕様として受け取らないでください。今現在の状態を説明しているだけで、将来変更される可能性があります。
QML Scene Graph は Qt と QML を最適に動かすための描画システムのコア技術の変更であり、新しい機能はそんなに多くありません。少し前に同僚と一緒に現状の描画システムに関する いくつかの記事 を書き、その中でいくつかの問題点にも言及しました。一番最初の シーングラフの記事 では、私たちがどのようにそれらの問題に取り組んで行くのかを説明しました。
QML のチームは現在新しいパーティクルのシステムなどの QML の機能拡張をしていますが、彼らには準備が整い次第それらについての記事を公開するように言うつもりです。
我々は、API を2つの異なるタイプに分けました。アプリケーション開発者によって使われるパブリック API と、システムインテグレーターが使用するバックエンド API です。パブリック API には QML を描画するために必要な全てのクラスが含まれます。新しいプリミティブや独自シェーダー等を導入するための API の他、低レベルの API を使用する便利な API をいくつか提供します。ドキュメントに記載されている全てのファイルはパブリック API になります。ここで、そのドキュメントへのリンクを貼ろうと思ったのですが、モジュール化されたリポジトリではまだドキュメントの自動生成が行われていないため、後回しにします。
バックエンド API にはレンダラやテクスチャなどの実装が含まれます。これによりこのシステムのいくつかの部分を、ハードウェア毎に必要に応じて最適化ができるようになります。バックエンド API はプライベートヘッダの中にあります。その中のいくつかはパブリック API になるかもしれませんが、そのために必要な API の確定にはまだ時期尚早だと思っています。
基本的にシーングラフとは定義済みのノードで構成される1つのツリーです。描画はそのツリーのジオメトリノードを配置することで行われます。ジオメトリノードとは描画する頂点やメッシュのジオメトリと、そのジオメトリをどう処理するかを定義するマテリアルで構成されます。マテリアルは基本的には QGLShaderProgram にロジックをいくつか追加したものになります。
ノードを描画する際には、レンダラがツリーを取得し描画します。パフォーマンスの改善のため、レンダラは描画が正しいことを保証した上でそのツリーを自由に解釈することができます。デフォルトのレンダラでは非透過のジオメトリと半透明のジオメトリを別々に持ちます。非透過のジオメトリが先に描画され、ステートの変更が最小限になるようにマテリアルの順番が計算されます。半透明のジオメトリは色バッファと深度バッファの両方に描画されます。アイテムの深度はグラフの中の元の順序によって決定されます。半透明のジオメトリを描画する際には深度のチェックを有効にし、非透過のジオメトリが半透明ジオメトリより手前に描画される場合には GPU は透過ピクセルに対して何もしないようにしています。デフォルトのレンダラには非透過のジオメトリを厳密に手前から奥に向かって描画するスイッチ(現在は -opaque-front-to-back
オプションを qmlscene に指定することで有効になります)があります。バックエンド API を使用して、別のアルゴリズムでツリーを描画するようなカスタムレンダラを実装することもできます。
シーングラフは1つの OpenGL コンテキストで描画を行いますが、それを生成したり所有したりはしません。様々なコンテキストが使用可能で、QSGView の場合には QGLWidget の派生クラス由来のコンテキストを使用しています。今年の夏以降に予定されている master でのグラフィックスタックの差し替え後は、OpenGL コンテキストはウィンドウ由来のものになるでしょう。
QML Scene Graph はスレッドとは無関係なため、OpenGL コンテキストが関連づけられていればどんなスレッドでも動作可能です。しかし、シーングラフがコンテキスト/スレッドへ設定された後にはスレッドを移動することはできません。初めは、QML のアニメーションと全ての OpenGL の呼び出しを描画専用のスレッドで行おうと思っていましたが、QML を動かす仕組み的にそれは実現できないということが分かりました。QML のアニメーションは QML のプロパティに作用する機能で、このプロパティは QObject で C++ で実装されています。もし描画スレッドでアニメーションを実行し C++ のコードを呼び出すとなると、同期地獄に陥るでしょう。このため、別のアプローチを取ることにしました。
OpenGL のコンテキストと全てのシーングラフ関連のコードはドキュメントに明示的に記載されていない限りは描画スレッド(Render Thread)で動作します。アニメーションは GUI スレッド(GUI Thread)で動作しますが、描画スレッドから送られたイベントによって操作されます。1つのフレームの描画が始まる前に GUI スレッドを一瞬止めて、QML ツリーとその変更をシーングラフにコピーします(Synchronize Graph)。シーングラフはその時点での QML ツリーのスナップショットを表します。ユーザー API で見ると、これは QSGItem::updatePaintNode() の中で起こります。これを図で示すと以下のようになります。
このモデルの利点は、現在のフレームの描画を描画スレッドの中で実行している間にも、次のフレームのためのアニメーションの計算や GUI スレッド上での JavaScript のバインディングの評価ができることです(Advanced Animations Frame)。アニメーションを先に進め、JavaScript の評価をするのは一般的に描画(Render Frame)よりも時間がかかりますが、この処理は描画スレッドが次の vsync シグナルまでの間にブロックしている間(Swap and wait for vsync)にも実行されることになります。シングルコアの CPU であっても、描画スレッドが vsync シグナルを待っているアイドル状態でもアニメーションを先に進めることができます。これは一般的に swapBuffers() で行われます。
QML Scene Graph 自体は QPainter を使用しませんが、使用する方法はいくつかあります。真っ先に思い浮かぶ方法は QSGPaintedItem
クラスを使用することです。このクラスはユーザーの要求に応じて QImage か FBO(FrameBuffer Object) に対して QPainter をオープンし、upadte() により描画のリクエストがあった際に仮想関数 paint() を呼び出します。このクラスが QDeclarativeItem がシーングラフで動作するようになった際の移植の第一候補になります。この2つのクラスの API は同じですが、内部の動作は少し異なります。paint() 関数はデフォルトでは "sync" フェイズの間に呼び出され、この間 GUI スレッドはスレッドに関わる問題を回避するためにブロックされます。しかし、GUI スレッドとは分離した描画スレッド上で実行するように切り替えることも可能です。
もう1つの選択肢は FBO や QImage に自分で描画をし、その結果を QSGSimpleTextureNode などに追加することです。
OpenGL と一緒に使うための方法は3つあります。
今現在、アニメーションを追跡するためにいくつかの環境変数を用意しています。
この2つを組み合わせることによって、現在の状態で裏に隠れて行われているアニメーションを調査することができます。
これらの新しいものを使う際には、バグを報告したり、機能不足を指摘したり、改善の提案などをしていただけると助かります。それぞれ正しい場所は以下の通りです。
ここでいくつかの数字をシェアしようと思います。以下の数値は demos/declarative
以下にある photoviewer のデモを QML 1 上で、ラスタ、OpenGL、LLVMpipe を使用した Mesa によるソフトウェアでの描画と、QML 2 上で、OpenGL、Mesa/LLVMpipe で実行した結果になります。Intel Sandy Bridge i7-2600K で、オンボードの Intel HD Graphics 3000 GPU を使用し、Qt 5 の HEAD を Linux 上で XCB のバックエンド(ラスタと OpenGL の描画エンジンでは 4.8 での X11 と同じになります)を使用して実行しました。
上記のグラフでお分かりのように、 QML 1 の描画スタックと比較すると QML Scene Graph では、任意の QML のサンプルで全体的に 2.5 倍ほど高速になっています。もう1つ興味深い部分は、LLVMpipe が我々のソフトウェアのラスタエンジンと同じレンジにあり、実際は少しそれよりも高速だということです。これは LLVMpipe が基本的には全く同じようなことをしようとしているので、驚くことではありませんが。QML 2 のマルチスレッドの LLVMpipe 版は QML 1 を OpenGL で動かした場合よりも高速です。これらの結果が Qt 5 が OpenGL に依存するということに対する懸念を払拭する助けになればと思います。