Bash on Windows (Windows Subsystem for Linux) でvalgrindを動かす

Windows上でLinux(Ubuntu)のバイナリを動かすBash on Windowsが発表され(Windows Subsystem for Linuxという呼び方もされているが、用語の使い分けがよく分からない)、bash, gcc, clangといったツールがWindows上で動くようになった。

gccやclangを動かすだけなら今までもCygwinやMSYSといった環境があったが、これらの環境にはvalgrindを動かすことができないという大きな問題がある。メモリリークのチェックなら他にもいくつかツールはあるが、やはりvalgrindが優秀だと思う。

Bash on WindowsならLinuxのシステムをかなり忠実に再現していて、Ubuntuのバイナリがそのまま動いているらしい。であればvalgrindもいけるはず、ということで試してみた。

インストールはapt-get install valgrindで簡単にできる。で動かしてみたところ、

$ valgrind --leak-check=full ./a.out
--15035:0:aspacem   -1: ANON 0038000000-00383d5fff 4022272 r-x-- SmFixed d=0x000 i=421087  o=0       (0) m=0 /usr/lib/valgrind/memcheck-amd64-linux
--15035:0:aspacem  Valgrind: FATAL: aspacem assertion failed:
--15035:0:aspacem    segment_is_sane
--15035:0:aspacem    at m_aspacemgr/aspacemgr-linux.c:1502 (add_segment)
--15035:0:aspacem  Exiting now.

というエラーを吐いて終了。a.outコマンドを実行するのに失敗しているという分けでもなく、valgrind --helpだけでも落ちる。

ググってみたところ、bash on windowsのGithubによれば、valgrind-3.11.0をソースから入れれば動くらしい。

ということでやってみた。

ソースのダウンロードは公式サイトのダウンロードページから。ビルドは特に難しいことなく、configure; make; make installでできるっぽい。aptで入れたvalgrindとぶつからないよう、--prefixオプションを使おう。

$ tar xvjf valgrind-3.11.0.tar.bz2
$ cd valgrind-3.11.0
$ ./configure --prefix=/usr/local/valgrind-3.11.0
$ make -j4
$ sudo make install

これだけでビルド・インストールできたので、早速メモリリークを起こすサンプルを作って試したところ……

$ /usr/local/valgrind-3.11.0/bin/valgrind --leak-check=full ./a.out
==15064== Memcheck, a memory error detector
==15064== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==15064== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==15064== Command: ./a.out
==15064==
==15064==
==15064== HEAP SUMMARY:
==15064==     in use at exit: 1 bytes in 1 blocks
==15064==   total heap usage: 1 allocs, 0 frees, 1 bytes allocated
==15064==
==15064== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1
==15064==    at 0x4C2B145: operator new(unsigned long) (vg_replace_malloc.c:333)
==15064==    by 0x40071E: main (main.cpp:9)
==15064==
==15064== LEAK SUMMARY:
==15064==    definitely lost: 1 bytes in 1 blocks
==15064==    indirectly lost: 0 bytes in 0 blocks
==15064==      possibly lost: 0 bytes in 0 blocks
==15064==    still reachable: 0 bytes in 0 blocks
==15064==         suppressed: 0 bytes in 0 blocks
==15064==
==15064== For counts of detected and suppressed errors, rerun with: -v
==15064== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)

できた!

これでBash on WindowsでのC/C++の開発も結構いけるんじゃないだろうか。

ちなみに、valgrindのビルドにかかった時間(make -j4の時間)は、Windows 10 (64bit)、Core i7-2600 (3.4GHz)、RAM 16GB、SSDの環境で約47秒。また、このPCのVMwareの仮想環境に入れたUbuntu-16.04 (64ビット)の環境では、make -j4で1分12秒かかった。それぞれ1回しかやっていないので正確な計測ではないが、Bash on WindowsのC言語開発環境のパフォーマンスはVMware上のLinuxと比べてもそれほど大差はないのかもしれない。

GCC7ではエラーメッセージが改善されるらしい

プログラムを書いているときには当然色々ミスをするが、GCC(gcc/g++)が出すメッセージは Clang(clang/clang++)に比べて分かりにくい/不親切なことが多い。

例えば、FooBarという名前のクラスが宣言されていて、それを使おうとしたときに以下のように間違ってFooBazと書いたとする。

int main(int, char**){
    FooBaz foobar;
}

g++-5.4.0でコンパイルした場合のメッセージは以下のようになる。

g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:9:5: error: 'FooBaz' was not declared in this scope
     FooBaz foobar;
     ^

最新のg++-6.2では、エラーの位置を示す'^'が波線(~~~~~~)になるが、内容は特に変わらない。

/usr/local/gcc-6.2.0/bin/g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:9:5: error: 'FooBaz' was not declared in this scope
     FooBaz foobar;
     ^~~~~~

clang++-3.8では以下のように修正候補を挙げてくれる。

clang++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp:9:5: error: unknown type name 'FooBaz'; did you mean 'FooBar'?
    FooBaz foobar;
    ^~~~~~
    FooBar
