描画結果を画面に表示するまでの流れ

fadis氏:こんにちは、松林です。今日はGUIの話をします。

画面になにかを表示したい場合、それはGPUを介して行うことになります。GPUというハードウェアは、大きく分けて2つの機能を備えています。1つはビデオメモリに書かれたイメージを画面に送って表示する機能、もう1つはビデオメモリ上で計算をする機能です。

Linuxを含むマルチタスクOSでは、複数のプロセスが同時に存在しています。この中にはGPUで計算したいプロセスが複数あるかもしれません。Linuxカーネルは、複数のプロセスがGPUを使えるように、スケジューラでプロセスにGPUを割り当てます。しかし、複数のプロセスが画面になにかを表示したい時、画面にどのような表示をすべきかは自明ではありません。

Linuxのユーザー空間プロセスがGPUを操作したい時、プロセスはLinuxカーネルのDRM/KMSと呼ばれるインターフェイスを通して要求を投げます。このインターフェイスはプライマリノードとレンダーノードに分かれています。

レンダーノードに要求を投げる場合、GPUのメモリを確保したりGPUで計算をしたりすることはできますが、画面への表示に関する操作はできません。レンダーノードは、1つのデバイスファイルを複数のプロセスで同時に開くことができます。

プライマリノードは、レンダーノードでできる操作に加えて、画面に表示するための操作も受け付けます。ただし、このデバイスファイルを同時に開けるプロセスは1つだけです。

デスクトップLinuxの場合、Xサーバーのようなコンポジタがプライマリノードを占有します。X上でなにかを表示するアプリケーションはレンダーノードで描画を行い、結果をXサーバーに渡して表示してもらいます。

この流れはWaylandの場合も変わりません。Waylandコンポジタは、プライマリノードを占有し、アプリケーションはレンダーノードで描いた結果をWaylandコンポジタに渡して表示してもらいます。

Linuxカーネルはベンダーによって異なるGPUの操作方法を抽象化しないので、ユーザー空間ではしばしばMesaのようなライブラリが用いられます。MesaはVulkanやOpenGLといったハードウェア非依存のAPIで、アプリケーションからの要求を受け付け、特定のGPU向けのコマンドをレンダーノードに投げます。

Xのウィンドウに描画結果を表示するためには、Xサーバーとアプリケーションの双方から見える共有メモリに表示したいものを描く必要があります。この共有メモリをもらうために、アプリケーションはXプロトコルをしゃべってXサーバーに要求を投げます。

アプリケーションがXサーバーに対して描画の完了を通知すると、Xサーバーは共有メモリの内容を画面に送るイメージにコピーします。この時の操作もMesa等を介してハードウェア非依存のAPIで行われます。

最後にXサーバーは、プライマリノードの特権、イメージを表示にまわす機能を使って結果を画面に表示します。アプリケーションはQtやGTK+を使わずに、直接xcbやMesaと会話して描画します。

インスタンス拡張「VK_KHR_display」を使って画面を占有する方法

ここまでプライマリノードはXサーバーが占有していましたが、もしXサーバーがおらずアプリケーションがホスト内でGPUを使いたい唯一のプロセスだった場合、アプリケーションが直接プライマリノードを握って、描画結果を画面に表示できるはずです。

Vulkanには、DRM/KMSをこのように使うためのインスタンス拡張、VK_KHR_displayが用意されています。この拡張はVulkanの標準の機能ではありませんが、Linux上でvulkan-loaderを使っている場合、必ず利用可能になっています。

VK_KHR_displayを使って画面を占有してみましょう。Vulkanのインスタンスを作る際に、BK_KHR_displayを使うことを伝えます。この拡張を有効にすると、GPUに接続されているディスプレイの情報が取れるようになります。

ディスプレイは、通常縦横のピクセル数やリフレッシュレートが異なる複数の表示モードをサポートしています。getDisplayModePropertiesで対応している表示モードを調べます。今はNVIDIAのGPUにASUSの4Kディスプレイがつながっているので、3,840×2,160、およそ60ヘルツでの表示が可能になっています。

1個目のGPUにつながっている1個目のディスプレイに、1個目の表示モードで表示してみましょう。表示モードを決めると、画面に送るイメージの置き場所であるサーフェスが手に入ります。サーフェスを調べると、画面に送るイメージで使えるピクセルの形式が得られます。

30bit BGR、24bit BGR、16bit RGBの順で使える形式を探します。サーフェスには最低でも2画面分のイメージが含まれています。これは書き換え途中のイメージを表示に回さないためです。アプリケーションはサーフェスのイメージのうち今表示中でないものに描画して、描画が終わったら表示するイメージを切り替えます。

