ストリームアウト

おかえり! 今回はストリームアウト (SO) に焦点を当てる。これはジオメトリシェーダーステージの出力を以降のパイプラインに渡すのではなくメモリに格納するための機能である。例えばスキン済みの頂点データをキャッシュしたり、D3D10 レベルのハードウェアで D3D10 API を使った貧者のコンピュートシェーダーを実行したりするのに SO は利用できる (なお D3D11 では D3D10 ハードウェアでも CS 4.0 が使える)。前章で触れた GS インスタンシングと同様に、SO に関連する機能の一部は API ドキュメントがかなり乏しい。そこで正確にはシリーズの意図とは外れてしまうのだが、この章では API の使い方に関してもいくつかコメントを入れることにする。

頂点シェーダーストリームアウト (ヌル GS による SO)

頂点シェーダーストリームアウト (VS SO) は D3D10 (および D3D11) のドキュメントできちんと説明されていない機能の一つである。というより Getting Started with the Stream-Output Stage にポツンと一言二言あるのが唯一の言及で、後はサンプルコードから理解することが求められる──そのサンプルコードも分かりやすいとは言えない。これは残念なことだ──VS SO は GS SO より簡単であり、優れた応用例 (例えばスキン済み頂点のキャッシュ) も存在している。

D3D10 と D3D11 における VS SO の使い方を示そう: GS のバイトコードの代わりに VS のバイトコードを CreateGeometryShaderWithStreamOutput に渡すだけだ。ドキュメントには「コンパイルされたジオメトリシェーダーのサイズ」に関しても言及があるが、これは無視して構わない。こうすると事実上ヌルのジオメトリシェーダーが作成される──これをパイプラインに設定して実行しても、頂点は GS プロセッシングを通過しない。その GS は API モデルに合わせるためのラッパー (というよりダクトテープ) であり、レンダリングは GS ステージを何もせずに通過するだけで GS の直後に SO が来る──ただし前章で説明したように、実際のハードウェアは GS が設定されていないとき GS ステージを完全に飛ばすことが多い。

VS SO を使ったときシェーディングされた頂点は以前と同様にプリミティブとして組み立てられるが、それらはこれまでに説明してきたパイプラインに送られるのではなく、SO に転送されて──ここでも──バッファに格納される。このバッファで正確に何が起こるかは SO の (作成時に渡される) 宣言に応じて変わる。 SO の宣言でアプリケーションはそれぞれの出力ベクトルをストリームターゲット (SO ターゲット) にどのように格納するかを指定する。SO の宣言が頂点シェーダーにおける出力の宣言と “マッチ” する (同じアトリビュートが同じ順序で並ぶ) なら、入力バッファからのデータはほぼ処理を受けずにメモリに転送される。二つの宣言が完全にマッチしない場合は、シェーダーが書き込んだアトリビュートの一部が飛ばされたり、順序が変わったりする──いずれにせよ順序を入れ替える追加の処理が必要になる。これは専用の順序変更ユニット (SO の入力バッファに対する gather タイプの操作を実装するもの) を使うか、あるいは大きなバースト書き込みではなく大量の小さな書き込みを生成することで行われる。どちらの方法でも追加で処理が必要であり、一般に遅くなる。低速なパスを踏む正確な条件はハードウェアの詳細によって異なるが、それを知ったところで大きく速度が改善するわけではない。最適な SO のパフォーマンスを得たいなら、黙って SO の宣言と VS 出力の宣言を合わせるべきだ。

もう一つ重要なのが、通常 SO はメモリサブシステムへの非常に高性能なパスを持たない点である。ROP などとは異なり、SO は現代的な GPU 設計における完全な市民とは (まだ?) みなされていないので、メモリチャンネル一つ程度のアクセスしか持たない。SO で大量のデータを生成するときはこの事実を頭に入れておく必要がある。この欠点は SO が必ず完全な浮動小数点を出力する事実によって悪化する。つまり頂点データに詰めた型を使って帯域を節約することはできない。

VS SO について最後に一つ: 先述したように、SO は組み立てられたプリミティブに対して操作を行う (個別の頂点ではない)。プリミティブアセンブリは隣接情報が伝わったとしても捨てるので、そしてプリミティブアセンブリは SO の前に起こるので、隣接情報に対応する頂点を SO バッファに入れることはできない。SO が個別の頂点ではなくプリミティブに対して操作を行うという事実は、例えば単一のスキン済みメッシュ (単一のポーズをしているもの) を複数回インスタンス化するときに重要になる。もし三角形メッシュを通常通りに描画して、その途中で SO を使うようにすると、データ量が爆発する──入力プリミティブ一つにつき三つの共有されていない未圧縮の頂点が生成されてしまう。これでも正しい動作は得られるが、SO と頂点入力の両方において帯域を効率的に活用できるとは言えない。そうではなく、最初のパスでは三角形メッシュを (インデックスの付いていない) 点リストとして描画し、各頂点に対してちょうど一度だけシェーディングを行うべきである。すると SO バッファには元の頂点バッファと一対一に対応するスキン済みの頂点が収まる。後はその頂点バッファと元のプリミティブトポロジーを表すインデックスバッファを使って描画を行えばよい。

