DirectXの話 第168回

Variable Rate Shading

20/01/13 up

今回は昨年の19H1アップデートから使えてたらしい Variable Rate Shading を試してみました。

ハードウェアとドライバが対応していれば動作するはずですが、自宅ノートPCのRTX2060では動作しませんでした。

GPU自体は対応しているはずなので、ドライバの問題かなと思われます。

サンプルは例によってGitHubに上げています。

今回は Sample020 です。

GitHub

Variable Rate Shading とは?

Variable Rate Shading (以下 VRS) はピクセルシェーダの起動を1ピクセルごとではなく、複数ピクセル合わせて1回のピクセルシェーダとして起動してしまうという技術です。

例えば 2x2 を指定してピクセルシェーダを動作させると、2x2 のピクセルが1ピクセルとして計算され、その計算結果が 2x2 のピクセルに書き込まれます。

すごく大雑把に言うと、レンダーターゲットのサイズを変更せずに縮小バッファを実装する技術、ということになります。

縮小バッファとの違いは、拡大して元解像度に引き伸ばす手間が省けること、そしてレンダリングした範囲のみ対応できるということです。

使いやすそうな場面を考えるとパーティクルが挙げられるのではないでしょうか。

縮小バッファを利用したパーティクル描画は昔からある手法ですが、縮小した深度バッファを用意する必要があったり、拡大する際にバイラテラルアップサンプリングをしたりという手間がありました。

VRS を利用することでこれらの問題を解決できる可能性があるわけで、これはかなり有用といえるのではないでしょうか?

まあ、最近は画面全体の解像度を動的に変化させる Dynamic Resolution (動的解像度) が主流になってきてはいますが…

また、確実に使われそうな部分では VR 系の映像です。

これらは画面の中心 (主に視線が向いているので) に高解像度を適用し、周辺部分は低解像度にするという手法が現在も存在します。

この実装のために低解像度バッファを用意したりマスクしたりという手間が省けるなら大いに結構ではないでしょうか。

VR 以外でも複数解像度でレンダリングするという技術は昔から存在しています。

Deferred Rendering をするにあたってライティング計算は基本的に重い処理ではありますが、深度も法線もそれほど変化しないピクセル群であれば1つにまとめて処理してもいいのでは?という考え方が存在します。

Mixed Resolution Rendering と呼ばれるこの技術は GDC09 にて AMD が発表をしています

これも VRS で実現が用意になる手法と言えるでしょう。

VRS を使うために

VRS が使えるハードウェアは限られていて、さすがに DXR よりは広い範囲で使えるようなのですが、ドライバが対応してないとかで使えない場合も存在します。

なのでまず、VRS が使用可能かどうかをチェックしなければなりません。

これを実現するには以下のようなコードを書きます。

D3D12_FEATURE_DATA_D3D12_OPTIONS6 options = {};if (FAILED(device_.GetDeviceDep()->CheckFeatureSupport( D3D12_FEATURE_D3D12_OPTIONS6, &options, sizeof(options)))){ return false;}if (options.VariableShadingRateTier < D3D12_VARIABLE_SHADING_RATE_TIER_2){ return false;}

ID3D12Device::CheckFeatureSupport() 命令を用いて Option6 をチェックします。

チェックに失敗した場合は対応していないので false を返すことになります。

次の if 文では VRS の Tier をチェックしています。

本サンプルでは Tier2 が必要なので、Tier2 をサポートしていない場合は起動しません。

VRS には現在 Tier1 と Tier2 が存在し、有効な機能が異なってきます。

機能についての詳細はマイクロソフトのドキュメントを参考にしてください。

簡単に解説すると、Tier の違いによる機能の違いは以下のとおりです。

Per Draw は描画コールごとです。つまり、Draw~を行う度に変更が可能ということになります。

パーティクルでも、こっちのパーティクルはモヤモヤしたものなので 2x2 でレンダリング、別のパーティクルはエッジがしっかりわかるタイプだから 1x1 でレンダリング、ということが出来ますが、その描画コール内ではすべてが同じシェーディングレートになります。

Per Primitive はプリミティブごとです。

これを使うには頂点シェーダ、もしくはジオメトリシェーダ内で SV_ShadingRate セマンティクスをつけた値を出力する必要があります。

このセマンティクスは ShaderModel 6.4 でのみ使用できるっぽいので注意してください。