main.cpp:3:7: note: 'FooBar' declared here
class FooBar {
      ^
1 error generated.

あるいは、以下のように文末のセミコロンを忘れた場合、

int main(int, char**){
    FooBaz foobar
    foobar.method1();
}

g++-5.4、g++-6.2、clang++-3.8でコンパイルした結果はそれぞれ以下のようになる。

# g++-5.4
g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:10:5: error: expected initializer before 'foobar'
     foobar.method1();
     ^

# g++-6.2
/usr/local/gcc-6.2.0/bin/g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:10:5: error: expected initializer before 'foobar'
     foobar.method1();
     ^~~~~~

# clang++-3.8
clang++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp:9:18: error: expected ';' at end of declaration
    FooBar foobar
                 ^
                 ;
1 error generated.

g++では、セミコロンを忘れた行の次の行がエラーとなっているが(普通に解析したらそうなるだろう)、clang++ではちゃんと「セミコロンがない」というメッセージになっている。

これがClangを使う一つの理由だったのだが、Phoronixの記事によれば、GCC7ではエラーメッセージか改善され、タイプミスに対する修正のヒント(Fix-it hints)を出すようになるらしい。

また、その場所にアンダーラインが引かれる、セミコロンがないことに関するFix-it hintsを出す、などと書かれているで、多分Clangと似たような感じになるのだろう。

一昔前まで、フリーで使えるC/C++コンパイラと言えばGCC一択だったところにClangの登場し、GCCにない機能が色々使えるようになったが、それに刺激されたGCCも改善が進んでいるようだ。

valgrindが検出するメモリリークの種類

C/C++のメモリリークチェックツールと言えばvalgrindが有名だが、メッセージの詳細を日本語で説明しているサイトはあまりないようなので調べてみた。

valgrindは、mallocやnewで確保されたメモリ領域がポインタによって辿れるかどうかをチェックすることでメモリリークを検出する。以下の例を見てみよう。

char *g_ptr;

void func(){
    char *p = new char[32];
    // (A)
}

int main(){
    func();
}

main関数から始まり、そこからfuncが呼ばれ、現在 (A) の位置にいるとしよう。このとき、メモリ上にはmain関数とfunc関数で定義された変数があり、また、グローバル変数であるg_ptrも有効な状態で存在している。func関数が抜けると変数pは無効になる。

このpやg_ptrのようなデータのことをroot-setと呼ぶ。valgrindの公式マニュアルにある公式な定義では、root-setは以下の2つからなる。

  • general purpose registers of all threads(全スタックの汎用レジスタ)
  • initialised, aligned, pointer-sized data words in accessible client memory, including stacks(スタックを含むアクセス可能なクライアントメモリにある、初期化された、整列された、ポインタサイズのデータワード)

この説明だと分かりにくいが、実際には

  • グローバル変数や関数内static変数などの静的データ
  • スタック中のデータ
  • レジスタ中のデータ

ということだと思う。また、メモリリークのチェックはプログラム終了時の状態を見るものなので、つまりmain関数も終わった状態なので、スタック領域は基本的にはない(実際にはmain関数が最後の関数というわけではないが、一般的なプログラムソースコードに書かれているような自動変数はすべてなくなっている)。

valgrindはプログラム終了時に、ヒープ領域中に確保されたブロックがroot-setから辿れるかどうかをチェックすることでメモリリークを検出する。また、その状況によって以下の4種類に分類される。場合によっては修正する必要がないケースもある。

  • definitely lost
  • indirectly lost
  • possibly lost
  • still reachable

definitely lost

definitely lostは、ヒープ中のブロックを参照するポインタがroot-set上にないということを意味する。以下に簡単な例を上げる。

int main(int, char**){
    char* p = new char[128];
}

このmain関数の中でnewによってヒープ領域に128バイト確保され、ポインタpはその領域へのポインタとなる。pはmain関数で定義されたもので、スタック領域に確保されている。そのため、main関数が終わればpは削除される。その結果、newで確保された領域は解放されないまま、どこからも指されていない状態になる。このような領域はdefinitely lostとなる。

main関数実行中
 root-set     Heap
+-------+    +--------------------------------------+
|   p---------> [128バイトデータ]                   |
+-------+    +--------------------------------------+
main関数終了後
 root-set     Heap
+-------+    +--------------------------------------+
|       |    |  [128バイトデータ]                   |
+-------+    +--------------------------------------+

indirectly lost

indirectly lostは、そのブロックを参照するポインタはあるがroot-setからは辿れない状態を指す。以下に例を挙げる。

class B {
};

class A {
public:
    A() : m_b(new B) {}
    ~A() {
        delete m_b;
    }
private:
    B* m_b;
};

int main(){
    A* a = new A;
}

この例では、クラスAをnewで確保し、さらにAのコンストラクタでクラスBを確保している。その結果、メモリの状態は以下のようになる。

 root-set     Heap
+-------+    +---------------------------------+
|   a---------> [A instance] ---> [B instance] |
+-------+    +---------------------------------+

main関数が終了し、ポインタaがなくなると、以下のようになる。

 root-set     Heap
+-------+    +---------------------------------+
|       |    |  [A instance] ---> [B instance] |
+-------+    +---------------------------------+

ここで、BインスタンスはAインスタンスが参照されているが、参照元を辿っていってもroot-setには辿りつけない。つまり、root-setからは辿れない。この状態をindirectly lostと呼ぶ。一方インスタンスAはどこからも参照されないのでdefinitely lostとなる。

indirectly lostもたいていの場合は修正するべきものだが、まずはdefinitely lostの修正を優先するのが良いだろう。というのも、indirectly lostの領域の解放は、それを参照している参照元インスタンスの責任である場合があるからだ。そのため、valgrindはデフォルトではindirectly lostについては詳細を表示しない。

この例でもAのデストラクタでBも解放するようになっているため、main関数でaをdeleteすればBも解放される。そのため、「Bインスタンスを解放する」という修正を直接行う必要はない。

一方、もしAのデストラクタがBを解放しない場合、aをdeleteするとBインスタンスだけが残る。

 root-set     Heap
+-------+    +------------------------------------+
|       |    |                       [B instance] |
+-------+    +------------------------------------+

すると今度はBインスタンスがdefinitely lostになるので、改めて修正方法を検討しよう。

still reachable

プログラム終了時にヒープ領域にデータが残っているが、それがroot-setから辿れる状態にあることをstill reachableと呼ぶ。以下に例を挙げる。

char* g_ptr1;

int main(){
    g_ptr1 = new char[128];
}

この例では、newで確保された領域がグローバル変数g_ptr1に参照されたままになる。このような状態はstill reachableとなる。

still reachableもデフォルトでは詳細は表示されない。still reachableは修正する必要がない場合が多い。例えばグローバル変数にシングルトンインスタンスとして使うようなデータが保持されている場合、必ずしもそれを解放する必要はないだろう。

possibly lost

possibly lostは、malloc/newで確保された領域の内の先頭以外のアドレスが参照されている場合に起こる。以下に例を挙げる。

char* g_ptr2;

int main(){
    char* p = new char[128];
    g_ptr2 = &p[10];
}

この例ではnewで確保された領域の先頭をpが参照しているが、g_ptr2はその領域の途中を指している。