ジオメトリシェーダー SO: 複数ストリーム

これはヌル GS を使った SO と基本的に同様だが、ジオメトリシェーダーが追加されるので新しい機能 (および複雑さ) が加わる。VS では出力ストリームが一つあるだけだった (ストリームは D3D11 以降の機能である──D3D10 レベルのハードウェアには存在しない)。そのストリームは SO に送ることも、パイプラインに送ってビューポート/クリッピング/カリングを行うこともできたが、選択肢はそれだけだった。これに対してジオメトリシェーダーでは複数のストリームを利用できるので、出力の振り分けが多少複雑になる。

基本的に、どの GS も (D3D11 では) 最大で四つのストリームに書き込める。各ストリームは SO ターゲットのそれぞれに送ることができる──「各」ストリームである: 一つのストリームを複数の SO ターゲットに書き込むこともできる。ただし一つの SO ターゲットは一つのストリームからだけ値を受け取れる。言い換えれば一対多の関係であり、完全な多対多の関係ではない。複数のストリームの存在は SO のバッファ処理にも影響を与える──ヌル GS では入力バッファが一つだけだったのが、ここではストリームごとに一つずつ複数の入力バッファが用意される。また SO ターゲット以外にも、最大で一つのストリームを 3D パイプラインに流すこともできる──言い換えると、通常のレンダリングパイプラインと SO を同時に利用できる。

ヌル GS の場合と同様に、SO は個別の頂点ではなくプリミティブに対して動作する──つまり GS で出力したストリップは SO に到着する前に完全な直線や三角形に展開される。

出力サイズの管理

もう一つ別の問題がある: SO が生成する出力データのサイズは必ず事前に分かるわけではない。この問題が GS を使ったときに発生するのは、起動されたそれぞれの GS が出力するプリミティブの個数が可変であるためである。しかし、より単純なヌル GS (VS だけ) の SO を使う場合であっても、インデックス付きのプリミティブが使われてさえいれば、「プリミティブカット」インデックスが飛ばされる可能性があるために SO バッファへ書き込まれるプリミティブの個数は変化する。後からその SO バッファを使って描画を行うとき、これは問題になる。そこにある頂点数が分からない! 個数の上限なら分かっている──バッファを作成したときの最大容量だ──が、それだけしか分からない。さて、これは何らかの問い合わせ機構を用意すれば解決できるが、少し考えれば分かるように、この解決法には明らかに変な部分がある: そもそも SO ユニットは現在の出力位置を知っているじゃないか! 問い合わせ機構を使ったとしたら、単一の 32 ビット値がバスからドライバに渡り、さらに API を通ってアプリケーションに届く──そしてアプリケーションはその値を使って描画をディスパッチすることになる。このとき同じ値が全てのレイヤーを逆方向に伝っていくだけだ。

というわけで、この問題はそのようには解かれない。代わりに DrawAuto が用意される。アイデアは非常に簡単である──GPU は出力バッファへ実際に書き込まれた正当な頂点の個数を知っており、SO ユニットはこの個数をメモリに保存している (一つの SO バッファへのレンダリングがマルチパスになる可能性があるため)。DrawAuto はこの個数をアプリケーションが明示的に送信する値の代わりに利用する──処理が非常に単純になり、コストの高いラウンドトリップが避けられる。先述の問い合わせ機構も存在することに注意してほしい──書き込まれた頂点の個数とオーバーフローの発生をチェックできる。しかしこれは SO バッファを使ったレンダリングのクリティカルパスに存在していないので、ドライバ開発者にとって物事がずっと簡単になる。

SO についてはなんとこれで以上である。今回はハードウェアの関する情報があまりなかったし、パイプライン的にもあまり面白くなかった。この章の執筆に時間がかかったのもそれが理由だ: すまなかった。次はテッセレーションである──これはすぐに書けるだろう。面白い話題だから :)

関連書籍 (Amazon アソシエイト)
Jim Blinn's Corner: A Trip Down the Graphics Pipeline
[増補改訂] GPUを支える技術 ── 超並列ハードウェアの快進撃
マンガとイラストでわかる! GPU最適化入門