High-Performance Software Rasterization on GPUs (2)
パイプラインの設計
- triangle setup
- ビューポートカリング
- 三角形のタイルへの割り付け
- ビューフラスタムカリング
- クリッピング
- bin/coarse rasterizer
- 頂点をタイルごとにまとめる
- fine rasterizer
この 4 つのステージがそれぞれ CUDA カーネル関数として用意されてて、CPU 側から順番に呼び出すかんじ。ソースコードでいうと src/framework/CudaRaster.cpp の 231 行目から、関数でいうと CudaRaster::launchStages(void) のところ。こうすると各ステージ内では処理順と依存関係を気にせず全力で処理できる。bin → coarse のとこでマージ(全タイル走査して自分とこで処理するやつを拾ってきてソート)のコストはかかるけど、bin 内で同期をとるよりはずっと速い。
各ステージの詳細
ものすごくざくっと書くけど、まぁ、ソースコード公開されてるし。これでいっか。読むのは src/curaraster/cuda の中身。
1. triangle setup
トライアングルごとにスレッドを起動。出力は配列に記録するけど、入力三角形のインデクスが1だったら配列のインデクス1の位置に記録する、みたいなことやってるから、入出力間で順序がズレるとかは気にしなくていい。in_arr[idx] = { i0, i1, i2 } みたいな三角形が入力されてきたら、out_arr[idx] の位置にその三角形の処理結果をいれとく、でいいのかな。
クリッピングで三角形が増えたら、その増えた三角形への参照を out_arr[idx] にいれとく。こうしとけば out_arr を動的確保しなくて済む。その参照先は動的確保しなきゃだけど、そもそもそんなん全体からみればレアケースだし。
あと、カリングのロジックがよくわからない。なんでこれでビューフラスタムの内外判定になってるんだろう。。
if (v0.w < fabsf(v0.x) | v0.w < fabsf(v0.y) | v0.w < fabsf(v0.z)) { if ((v0.w < +v0.x & v1.w < +v1.x & v2.w < +v2.x) | (v0.w < -v0.x & v1.w < -v1.x & v2.w < -v2.x) | (v0.w < +v0.y & v1.w < +v1.y & v2.w < +v2.y) | (v0.w < -v0.y & v1.w < -v1.y & v2.w < -v2.y) | (v0.w < +v0.z & v1.w < +v1.z & v2.w < +v2.z) | (v0.w < -v0.z & v1.w < -v1.z & v2.w < -v2.z)) { triSubtris[taskIdx] = 0; return; } }
2. bin rasterizer
SM ごとにいっこの CTA を起動して、そこで 512 個の三角形を処理する。前段の出力配列には 1 要素(input batch)あたり 0 から 7 個の三角形がはいってるから、まずはそこから、512 個取り出す。全部の CTA がこの処理を終えるまでが第一段階。
次に、どの三角形(の AABB)がどの Bin を覆うのかを計算する。図を見るとわかりやすいけど、各三角形ごとに 0~4 個。ここで Bin 単位の各三角形の描画位置が決まる、つまり、描画位置を基準にして三角形をソートしてることになる。この計算は各 CTA 内で完結してるから、同期を取る必要はない。
3. coarse rasterizer
前段と同じく、ここでも 512 個ずつ三角形を処理する。前段で計算した「どの三角形がどの Bin に書き込まれるか」を頼りに、自分が担当する Bin にかかる三角形を入力にする。
次に 1 スレッドあたり 1 三角形の割り振りで、どの三角形がどの Tile を覆うかを計算する。で、この処理も各 CTA ごとに独立してるから同期は取らない。
4. fine rasterizer
いわゆるラスタライズと呼ばれる処理。1 タイルあたり 20 個の warp を起動して、各 warp が 32 個単位で三角形を処理してく。
第一段階では、三角形単位でのアーリーデプステストをやって、そっからピクセルカバレッジを計算、結果をルックアップテーブルに書き込んでく。フラグメントが 32 個たまったら第二段階へ。
第二段階では、1 フラグメント 1 スレッドでラスタライズ処理をする。デプステストしてシェーダ動かしてから、最後に ROP。シェーダまではブロックの共有メモリ上でできるんだけど、それだけじゃメモリ足りないからグローバルメモリ上で直接 ROP しなきゃなんないのが悩みどころなんだってさ。