 p   g_ptr2
 |    |
 v    v
 +----------------------+
 |Heap area             |
 +----------------------+

main関数が終わるとpはなくなり、g_ptr2だけが残る。すると、確保された領域の先頭ではない場所を参照するポインタのみが残る。このような状態をpossibly lostと呼ぶ。

possibly lostは一般に修正するべきものであり、valgrindもデフォルトで詳細を表示する。上に挙げた例は恣意的に起こした例だが、公式にマニュアルにはpossibly lostになる例がいくつか挙げれられている。一例としては、いくつかのstd::stringの実装は、確保された領域の先頭3ワードは文字列の長さ、領域のサイズ、参照カウントに使われるため、文字列データへのポインタは確保領域の途中から始まるらしい。

メッセージの表示・抑制

valgrindでメモリリークのチェックをするなら、--leak-check=fullを付けよう。でないとリークした領域を確保したソースコード位置など、修正に必要な情報が表示されない。

また、デフォルトでは詳細情報についてはdefinitely lostとpossibly lostしか表示されない。indirectly lostやstill reachableを表示するには--show-leak-kindsオプションを使う。引数はdefinite、indirect、possible、reachableをカンマ区切りで繋げる。また、すべて表示したい場合はallとする。

 # indirectly lostとstill reachableのみ表示する
 $ valgrind --leak-check=full --show-leak-kinds=indirect,reachable prog-name
 # すべての種類について表示する
 $ valgrind --leak-check=full --show-leak-kinds=all prog-name

特定のリークを無視したい場合

特定のリークを無視したい場合(誤検出である場合、依存ライブラリの問題であり自分では修正できない場合など)、表示を抑制することもできる。"valgrind suppressions"で調べれば色々出てくると思う。

まとめ

  • リークの種類の違いを理解しよう
    • definitely lostは修正するべき。
    • possible lostも修正するべき。
    • indirectly lostは直接修正するのではなく、definitely lostを直そう。それでダメならdefinitely lostに変わっているだろう。
    • still reachableは多くの場合修正する必要はない。
  • 必要に応じて--show-leak-kindsオプションを使って不要なメッセージを抑制しよう。
  • 特定の箇所に関する表示を抑制することもできる。

【翻訳】 The Linux Graphics Stack

この記事はGNOME関連の開発を行っているJasper St. Pierre氏のブロク記事「The Linux Graphics Stack」の翻訳である。

Linuxのグラフィックス環境はX Window Systemがデファクト標準になっているが、最近はWaylandやMirといった新しいディスプレイサーバが出てきている。しかし、なぜ置き換えようとしているのか、WaylandやMirがX Window Systemと比べてどの辺りが優れているか、という具体的な情報はあまり出てこない。検索しても出てくるのは「Waylandを使ってみた」のような話か、あるいはディスプレイサーバに関するWayland陣営とMir陣営の争いのような話ばかりで、(少なくとも日本語のサイトには)あまりそういう情報がないようだ。

Waylandの公式サイトにはその辺りの話も書いてあるようだが、色々用語がよくわからない。X Window Systemも長い時間の間にさまざまな拡張が追加されており、名前は聞いたことがあるが意味はよく知らない言葉が多い。

仕方なく英語のサイトもいくつか漁ってみたところ、うまくまとまってそうな感じのサイトがあったので、翻訳してみた。原文の著者に聞いたところ快く承諾してもらい、また図についてはコピーを認めてもらった。なおPierre氏によればこの記事は少し古くなっているので直すかもしれない、とのことだ。

いざ翻訳してはみたものの、英語はあんまり得意じゃないので翻訳としての質はひどいと思う。翻訳以前に意味が取れていないところがあって、助動詞のニュアンスや場合によっては一文丸々分かっていないところもある。そういうところは誤魔化して書いたり、単なる直訳になったりしているので意味不明なところがあるだろう。これは翻訳の問題ではなく原文の意味が分かっていないのが原因なのでどうしようもない。

ともかく、一応何となくLinuxのグラフィックスシステムの構造の雰囲気は伝わるのではないかと思う。これからもう少しこの辺の技術を調べてみようと思っているので、もう少し理解が深まればここの翻訳も改善されるかもしれない。

なお、{ } で囲ったところは原文の単語やフレーズが入っている。用語の正しい訳語が分からなかった場合や、本当にイミフだった部分である。{{ }} の部分は訳注だ。

ということで以下翻訳。


The Linux Graphics Stack

これはLinuxグラフィックススタック、そいてそれらがどのように組み合わされているの初歩的なの概要だ。私はこのスタックについてOwen Taylor、Ray StrodeやAdam Jacksonといった人々と話し合った後、初めはこれを私自身のために書いた。私が個々のピースを忘れてしまったので、私は毎月かそこらで彼らのところに戻り、その土台から全てに渡って学ばなければならなかった。私は、彼らを困らせることがないよう、彼らに良い高レベルの概要ドキュメントを求めた。彼らは何も知らなかった。私はこれを始めた。Adam JacksonやDavid Airlieにレビューしてもらった。彼らは両方ともまさにこのスタックに従事している。

また私は、このスタックの大部分はフリーソフトウェアドライバにのみ適用されることを指摘したい。それは、あなたがここで読む多くのことはAMD CatalystやNVIDIAのプロプライエタリなドライバには適用できないかもしれないということた。それらは自身のOpenGL実装を持っていたり、あるいはMesaの内部フォークを持っているかもしれない。私はフリーなradeon、nouveauやインテルのドライバ由来のスタックについて説明している。

もし質問や不明確な部分があったら、あるいは私が何かについてひどくひどく間違っていたら、またあるいは文をやり損なっていて理解不能になっていたなら、下のコメントセクションで私に尋ねる、あるいは知らせて欲しい。{{訳注: 本当に質問があったら原文の著者にしよう。この記事のコメントに書いてもらっても良いが、多分答えられない。}}

始めるにあたって、それぞれのピースがこのスタックのどこにあたるかの大まかな概要が分かるよう、全体の大きなスタックをここに貼ろうと思う。これがすぐに理解できなくても怖がらなくていい。この投稿のあらゆるところにあるこれを参照して欲しい。これが便利なリンクだ。{{訳注: 原文ではこのすぐ下の段落へのリンクになっている。}}

ところで、正確に言えば、あなたが行っているレンダリングの種類に応じて2つの異なる道がある。

OpenGLによる3Dレンダリング