GPUには、しばしばコマンドを投げるためのキューが複数備わっています。キューの中には限られたコマンドしか受け付けないものもあります。今は描画を行いたいので、描画命令を受け付けるキューを探します。

使う物理的なGPUと、使うキューにGPUの振る舞いに関する設定をくっつけたものを、「論理デバイス」といいます。VulkanでのほとんどのGPUの操作は、この論理デバイスに対して行います。論理デバイスを作ってキューをもらっておきます。

サーフェスを構成するイメージのうち、どれが表示中でどれがアプリケーションが描ける状態かを管理するスワップチェインを作ります。画面への表示を始める前に、サーフェスのすべてのイメージをもらってきて、すべてを真っ青に塗りつぶすコマンドを用意して、GPUに投げておきます。

その後、スワップチェインから表示中でないイメージを1つもらい、内容を書き換えないで、つまり真っ青なままで表示に回します。これをディスプレイのリフレッシュレートに同期して繰り返します。実際には、Vulkanには面倒臭いGPUの設定がほかにも必要ですが、そのあたりは以前の発表で解説しているのでそちらを見てください。

実行してみましょう。今XサーバーもWaylandコンポジタも、起動していません。UEFIフレームバッファにpxelinuxからLinuxをブートした時の表示が残っている状態です。

ここで先ほどのアプリケーションを起動すると、画面が真っ青になります。

長方形・画像・文字の描画を実装

画面のコントロールを手に入れるとGUIを描きたくなります。UIは、入力デバイスからのイベントなどが来るまで内容が変化しません。

そこでUIの描画結果をイメージにキャッシュし、UIの内容が変わった時だけ再描画をするようにします。この2D描画結果をキャッシュするイメージのことを、うちでは「キャンバス」と呼んでいます。

GUIツールキットにはさまざまなUIコンポーネントが用意されているのが一般的ですが、表示上の分類だけで言えば長方形、画像、文字、記号の4つでだいたいのUIが表現されています。

このうち長方形と画像はGPUで描く観点から言えば、テクスチャを貼っているかいないかの違いしかありません。また、今日一般的なフォントはグリフをベクター画像で表現するため、文字の描画と記号の描画は、どちらもベクター画像を描きたいという共通のタスクです。

この発表をしている時点で実装したのは、長方形、画像、文字までです。スケーラブルベクターグラフィクスの描画は、今回の発表には含まれません。

長方形を描く

長方形の描画から見ていきましょう。たくさんの長方形の描画を1つずつGPUに要求するのはよくありません。CPUはコマンドを投げるので手一杯になり、GPUは小さい要求が散発的に飛んでくるので性能を発揮できません。

GPUで同じトポロジのものを効率よく大量に描くために、GPUには古くから「インスタンシング」と呼ばれる仕組みが備わっています。これは事前に個々のインスタンスの違いを表すデータをGPUから見えるメモリに置いておいて、大量の描画を1回のコマンドで要求します。

(スライドを示して)これは今回GPUに事前に渡しておくデータの構造体です。Vulkanのスクリーン座標系は右上のような-1から1の範囲になっているのですが、UIを描く場合はピクセルで座標を指定できたほうが便利なので、ピクセル座標系への変換行列を先頭に置いています。

その後ろに個々の長方形の情報の配列が続きます。ここには長方形の色と、どこから描き始めてどのくらいの大きさなのかという情報が含まれます。1度のDrawコマンドで最大65,536個の長方形を描くことができます。

(スライドを示して)これは長方形の描画に使う頂点シェーダです。インスタンスインデックスを使って自分が担当する長方形の情報を取り、単位サイズの長方形を変形させます。CPU側で長方形の追加が要求されたら、新しい長方形の情報を先ほどの構造体に載せます。

試しに色とサイズを乱数で決めた長方形を1,024個用意します。これは実際に描画コマンドを投げている部分です。実行すると、こんな感じのものが描かれます。

画像を表示する

長方形が描けたので、次は画像を表示しましょう。GPUでメッシュにラスタ画像を貼り付けて表示するなら、テクスチャサンプリングが定番の方法です。

長方形を描くためのフラグメントシェーダにテクスチャIDが渡っていたら、対応するサンプラーから色を取るようにしておきます。キャンバスの初期化時に引数で渡されたイメージをテクスチャとして使えるようにしておきます。GPUのメモリに画像を置いて、それをcanvasの引数に渡します。長方形に画像を貼り付けて、1フレームごとにサイズを変えて再描画すれば、古いMacintoshも思わず踊り出します。

ここまでの機能で適当なサイズのキャンバスを作って、使う画像を渡して、ボタンなどを描いて、その描画結果をデスクトップ用のcanvasに渡すようにすれば、デスクトップにダイアログが出ている感じの表示を組めます。

(次回へつづく)