本サンプルでは使用していません。

Per Tile はタイルごとです。

VRS の Tier2 では画像リソースを指定することでタイルごとにシェーディングレートを変更できます。

タイルのサイズはGPUによって違ってくる可能性がありますが、RTX 2080 では 16x16 のタイルサイズでした。

用意する画像リソースはフォーマットが DXGI_FORMAT_R8_UINT で、幅と高さは画面解像度の幅と高さをタイルサイズで割った値を利用します。

各ピクセルには D3D12_SHADING_RATE の値を入力します。0 の場合は 1x1 のシェーディングレートが用いられます。

つまりこの画像を何らかのガイドを元に更新することで Mixed Resolution Rendering が実現できるというわけです。

VRS の設定方法

最近のグラフィクスAPIの新技術は設定が面倒なのが多いのですが、驚くことに VRS の設定はとても単純です。

まず Tier1 でも使用できる Per Draw の設定は、描画コールをする前に以下の命令を呼び出すだけです。

なんとたったの1行です。

ID3D12GraphicsCommandList5::RSSetShadingRate() 命令を使い、第1引数に使用したいシェーディングレートの値を入れるだけです。

今回のサンプルで VRS Type に [All MxN] と書かれているタイプはこの命令で第1引数を変化させているだけです。

Per Primitive や Per Tile でレンダリングする場合は少し命令数が増えますが、これまたとても簡単です。

増えた命令は ID3D12GraphicsCommandList5::RSSetShadingRateImage() だけです。

こちらはその名の通り、タイルごとにシェーディングレートが設定された画像リソースを指定するだけです。

使用しない場合は nullptr にしておけばOKです。

リソースの状態は D3D12_RESOURCE_STATE_SHADING_RATE_SOURCE にする必要があります。

もう1つ増えた要素は ID3D12GraphicsCommandList5::RSSetShadingRate() の第2引数です。

これはコンバイナを指定する必要があり、Tier2 の機能を利用する場合は必ず2要素の配列である必要があります。

コンバイナは Per Draw, Per Primitive, Per Tile のシェーディングレートの結果をどのように結合するかという指定です。

0番目の要素は Per Draw と Per Primitive の結合方法で、これは Per Primitive を指定していなくても設定する必要があります。

上のサンプルでは PassThrough が指定されていますが、この場合は Per Draw がそのまま使用されるという意味になります。

1番目の要素は前段 (つまり0番目要素) の結果と Per Tile の結合方法です。

サンプルでは Override が指定されており、この場合は Per Tile のみを有効にするという意味になります。

コンバイナは他にも Min, Max, Sum があります。

たとえば、パーティクル描画の際に基本は 2x2 でレンダリングするけど、深度が大きく分断されている部分は 1x1 にする、といった方法にも使えるってわけです。

でもまあ、あまり使わない気もしますが…

注意点としては、MSのドキュメントによるとタイル画像リソースは描画ターゲットや深度ターゲットには出来ないらしいです。

更新する場合は UAV として描画するか、更新した描画ターゲットをコピーして使うなどが必要そうです。

サンプルではコンピュートシェーダで更新しているので UAV を使っています。

パフォーマンスはどうなの?

MSのサンプルでは十分効果があったシェーディングレートなのですが、自分のサンプルではほとんど効果が出ていないという結果になっています。

これは Per Draw のみでやっていてもあまり効果がなくて、その理由としてはそもそもピクセルシェーダがあまりネックになっていないからだと思われます。

今回のサンプルではディレクショナルライトとスカイライトの計算を Deferred Rendering で行っているのですが、この程度だとピクセルネックにならないっぽいんですよね…

別のサンプル (Sample018) で試したときは Z Per Pass と Lighting Pass の両方に適用して少しばかり高速化したというくらいだったので、ネックになっていない状態ではわざわざ使う理由はないという感じです。

特に Per Tile の場合は画像リソースの更新作業があり、これによって逆に赤字になってしまっている状態です。

実際のゲームアプリケーションならピクセルシェーダネックになる場面は多いと思いますし、画像リソースの更新はコンピュートパイプでも可能なので最終的には黒字を目指せるんじゃないかと思いますが、ちょっとしたアプリ程度では不要かもしれません。

というわけで、ちゃんとボトルネックを確認してから実装しましょう。