  1. あなたのプログラムが起動し、OpenGLを使って描画する。
  2. ライブラリ「Mesa」がOpenGLを実装する。それはカード固有のドライバを使い、APIをハードウェア固有の形式に変換する。もしそのドライバが内部でGalliumを使っていたら、OpenGL APIを共通の中間表現にする共有コンポーネント、TGSIがある。APIはGalliumを経由して渡され、全てのデバイス固有のドライバが行うのはTGSIからハードウェアコマンドに変換することだ。
  3. libdrmが特別な秘密のカード固有のioctlを使い、Linuxカーネルと話す。
  4. Linuxカーネルは、特別な許可を持っており、カード上にメモリを確保する。
  5. Mesaのレベルに戻り、Mesaはバッファの反転 {buffer flip} やウィンドウの位置などが同期されているを確認するため、DRI2を使ってXorgと話す。

cairoによる2Dレンダリング

  1. あなたのプログラムが起動し、cairoを使って描画する。
  2. あなたはグラデーションを使った円をいくつか描画する。cairoはその円を台形に変換し、それらの台形とグラデーションをX Render拡張を使ってXサーバに送る。XサーバがX Render拡張をサポートしていない場合は、cairoはlibpixmanを使ってローカルで描画し、他のいくつかの方法を使ってレンダリングされたpixmapをXサーバに送る。
  3. XサーバはX Renderリクエストに応答する。Xorgは描画するために複数の特殊なドライバを使うことができる。

A. ソフトウェアにフォールバックするケース、あるいはグラフィックスドライバがタスクを遂行できなケースでは、Xorgはpixmanを使って実際の描画を行う。これはcairoが行うケースに似ている。
B. ハードウェアアクセラレーションされるケースでは、Xorgドライバはカーネルに対してlibdrmを話し、同じ方法でテクスチャやコマンドを送る。

Xorgがスクリーン上でやることについては、Xorg自身がフレームバッファを準備し、KMSやカード固有のドライバを使って描画を行う。

X Window System、X11、Xlib、Xorg

X11はグラフィックスたけに関係しているわけではない。イベント配送システムや、ウィンドウへのプロパティの取り付けのコンセプトなどがある。その他のグラフィックスでないものの多くはその上に作られている(クリップボード、ドラッグアンドドロップ)。ここに挙げたのは完全性と紹介としてのものだ。X Window System、X11とすべてのその奇妙なデザイン決定については後でポストすることにする。

X11
X Window Systemによって使われるワイヤープロトコル。
Xlib
システムのクライアントサイドのリファレンス実装で、X Window System上でウィンドウを管理するたくさんの他のユーティリティのホスト。GTK+やQtなど、Xをサポートするツールキットで使われる。今日ではアプリケーションで見られることはほぼない。
XCB
時々、Xlibの代わりと言われる。それはX11プロトコルのたくさんを実装する。そのAPIはXlibに比べてずっと低レベルで、実際、現在ではXlibはXCBの上に作られている。これを言及するのは単にそれがもう一つの頭字語だからだ。
Xorg
システムのサーバサイドのリファレンス実装。

私は注意深く物事を正確に表示したいと思う。もし私が「Xサーバ」と言ったら、一般的なXサーバについて話している。それはXorgかもしれないし、AppleのXサーバ実装かもしれないし、Kdriveかもしれない。{{訳注: Kdriveで検索するとKINGSOFTが以前に提供していたオンラインストレージサービスが引っかかるが、もちろんそれではない。このKdriveは、以前にKeith Packardが作ったXサーバで、省メモリ環境を想定したものらしい。}} 我々には分からない。もし私が「X11」あるいは「X Window System」と言ったら、プロトコルの設計あるいは全体システムについて話している。もし私が「Xorg」と言ったら、それは最も使われているXサーバであるXorgの実装の詳細を意味しており、他のXサーバには全く適用できないだろう。もし私が「X」それだけを言ったら、それはバグだ。

X11プロトコルは拡張性があるように設計された。それは、新しい機能が新しいプロトコルを作ったり既存の古いクライアントを壊すことなく追加可能であることを意味する。例として、xeyesやoclockはShape拡張によってその洒落た形を得ており、それは矩形でないウィンドウのサポートを提供するものだ。もしあなたが、この魔法のような機能がどのようにしてどこからともなく現れたのか興味があるなら、答えはそうではないということだ。拡張のサポートは、使われるようになる前にサーバとクライアントの両方に追加される必要がある。コアプロトコル自身に、クライアントがサーバにどの拡張がサポートされているかを尋ねることができるような機能があるため、どの機能が使えるか、あるいは使えないかを知っているのだ。

X11はまた、「ネットワーク透過」であるよう設計された。最も重大に意味するのは、我々はXサーバとXクライアントが同じマシン上にいるということをあてにはできないということだ。だから、2つの間の会話はネットワーク上を行く。実際には、モダンなデスクトップ環境はこのシナリオのそのままでは動いておらず、たくさんのプロセス間通信がX11以外、DBusなどを通して行われている。サーバとクライアントが同じマシン上にあるとき、ネットワーク上を行く代わりにUNIXソケット上を行き、カーネルはデータをコピーする必要がない。

cairo

cairoはベクトル図形を描画するための描画ライブラリで、Firefoxのようなアプリケーションから直接使われたり、あるいはGTK+のようなライブラリを通して使われる。GTK3+の描画モデルは完全にcairo上に建てられている。もしあなたがHTML5の<canvas>を使ったことがあるなら、cairoは実質的に同じAPIを実装している。<canvas>は元々Appleによって開発されたが、そのベクトル描画モデルはPostScriptのベクトル描画モデルとして広く知られたもので、その他のベクトルグラフィックス技術、例えばPDF、Flash、SVG、Direct2D、Quartz2D、OpenVG、それに完全にリストアップできないたくさんのものに見られる。

cairoはXlibバックエンドを通して、X11サーフェスへの描画をサポートする。

cairoはGTK+のようなツールキットで使われている。機能はGTK+2に追加され、GTK+2.8ではオプションとして使われている。GTK+3の描画モデルはcairoを必要とする。

XRender拡張

X11は特別な拡張、XRenderを持っている。それはアンチエイリアスされた描画プリミティブ(X11の既存のグラフィックスはエイリアスだ)、グラデーション、行列変換などをサポートする。元々の目的は、ドライバが特定の描画のために特別にアクセラレートされたコードパスを持てるようにすることだ。残念ながら、直感的でない理由により、それはソフトウェアのラスタライズが同程度に速いというケースになるように見える。ともかく。XRenderは整列された台形、つまり左右のエッジへの傾斜を持つ矩形、を扱う。Carl WorthとKeith Packardは台形の「高速な」ラスタライズ手法を思いついた。台形はものすごく簡単に2つの三角形に分解でき、高速なハードウェアレンダリングができる。cairoは、それが行う台形へのテッセレーションの実態を垣間見せてくれる素敵なshow-trapsユーティリティを含んでいる。

f:id:wagavulin:20160719194614p:plain

これは我々が描いた単純な赤い丸だ。これは2セットの台形に分解される。一つは線 {stroke}、もう一つは塗りつぶし {fill} だ。show-trapsがデフォルトで見せてくれる図はあまりためにならないので、私はそれをハックして個々の台形を一意な色を与えるようにした。これが黒い線の部分の台形のセットだ。

f:id:wagavulin:20160719194615p:plain

サイケデリックだ。

pixman

Xサーバとcairoはいくつかの点でピクセルレベルの操作が必要だ。cairoとXorgは基本的なラスタライズアルゴリズム、ある種のバッファ(ARGB32、RGB24、RGB565)へのピクセルレベルアクセス、グラデーション、行列などの独立した実装を持っていた。現在、Xサーバとcairoの両方とも、これらのことを扱うpixmanと呼ばれる例レベルのライブラリを共有している。pixmanは公開APIとなるものではなく、描画APIでもない。それは本当にAPIではなく、様々な場所でのコードの重複にたいするソリューションに過ぎない。

OpenGL、Mesa、gallium

今、面白いところにたどり着いた、モダンなハードウェアアクセラレーションだ。私は皆さんがOpenGLとは何かはすでに知っていると思っている。それはライブラリではなく、libGL.soになる1セットのソースは決してない。それぞれのベンダは独自のlibGL.soを提供することになっている。NVIDIAはWindowsとOS Xの実装に基づく独自のOpenGL実装を提供し、その独自のlibGL.soを出している。

もしあなたがオープンソースのドライバを走らせているなら、あなたのlibGL.so実装は多分Mesa由来のものだ。Mesaは多くのことを行うが、それが提供する最も重要なもので、それを最も有名にさせているのは、そのOpenGL実装だ。それはOpenGL APIのオープンソースの実装だ。Mesa自身はそれを提供する複数のバックエンドを持っている。3つのCPUベースの実装、swrast(旧式で古く、使わない)、softpipe(遅い)、llvmpipe(潜在的に高速)がある。Mesaはまたハードウェア固有のドライバも持っている。IntelはMesaをサポートし、Mesa内部に積まれたた、たくさんの彼らのチップセットのドライバを構築した。radeonとnouveauドライバもまたMesaでサポートされているが、異なるアーキテクチャ上に構築された。galliumだ。

galliumは魔法などではない。それは、ドライバ実装をとても簡単にするコンポーネント一式だ。アイデアは以下のようなものだ。ステートトラッカはいくつの形式のAPI(OpenGL、GLSL、Direct3D)を実装し、ステートを中間表現(Tungsten Graphics Shader Infrastructure、あるいはTGSIとして知られる)に変換する。{{訳注: このリンクはすでに切れている。}} そして、バックエンドはその中間表現を受け取り、それをハードウェア自身によって消費される命令への変換する。

悲しいことに、IntelドライバはGalliumを使っていない。私の同僚が言うには、Intelのドライバ開発者はMesaと彼らのドライバの間にレイヤを挟みたくないから、ということだ。

余談: さらに頭字語

カバーするべきややこしい頭字語がたくさんあるが、私はそれぞれに対してH3を与えて1段落を書きたくはない。ここに挙げるだけにしよう。ほとんどは今日の世界では問題になるものではなく、あなたが混乱しないようにするための簡単リファレンスというだけだ。

GLES
OpenGLは異なるフォームファクタ用のいくつかのプロファイルを持つ。GLESはそのうちの一つで、「GL Embedded System」あるいは「GL Embedded Subset」の略であり、あなたが誰に尋ねるかによる。それは組み込み市場にターゲットする最新のアプローチだ。iPhoneはGLEL2.0をサポートする。
GLX
OpenGLはプラットフォームやウィンドウシステムというコンセプトがない。そのため、OpenGLとX11のようなものの違いを変換するためのバインディングが必要だ。例えば、OpenGLシーンをX11のウィンドウに置く。GLXはこの接着剤だ。
WGL
上を見て、「X11」を「Windows」Microsoftのオペレーティングシステムに置き換えてほしい。
EGL
EGLとGLESはよく間違われる。EGLは新しいプラットフォーム非依存のAPIで、Khronosグループによって開発された(同じグループがOpenGLを開発し、標準化した)。それはOpenGLシーンをプラットフォーム上で動作させる設備を提供する。OpenGLと同様、これはベンダが実装する。それは WGL/GLXのようなバインディングの代わりであり、GLUTのようなそれらの上にあるライブラリではない。
fglrx
fglrxはAMDのプロプライエタリなXorg OpenGLドライバの旧名であり、今では「Catalyst」として知られている。それは「FireGL and Radeon for X」の略だ。プロプライエタリなドライバであるが、独自のlibGL.so実装を持っている。Mesaに基づくものであるかは私は知らない。これに言及したのは、それが時々AIGLXやGLXのような一般的な技術と、「GL」や「X」という文字があるために間違われることがあるからだ。
DIX、DDX
XorgのXグラフィックスの部分は大きく2つの部分からなる。DIX、「Driver Independent X」サブシステムと、DDX、「Driver Dependent X」サブシステムだ。我々がXorgドライバについて話すとき、より技術的に正確な用語はDDXドライバだ。

Xorgドライバ、DRM、DRI

少しの前に、私はXorgはハードウェアの特定のピースに基づく、アクセラレートされたレンダリングの能力があることに言及した。また、これはX11の描画コマンドからOpenGL呼び出しへの変換によって実装されたものではないことも言った。ドライバがMesaの世界で実装されているなら、これはどのようにしてMesaに依存することなく動作することができるのだろうか?

答えは、MesaとXorgの間で共有される新しい基盤の要素を作ることだ。MesaはOpenGLの部分を実装し、XorgはX11描画部分を実装し、それらの両方はカード固有のコマンド一式に変換する。これらのコマンドはそれから、「Direct Rendering Manager」あるいはDRMと呼ばれるものを使ってカーネルにアップロードされる。libdrmは、カード上にものを確保するため、一般的なプライベートなioctlをカーネルに使い、コマンド、テクスチャ、そこで必要になるものを詰め込む。このioctlインターフェースは2つの形式がある、IntelのGEMとTungsten GraphicsのTTMだ。それらに良い区別はない。それらは両方とも同じことをする。単に競合する異なる実装というだけだ。歴史的に、GEMはTTMに対するシンプルな代替とデザインされ、また自慢げにアナウンスされたが、そのうちにTTMと同じくらい複雑に成長した。Welp。

これは、あなたがglxgearsのようなものを走らせるとき、それがMesaをロードすることを意味する。Mesa自身はlibdrmをロードし、GEM/TTMを使ってカーネルドライバと直接話す。そう、glxgearsはカーネルドライバと直接話して、回転するギアを見せ、厳格にそのユーティリティのベンチマークコンテンツを厳格に思い出させる。

もしあなたが ls /usr/lib64/libdrm_* の中を探し回れば、ハードウェア固有のドライバがあることに気が付くだろう。GEM/TTMが十分でないケースでは、MesaとXサーバドライバはカーネルと話すため、プライベートなioctlを持つだろう。それはここにカプセル化されている。libdrm自身は実際にはこれらをロードしない。

Xサーバはここで、同期のようなことができるよう、何が起きているかを知る必要がある。あなたのglxgears、カーネルとXサーバとの同期はDRI、あるいはより正確にDRS2と呼ばれる。「DRI」はMesaとXorgを結びつける(DRMやこの記事で話した一連のことを導入する)プロジェクトと、DRIプロトコルやライブラリの両方を指す。DRI1は良くなかったので、それを投げ捨ててDRI2で置き換えた。

KMS

余談のようなものとして、あなたは新しいXサーバに従事している、あるいはあなたはXサーバを使わずにVT上にグラフィックスを表示したいとしよう。どのようにしてやるだろう?あなたはグラフィックスを置けるよう、実際のハードウェアを設定しなければならない。libdrmとカーネルの内部では、まさにそれを行う特別なサブシステムがあり、KMSと呼ばれていて、「Kernel Mode Setting」の略だ。思い出してほしいが、TTYに直接表示するため、ioctlのセットを通してグラフィックスモードのセットアップ、フレームバッファのマップなどが可能である。以前はハードウェア固有のioctlがあった(今もある)ため、libkmsという共有ライブラリが共有APIを与えるために作られた。やがて、カーネルレベルの新しいAPIがあり、文字通りに「dumb ioctl」と呼ばれた。新しいdumb ioctlが整っているので、libkmsではなくそちらを使うことが推奨される。

それはとても低レベルだが、実行することが可能だ。Plymouth、モダンなディストリビューションに統合された起動スプラッシュスクリーンは、Xサーバに頼ることなくグラフィックスをセットアップするとてもシンプルなアプリケーションの良い例だ {{訳注: Plymouthはもちろん都市名や車の名前ではなく、Fedoraのプロジェクトの名前。}}。

「Expose」モデル、リダイレクション、TFP、コンポジット、AIGLX

私は、何をコンポジットするか、またウィンドウマネージャが何をするかを説明せずに「コンポジット型ウィンドウマネージャ」という言葉を使った。{{訳注: 少なくともこの記事の中で「コンポジット」という言葉が出たのはここが最初なのだが…}} 80年代、UNIXシステム上でX Window Systemが設計され、HP、Digial Equipment Corp.、Sun Microsystems、SGIといったたくさんの他の会社がX Window Systemに基づく製品を開発してたころに戻ってみよう。X11は、どのようにしてウィンドウが制御されるかということに対する基本的なポリシーを意図的に持っておらず、それを「ウィンドウマネージャ」という別のプロセスに責任を委譲した。

例として、そのときのポピュラーな環境であるCDEは、「focus follow mouse」と呼ばれるシステムを持っていた。それは、ウィンドウにマウスを移動させたときにウィンドウがフォーカスされるというものだ。これはWindowsやMac OS Xがデフォルトで使う「click to focus」とは異なるものだ。

ウィンドウマネージャがより複雑になるにつれて、ドキュメントにたくさんの異なる環境の相互互換性についての説明が現れ始めた。それもまた、「Click to Focus」などのようなポリシーを命じるものではない。

さらに、80年代に戻ると、多くのシステムはあまり多くのメモリを持っていなかった。それらはすべてのウィンドウの内容のピクセルコンテンツすべてを保持することができなかった。WindowsとX11はこの問題を同じ方法で解決した。個々のX11ウィンドウはロッシーであるべきだということだ。つまり、ウィンドウが「exposed」されたということがプログラムに通知される。

f:id:wagavulin:20160719194612p:plain

ウィンドウがこのようになっていると想像してほしい。今ユーザがGIMPをドラッグしたとしよう。

f:id:wagavulin:20160719194613p:plain

濃い灰色の領域はexposeされた。Exposeイベントがそのウィンドウを保有するプログラムに贈られ、それは内容を再描画しなければならない。これが、なぜWindowsやLinuxで固まったプログラムが、それらの上にあるウィンドウをドラッグした後にブランクになるかという理由だ。Windowsでは、デスクトップ自身は何も特別な権限を持たない単なるプログラムであり、他と同様に固まることがあるという事実を考えてほしい。そしてあなたはひどいバグレポートをもらうことになる。

現在、マシンは十分なメモリがあり、我々は、リダイレクションという仕組みによってX11のウィンドウをロスレスにする機会を得た。ウィンドウをリダイレクトすると、Xサーバはバックバッファへの直接の描画の代わりに、個々のウィンドウに対するbacking pixmapを作る。これはウィンドウが完全に隠れることを意味する。何か他のものが機会を利用してメモリバッファのピクセルを表示しなければならない。

コンポジット拡張はコンポジット型ウィンドウマネージャ、あるいは「コンポジタ」がComposie Overlay WindowあるいはCOWと呼ばれるものをセットアップすることを可能にする。コンポジタはCOWを所有し、それを塗る{paint}することができる。あなたがCompizやGNOME Shellを走らせるとき、これらのプログラムはリダイレクトされたウィンドウをスクリーンに表示させるためにOpenGLを使っている。XサーバはGL拡張「Texture from Pixmap」あるいはTFPをによってそれらにウィンドウの内容を与える。それはOpenGLプログラムにX11のPixmapを、あたかもOpenGLテクスチャであるかのように使わせる。

コンポジット型ウィンドウマネージャは本質的にはTFPやOpenGLを使う必要はなく、単にそれをするのに最も簡単であるというだけだ。もしそうしたいなら、普通にCOW上にウィンドウのpixmapを描くこともできる。kwin4はウィンドウをコンポジットするのに直接Qtを使うと言われたことがある。

コンポジット型ウィンドウマネージャは、TFPを使ってXサーバからpixmapを取り込み、それをOpenGLシーン上の正しい位置に描画し、あなたがクリックしているのが実際にX11のウィンドウ上である考えるような錯覚を与える。これを錯覚と説明するのは馬鹿げているように聞こえるかもしれないが、GNOME Shellで遊んでみて、ウィンドウアクターのサイズや位置を調整してほしい(global.get_window_actors().forEach(function(w) { w.scale_x = w.scale_y = 0.5; }と打つ)。錯覚が崩壊するのが見えるだろう。あなたがクリックしたとき、あなたはまっすぐ動画プレーヤを突き出て、下にある実際のウィンドウを突くことが分かるだろう。(上の断片の0.5を1にすれば挙動を戻すことができる)。{{訳注: この辺りはGNOME Shellで実際に試してみればもう少しまともな訳ができると思うが、手元にないので試さずに書いてる。}}

この情報を基に、もう一つ頭字語、AIGLXを説明しよう。AIGLXは「Accelerated Indirect GLX」の略だ。X11はネットワーク化されたプロトコルであり、これはOpenGLがネットワーク越しに動作しなければならないことを意味する。OpenGLがネットワーク越しに使われるとき、これは、同じマシン上にある場合の「direct context」に対する「indirect context」と呼ばれる。「indirect context」で使われるネットワークプロトコルはかなり不完全で不安定だ。

AIGLXの背後にあるデザインの決断を理解するには、それが解こうとしている問題、つまりCompizのようなコンポジット型ウィンドウマネージャを高速にするという問題を理解する必要がある。NVIDIAのプロプライエタリなドライバはカスタムインターフェースによるカーネルレベルのメモリ管理を持ち、オープンソースのスタックはこの時点ではまだ達成していない。ウィンドウのテクスチャをXサーバからグラフィックスハードウェアに引き入れることは、ウィンドウが更新されるたびに毎回コピーされなければならないことを意味した。遅い。そのため、AIGLXはソフトウェア内でOpenGLを実装する一時的なハックで、ハードウェアアクセラレーションへのコピーを妨げる。Compizのようなコンポジタが使うシーンはそれほど複雑でないため、それは十分うまく動作した。

すべてのファンファーレやPhoronixの記事にかかわらず、AIGLXはしばらくの間現実には使われていない。コピーなしにTFPを実装するのに使うことができるDRIスタックを持っているが。

あなたが想像できるように、OpenGLテクスチャとしてペイントされることができるよう、ウィンドウテクスチャの内容をコピー(より正確にはサンプリング)することは、データのコピーが必要だ。そのため、大部分のウィンドウマネージャはフルスクリーンのウィンドウのリダイレクションをオフにする機能を持つ。これをunredirectionと表現するのは少し馬鹿げているように聞こえるかもしれない。しかし、それはウィンドウの初期状態かもしれないが、我々のモダンなデスクトップでは、とても普通の状態ではない。ここにあるロジックは、もしあるウィンドウがどもかくCOWをカバーし、またコンポジット機能が必要ないなら、
それは安全にunredirectされることができる。この機能は秒間60フレームの高いパフォーマンスで動作することが必要な、ゲームのようなプログラムに高いパフォーマンスをもたらすよう設計された。

Wayland

お分かりのように、我々はXの初期の一枚岩の挙動に由来する元々の基盤を分割した。これは、Xの一枚岩のパーツを取り壊した唯一の場所ではない。たくさんの入力デバイスの扱いはevdevでカーネルに移された、またデバイスのホットプラグのようなものはudevの中に移された。{{訳注: evdevもudevもこの記事では出てきていない。}}

X Window Systemが今まで残った理由は、単にそれを置き換えるのが多大な労力であるということだけだ。Xorgが初期にそうであったものから奪われて、またモダンなデスクトップ環境に必要とされる多くの機能がもっぱら拡張によってもたらせるようになり、なんと言うか、Xは期限切れだ

Waylandに入ろう。Waylandは我々が構築した既存の基盤の多くを再利用している。最も議論を呼ぶことの一つは、それがネットワーク透過性あるいは描画プロトコルを欠いているということだ。Xのネットワーク透過性は現代ではうまくいかない。多くのLinuxの機能が、DBusのよううな、Xでないものにインプレースにホストされている。ドラッグアンドドロップやクリップボードのサポートが、ネットワークサポートのためだけにX Window Systemの大量のハックによってなされているのを見るのは恥ずかしいことだ。

Waylandはあなたのモニターのフレームバッファを起動して動作させるための上記の詳細の完全なスタックをほとんど使うことができる。Waylandはまだプロトコルを持っているが、それはUNIXソケットとローカルリソースに基づくものだ。最大の劇的な変更は、/usr/bin/Xorgがあるような感じで動作する/usr/bin/waylandというものはないということだ。代わりに、モダンなデスクトップの助言に従い、このすべてをウィンドウマネージャのプロセスに移動した。ウィンドウマネージャは、Waylandの用語ではより正確には「コンポジタ」と呼ばれるが、evdevのようなシステムでカーネルからイベントを取り出し、KMSやDRMを使ってフレームバッファをセットアップし、OpenGLを含むどんな描画スタックによってでも、スクリーンにウィンドウを描画する。これはたくさんのコードがあるように聞こえるかもしれないが、これらのサブシステムは他の場所に移動されたが、これらのすべてを行うコードは多分2000-3000SLOCのオーダーだ。sane windowフォーカスやスタックポリシーの実装し、Xサーバとの同期するのにだいたい4000-5000SLOCだという不平の一部を考えれば、私が魅了されたのが少し分かるだろう。{{訳注: この文はさっぱり分からなかった。}}

Waylandはクライアントとコンポジタが使うべき両方のライブラリを持つが、それは単なる特定Waylandプロトコルのリファレンス実装だ。libwaylandの助けなしにWaylandコンポジタを完全にPythonやRubyで書き、純粋なPythonでプロトコルを実装することもできる。

Waylandクライアントはコンポジタと話し、バッファをリクエストする。コンポジタはOpenGL、cairoなどで描画可能なバッファを返す。コンポジタは自分の裁量で、バッファにしたいことを何でもする。それがすごいので表示する、アプリケーションが退屈なのでそれを火にくべる、あるいは、Linuxのキューブの回転のYoutubeビデオがもっと必要なのでキューブ上で回転させる。{{訳注: こういうやつのことだろう。}}

コンポジタはまた、入力イベントハンドリングも管理している。上のGNOME Shellでウィンドウの拡大率を設定してみると、はじめあなたは混乱させられ、次にマウスが変換されていないウィンドウに対応していることに気が付くかもしれない。これは、我々が *実際には* X11のウィンドウに影響を与えておらず、単にどのように表示されるかを変えているだけだからだ。Xサーバはウィンドウがどこにあるか把握しているが、Xサーバがそこにあると考えている場所にそれらを表示するのはコンポジットマネージャに任されている。でなければ混乱が起きる。

Waylandコンポジタはevedevからの読み込みとウィンドウへのイベント通知の役目を負っているので、ウィンドウがどこにあるかということについて多分より良いアイデアを持っており、内部で変形 {transformation} をすることができる。それは、我々に一時的にキューブ上のウィンドウをスピンさせてくれるだけでなく、キューブ上のウィンドウとやり取り {interact} させてくれるようになることを意味する。

概要

私は今でもXorgは実装がとても一枚岩だと聞くことがよくある。これはとても正しいが、以前ほどには正しくない。これはXorgの開発者が無能であるためではなく、この大部分は我々がサポートしなければならない荷物、例えばハードウェアアクセラレートされたXRenderプロトコル、さらにもっと遡ればXPolyFillのようなアンチエイリアスでない描画コマンドによるものだ。XがいつかWaylandに賛成して退場するのは明らかだが、この多くは{happening with the acknowledgement}とXorgとデスクトップ開発者の助けだということを私は明らかにしたい。彼らは頑固ではなく、無能でもない。Hell, 30歳のプロトコルと歴史に取り組み実装するという地獄に、彼らは優秀な仕事をしている。特に新しいアーキテクチャによって。

私はまたこの記事で言及したものに従事した皆さんに大きな感謝をしたい。また、私の馬鹿な疑問すべてに我慢強く答えてくれたことに対してOwen Taylor、Ray StrodeとAdam Jacksonに、この記事の技術的なレビューで助けれてくれたAdam Jacksonに個人的な感謝を表明したい。

私はこれらのピースそれぞれの詳細なレベルに入っていったが、もし興味があればあなたが学より詳細にぶことができるものがたくさんある。いくつか例を挙げよう。任意の図形を台形に変換するのにcairoが使っている幾何学アルゴリズムや理論を学ぶことができる。あるいは、Cral WorthやKeith Packardによる、台形の高速なソフトウェア描画アルゴリズムやそれがなぜ速いかを調査できる。DRI2の設計を見て、それがどうDRI1と違うのかを考えてほしい。あるいは、あなたはハードウェア自体に興味があり、グラフィックスカードのアーキテクチャを見て、あなたがどのようにプログラムするかを見るためのデータシートを見るかもしれない。そしてもしあなたがこれらの領域を助けたいなら、上に挙げたすべてのプロジェクトは喜んで貢献を受け取るだろう。

私は将来これらをもっと書こうと思っている。LinuxとGNOMEコミュニティで使われているスタックの多くの部分は、高レベルからそれらを詳解する概要ドキュメントがないのだ。