サヨナラGLSL…

こんにちわ,Pocolです。
久しぶりにa3dの更新作業をやっています。
今日は,ついにGLSLから脱却する対応を入れました。

…といっても大したことはしていなくて,DXCからSpir-Vを生成するようにしただけです。
ただ,普通のHLSLの記述だと対応できないそうなので,マクロをかまして対応するという感じにしました。
マクロの定義は下記の通り。

#ifdef __spirv__
#define A3D_LOCATION(index)              [[vk::location(index)]]
#define A3D_RESOURCE(var, reg, index)    [[vk::binding(index)]] var
#define A3D_FLIP_Y(var)                  var.y = -var.y
#else
#define A3D_LOCATION(index)
#define A3D_RESOURCE(var, reg, index)    var : register(reg)
#define A3D_FLIP_Y(var)
#endif

Vulkanの座標系はDirectXと比べて上下逆になる座標系なので,フリップ用のマクロも定義してあります。
実際のシェーダは下記のように書きます。

///////////////////////////////////////////////////////////////////////////////////////////////////
// VSOutput structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct VSOutput
{
    A3D_LOCATION(0) float4 Position : SV_POSITION;
    A3D_LOCATION(1) float2 TexCoord : TEXCOORD;
};

///////////////////////////////////////////////////////////////////////////////////////////////////
// PSOutput structure
///////////////////////////////////////////////////////////////////////////////////////////////////
struct PSOutput
{
    A3D_LOCATION(0) float4 Color : SV_TARGET0;
};

//-------------------------------------------------------------------------------------------------
// Samplers and Textures
//-------------------------------------------------------------------------------------------------
A3D_RESOURCE(SamplerState ColorSmp, s0, 1);
A3D_RESOURCE(Texture2D    ColorMap, t0, 2);

//-------------------------------------------------------------------------------------------------
//      ピクセルシェーダメインエントリーポイントです.
//-------------------------------------------------------------------------------------------------
PSOutput main(VSOutput input)
{
    PSOutput output = (PSOutput)0;

    output.Color = ColorMap.Sample( ColorSmp, input.TexCoord );

    return output;
}

あとは,dxc.exeで -spirv のオプションを付けてコンパイルするだけです。
DirectX用とVulkan用のシェーダバイナリの両方が1つのシェーダコードから生成できます。

これで,GLSLともようやくサヨナラです。

GPU最適化備忘録

こんにちわ,Pocolです。
今日はGPU最適化のための自分用のメモを書いておこうと思います。
自分がある程度理解できればいいことを前提に書いているので,「そもそも間違っている」とか「全然正しくない」とか「こうした方がもっといい」とかあれば,ご指摘ください。

P3 Method

基本的には,Louis Bavoil氏が書いている「The Peak-Performance-Percentage Analysis Method for Optimizing Any GPU Workload」に従ってボトルネックを探し,改善を行っていきます。GDCでも講演があるので,興味がある方はそちらの動画を見ると良いと思います。
この手法は次のステップで最適化を行います。

(1) “Top SOL%”の値をチェックする。
– (A) 80%より大きいの場合: SOLユニットから処理を取り除くことを試みる。
– (B) 60%より小さいの場合: top SOL%の値が増加するように試みる。
– 上記以外の場合: (A)と(B)の両方を行う。

(2) Top SOLユニットがSMの場合(あるいは,SOL%の項目が近い場合)
 “SM Throughput for Active Cycles”の値をチェックする。
– (C) 80%より大きい場合: 命令群を適宜スキップしてみたり、特定の計算をルックアップテーブルに移行することを検討してみる。
– (D) 60%より小さい場合: SM占有率(実行中のアクティブワープ数)の増加を試みる & SM発行ストールサイクルの数を減らすことを試みる
– 上記以外の場合: (C)と(D)の両方を行う

(3) その他がTop SOLユニットの場合、そのユニットに送られる作業量を減らすことを試みる。

ここまで出てきた用語は次の意味です。

  • SOL:Speed Of Light。最大スループットを意味する。
  • SM:Streaming Multiprocessor。共有メモリやコンスタンスとキャッシュ,テクスチャキャッシュ,複数のスカラープロセッサなどから構成されるもの。ものすごく雑に言えばUnified Shaderのこと。
  • Throughput。ハードウェアが単位時間あたりに処理できるデータ量のこと。あるいは,その数値を使ってデータ処理能力やデータ転送速度を表す。

さて,最適化する上で肝心なのは「Top SOLをどうやって調べるか?」ということになります。これはNVIDIA Nsight Graphicsを使って調べることができます。
まず,Nsightを立ち上げる前にパフォーマンスカウンターなどを取得できるように設定を変更しておきます。NVIDIAコントロールパネルを開きます。Windowsのタスクバー右下にある^をクリックして,NVIDIA設定を右クリックし,NIVIDIAコントロールパネルを選択します。選択してもコントロールパネルが開かない場合は,何らかの異常が発生している恐れがあるのでグラフィックスドライバーを再インストールして改善するか試してみてください。コントロールパネルを開いたら,メニューの「デスクトップ」から「開発者設定を有効にする」にチェックを入れておきます。これでカウンター等の値がとれるようになるはずです。

図1.NVIDIAコントールパネルでの設定

続いて計測を行います。Nsight Graphicsを立ち上げFrame Profilerとしてアプリケーションを立ち上げます。立ち上げ方はConnect to processのウィンドウで,Activityの項目をFrame Profilerに設定して,Application Executableにアプリケーション実行パスを指定,Working Directoryに作業ディレクトリを設定してください。自動的にアタッチする場合はAutomatically ConnectにYesを,Steamなどのようにいったん別exeをかましてから立ち上げる場合はNoに設定しておき,Target PlatformでAttachタブを選択してから,プロセスにアタッチするという方法を取ります。
アプリケーションが立ち上がったら,左上にオーバーレイが表示されるので,F11を押して測定したいフレームをキャプチャーします。

図2.オーバーレイの表示

キャプチャーすると,画面右上にRange Profilerというものが表示されるので,調べたいドローコール部分をRange Profilerでクリックします。クリックすると,少し時間をおいてから次のようにRange InfoやPipeline Overviewなどが表示されます。

図3.フレームキャプチャー

あとは,Pipeline Overviewに表示されるSM / L2 / VRAM / CROP / RAS の値を見て上記のステップに沿って改善を行います。

図4.Pipeline Overview

(A) Top SOL% > 80% の場合

GPU上で非常に効率的に動作していることが分かります。高速化するためには,このTop SOLユニットを処理から削除し,他のユニットのボトルネックを探していきます。例えば SM が Top SOL ユニットの場合は,命令群をスキップしてみたりなど。もう一つの例としては構造化バッファのロードを定数バッファに移して高速化するなどです。

(B) Top SOL% < 60% の場合

トップ SOL%の値が60%未満場合、トップ SOL ユニットと SOL% が低い他のすべての GPU ユニットは、使用率が低い(アイドルサイクル)、非効率的に動作している(ストールサイクル)、または与えられた作業負荷の仕様により高速パスに当たらないということを意味します。このような状況の例としては、以下のようなものがあります。

  • アプリケーションが一部CPUに制限される
  • Wait For IdleコマンドやGraphic←→ComputeスイッチでGPUパイプラインを何度も消耗している
  • テクスチャオブジェクトからのTEXフェッチは、フォーマット、次元、またはフィルタモードによって、設計上、スループットが低下して実行されます(GTX 1080のこれらの合成ベンチマークを参照)。例えば、3Dテクスチャをトライリニアフィルタリングでサンプリングする場合、50%のTEX SOL%が期待されます。
  • TEXやL2ユニットのキャッシュヒット率が低い、VRAMアクセスがまばらでVRAM SOL%が低い、GPU VRAMではなくシステムメモリからVB/IB/CB/TEXをフェッチするなど、メモリサブシステムの非効率性。
  • 入力アセンブリの32ビットインデックスバッファのフェッチ(16ビットインデックスに比べ半減)

この場合、トップSOL%の値を使用して、非効率な処理を削減することによってこの処理負荷で達成できる最大利得の上限を導き出すことができます。

(C) SM Throughput For Active Cycles > 80% の場合

SM がトップ SOL ユニットで、「SM Throughput For Active Cycles」が 80%より大きい場合、現在の処理負荷は主に SM スケジューラの発行率によって制限されているため、SM の占有率を上げてもパフォーマンスは大きく向上しません(少なくとも、処理負荷は 5%以上増加しない)。

この場合,次のステップは、SMスケジューラの帯域幅を飽和させているのはどのような命令かを把握することです。典型的なのは算術命令(FP32や整数演算)ですが、テクスチャフェッチや共有メモリアクセスなどのメモリ命令もあり得えます。また、SMがトップSOLユニットで、SM Throughput for Active Cyclesが80%以上のワークロードでは、TEXユニットもSMのSOL%に近い値でなければ、TEX命令(SRVおよびUAVアクセス)が性能を制限している可能性はないでしょう。

(D) SM Throughput For Active Cycles < 60% の場合

このGTC 2013の14分の講演のスライド15にあるように、あるワープ命令が発行できない場合(オペランドの準備ができていない、あるいは実行に必要なパイプラインサブユニットの準備ができていないため、これをワープストールと呼びます)、SM命令スケジューラは、別のアクティブワープに切り替えてレイテンシーを隠蔽しようとします。そこで、SMスケジューラがSMアクティブサイクルあたりにより多くの命令を発行できるようにするには、2つの方法があります。

(1)SMの占有率(スケジューラが切り替え可能なアクティブなワープの数)を上げる
(2)SM発行ストール待ち時間を短縮する(ワープがより少ないサイクルでストール状態に留まるようにする)。

アプローチ1:SM占有率を増加する

SMがトップSOLユニット(またはそれに近い)、「アクティブサイクルのSMスループット」60%未満であれば、SMの占有率を上げるとパフォーマンスが向上するはずですが、まず何が制限になっているかを把握する必要があります。

ピクセルシェーダとコンピュートシェーダで最も一般的な SM 占有率のリミッタは、シェーダが使用するスレッドごとの ハードウェアレジスタの数です。

ハードウェアのレジスタ数が最大理論占有率(アクティブサイクルあたりのアクティブワープ数)に与える影響は、CUDA Occupancy Calculatorで確認できます。
Maxwell、Pascal、Volta GPU では、レジスタの他に、以下のリソースでも SM 占有率を制限することができます。

グラフィックスシェーダについて:

  • 頂点シェーダー出力アトリビュートの合計サイズ。
  • ピクセルシェーダーの入力アトリビュートの合計サイズ
  • HS、DSまたはGSの入力および出力アトリビュートの合計サイズ。
  • ピクセルシェーダの場合、ピクセルワープのアウトオブオーダー完了(通常、動的ループや早期終了ブランチなどの動的制御フローに起因する)。CS のスレッドグループは任意の順序で完了できるため、CS にはこの問題があまりないことに注意してください。Gareth Thomas & Alex Dunnによる「Practical DirectX 12」についてのGDC 2016講演のスライド39をご覧ください。

コンピュートシェーダについて:

  • スレッドグループのワープがSM上でall-or-none方式で起動するため、スレッドグループのサイズはSMの占有率に直接影響します(つまり、スレッドグループのすべてのワープが必要なリソースを利用でき、一緒に起動するか、または何も起動しないかです)。スレッドグループのサイズが大きくなると、共有メモリやレジスタファイルなどのリソースの量子化が粗くなります。アルゴリズムによっては純粋に大きなスレッドグループを必要とする場合もありますが、それ以外の場合は、開発者はできるだけスレッドグループのサイズを64スレッドまたは32スレッドに制限するようにすべきです。これは、64 または 32 スレッドサイズのスレッドグループが、シェーダプログラムに最適なレジスタターゲットを選択する際に、シェーダコンパイラに最も柔軟性を与えるからです。
  • さらに、レジスタ使用量が多く (>= 64)、SM (HLSL の GroupMemoryBarrierWithGroupSync()) におけるスレッドグループバリアのストール時間が長いシェーダでは、スレッドグループのサイズを 32 に下げれば、64 と比較してスピードアップする可能性もあります。64 レジスタのガイダンスにより、SM あたり最大 32 スレッドグループの制限(CUDA Occupancy Calculator のアーキテクチャ依存の「Thread Blocks / Multiprocessor」制限)が、SM 占有率の主要制限要因にならないようにします。
  • CUDA Occupancy Calculatorに様々な数値を入力することで、スレッドグループごとに割り当てられる共有メモリの総バイト数もSMの占有率に直接影響を与えることが分かります。
  • 最後に、短いシェーダ(例えば、TEX命令と2つの算術命令)を持つDispatchコールのシーケンスでは、SMの占有率は、上流ユニットのスレッドグループの起動率によって制限される場合があります。この場合、連続したDispatchコールをマージすることが有効な場合があります。

CUDA Nsightのドキュメントページ「Achieved Occupancy」には、Compute ShadersのSM占有率制限の候補がいくつか挙げられています。

  • スレッドグループ内の処理負荷が偏っている。
  • 起動したスレッドグループが少なすぎる。これは、GPU Wait For Idles の間に SM を完全に占有するのに十分なワープを起動しないグラフィックシェーダの問題にもなりえます。

注:スレッドグループのサイズを小さくするには、実際には2つのアプローチがあります。

  • アプローチ1:スレッドグループのサイズをN倍に下げ、同時にグリッドの起動次元をN倍にする。
  • アプローチ2:N>=2個のスレッドの作業を1個のスレッドに統合する。これにより、マージされたN個のスレッド間でレジスタを介して共通データを共有したり、アトミック演算(HLSLのInterlockedMinなど)を用いて共有メモリではなくレジスタで削減を実行したりすることができます。さらに、このアプローチには、N個のマージされたスレッド間でスレッドグループの統一操作を自動的に償却する利点もあります。しかし、この方法によるレジスタの肥大化の可能性には注意する必要があります。

注:もし、あるワークロードの SM 占有率が、主にフルスクリーン・ピクセルシェーダとコンピュー トシェーダのどちらでレジスタカウントが制限されているかを知りたい場合は、次のようにしてください。

  • スクラバーで追加し… “Program Ranges” を実行し、調査したいシェーダプログラムの範囲を見つけます。Program Range を右クリックし、Range Profiler を起動し、Pipeline Overview Summary の “SM Occupancy” 値を確認します。
  • スクラバーの「Time(ms)」行をクリックして、Range内のいくつかのレンダーコールを選択します。タブを API Inspector に切り替え、ワークロードのサイクルのほとんどを占めるシェーダステージ(PS または CS)を選択し、シェーダ名の横にある「Stats」リンクをクリックします。
  • 「Shaders」Nsightウィンドウが表示され(下図14参照)、「Regs」列にシェーダのハードウェアレジスタ数が表示されます。シェーダの統計情報が入力されるまで数秒待つ必要があることに注意してください。
  • CUDA Occupancy Calculator グラフを使用して、このレジスタカウントに関連する Max Theoretical Occupancy を調べ、このシェーダの Range Profiler が報告する実際の 「SM Occupancy」 と比較します。
  • もし、達成した占有率が最大占有率よりずっと低ければ、SMの占有率はスレッドごとのレジスタの量だけでなく、他の何かによって制限されていることが分かります。

あるシェーダに割り当てられるレジスタの総数を減らすには、DX シェーダのアセンブリを見て、シェーダの各ブランチで使用されるレジスタの数を調べればよいです。ハードウェアは、最もレジスタを必要とするブランチに対してレジスタを割り当てる必要があり、そのブランチをスキップするワープは、最適ではない SM 占有率で実行されます。

フルスクリーンパス(ピクセルまたはコンピュートシェーダ)の場合、この問題に対処する典型的な方法は、ピクセルを異なる領域に分類するプリパスを実行し、各領域に対して異なるシェーダの並べ替えを実行することです。

  • コンピュートシェーダについては、このSIGGRAPH 2016のプレゼンテーションでは、シェーダの順列ごとにスレッドブロックの数を変えながら、画面上の異なるタイルに特殊なコンピュートシェーダを適用するためにDispatchIndirectコールを使用したソリューションが説明されています。
    “Deferred Lighting in Uncharted 4” – Ramy El Garawany (Naughty Dog).
  • ピクセルシェーダについては、異なる特殊化アプローチを使用することができます。フルスクリーンのステンシルバッファをプリパスで満たし、ピクセルを分類することができます。それから、ピクセルシェーダ実行の前に起こるステンシルテストに依存し(これは我々のドライバによって自動的に行われるべきです)、現在のシェーダの順列が触れていないピクセルを破棄するステンシルテストを使用することによって、複数の描画コールを効率的に実行することができます。このGDC 2013のプレゼンテーションでは、このアプローチでMSAA遅延レンダリングを最適化します。”The Rendering Technologies of Crysis 3″ – Tiago Sousa, Carsten Wenzel, Chris Raine (Crytek).

最後に、あるコンピュートシェーダのSM占有率制限をよりよく理解するために、我々のCUDA Occupancy Calculatorスプレッドシートを利用することができます。使用するには、GPU の CUDA Compute Capability とシェーダのリソース使用量(スレッドグループサイズ、Shader View からのレジスタ数、共有メモリサイズ(バイト))を記入するだけです。

アプローチ2:SM発行ストール待ち時間を減らす

SM占有率を上げる以外の方法でSM Throughput For Active Cyclesを上げるには、SM issue-stall cyclesの回数を減らすことです。これは、命令発行サイクル間のSMアクティブサイクルで、オペランドの1つがレディでない、または命令を実行する必要があるデータパス上のリソース競合が原因で、ワープ命令がストールしている間です。

PerfWorksのメトリクスsmsp__warp_cycles_per_issue_stall_{reason}は、ワープが{reason}で停止した命令イシュー間の平均サイクルを示しています。PerfWorksメトリックsmsp__warp_stall_{reason}_pctは、サイクルごとにその理由で停止したアクティブなワープの%です。これらのメトリックは、Range ProfilerのUser Metricsセクション、およびSM Overview Summaryセクションで、降順にソートされて公開されています。ワープが停止する理由としては、以下のようなものが考えられます。

  1. “smsp__warp_stall_long_scoreboard_pct” PerfWorks メトリクスは、L1TEX (local, global, surface, tex) オペレーションのスコアボード依存を待つためにストールしたアクティブワープのパーセンテージを示します。
  2. “smsp__warp_stall_barrier_pct” PerfWorks指標は、スレッドグループのバリアで兄弟ワープを待つためにストールしたアクティブワープのパーセンテージを示します。この場合、スレッドグループのサイズを下げるとパフォーマンスが向上する場合があります。これは、各スレッドが複数の入力要素を処理するようにすることで実現できる場合があります。

“sm__issue_active_per_active_cycle_sol_pct” が 80% より低く、 “smsp__warp_stall_long_scoreboard_pct” がワープストールの理由のトップなら、そのシェーダは TEX-latency limited であることが分かっているはずです。これは、失速サイクルのほとんどが、テクスチャフェッチ結果との依存関係 から来ていることを意味します。この場合は…

  1. シェーダのコンパイル時に反復回数がわかるループがある場合(ループ回数ごとに異なるシェーダの並べ替えを使用する場合もある)、HLSL の [unroll] ループ属性を使用して FXC にループを完全に展開させるようにしてみてください。
  2. シェーダが完全にアンロールできない動的ループ(たとえば、レイマーチ ングループ)を実行している場合は、テクスチャフェッチ命令をバッチして、TEX 依存のストールの数を減らしてください(HLSL レベルで、 独立したテクスチャフェッチを 2~4 のback-to-back命令のバッチに まとめることによって)。
  3. シェーダが与えられたテクスチャのピクセルごとにすべての MSAA サブサンプルを反復している場合、そのテクスチャの TEX 命令の 1 バッチで、すべてのサブサンプルを一緒にフェッチします。MSAA サブサンプルは VRAM 内で隣り合わせに保存されるため、一緒にフェッチすれば TEX ヒット率は最大になります。
  4. テクスチャの負荷が、ほとんどの場合、真になると予想される条件に基づいている場合(例えば、if (idx < maxidx) loadData(idx))、負荷を強制して座標をクランプすることを検討する(loadData(min(idx,maxidx-1)))。
  5. TEXキャッシュとL2キャッシュのヒット率を向上させることで、TEXレイテンシーを減少させてみてください。TEX および L2 ヒット率は、サンプリングパターンを微調整して隣接ピクセル/スレッドがより多くの隣接テクセルをフェッチするようにし、該当する場合はミップマップを使用し、さらにテクスチャのサイズを小さくしてよりコンパクトなテクスチャフォーマットを使用することによって向上させることが可能です。
  6. 実行されるTEX命令の数を減らしてみてください(おそらく、TEX命令述語としてコンパイルされる、テクスチャ命令ごとのブランチを使用します、例としてFXAA 3.11 HLSLを参照してください、例.”if(!doneN) lumaEndN = FxaaLuma(…);”).

トップSOLユニットがSMではない場合

どのユニットかを確認して,下記に従って対処を行います。

(1)トップSOLユニットが,TEX, L2, あるいは VRAMの場合

トップ SOL ユニットが SM ではなく、メモリサブシステム ユニット(TEX-L1、L2、および VRAM)の 1 つである場合、性能低下の根本原因は、GPUフレンドリーではないアクセスパターン(通常、ワープ内の隣接スレッドが遠く離れたメモリにアクセスする)により発生した TEX または L2 キャッシュのスラッシングである可能性があります。この場合、最上位制限ユニットは TEX または L2 であるかもしれませんが、根本的な原因は SM によって実行されるシェーダにある可能性があるため、最上位 SOL ユニットが SM の場合を使用して SM のパフォーマンスをトリアージ方法で決定する価値があります。

トップ SOL ユニットが VRAM であり、その SOL% 値が悪くない(60% 以上)場合、このワークロードは VRAM スループットが制限されており、別のパスでマージすることでフレームが高速化されるはずです。典型的な例は、ガンマ補正パスと他のポストプロセッシングパスをマージすることです。

(2)トップSOLユニットがCROPあるいはZROPの場合

CROP がトップ SOL ユニットである場合、より小さいレンダーターゲット形式(例:RGBA16F の代わりに R11G11B10F)を使用してみたり、Multiple Render Targets を使用している場合、レンダーターゲットの数を減らしてみたりすることができます。また、ピクセルシェーダでより積極的にピクセルをキルすることは価値があるかもしれません(たとえば、特定の透明効果では、不透明度が1%未満のピクセルを破棄する)。透明レンダリングを最適化するための可能な戦略については、「透明(または半透明)レンダリング」を参照してください。

ZROP が Top SOL ユニットである場合、より小さい深度フォーマット(例:シャドウマップ の D24X8 の代わりに D16、または D32S8 の代わりに D24S8)を使用し、さらに不透明オブジェクトを前から後ろへの順番に描いて、ZCULL(粗視化深度テスト)が ZROP とピクセルシェーダを起動する前に多くのピクセルが廃棄できるようにするとよいでしょう。

(3)トップSOLユニットがPDの場合

前述したように、PDは入力された頂点のインデックスを収集するために、インデックスバッファのロードを行います。PDがトップSOLユニットである場合、32ビットではなく16ビットのインデックスバッファを使用してみることができます。それでも PD の SOL%が増加しない場合は、頂点の再利用とローカリティのためにジオメトリを最適化することを試してみることができます。

(4)トップSOLユニットがVAFの場合

この場合、頂点シェーダーの入力アトリビュートの数を減らすと効果的です。
また、位置ストリームを他のアトリビュートから分離することは、z-only またはシャドウマップレンダリングに有効な場合があります。


頂点アトリビュートを減らす

前述した,P3 Methodでも出てきますが,頂点アトリビュートを減らすと高速になるケースがあります。プラットフォームによってはドキュメントに「XXX以下にすると速くなります」みたいな記述が載っていたり,実際にどのぐらい数でどういう結果が得られたのかをグラフ化してくれていたりします。
ここで重要なのは,なるべくパッキングして数を減らすということです。例えば,下記のようにします。

//変更前
struct VSOutput
{
    float4 Position : SV_POSITION;
    float2 TexCoord0 : TEXCOORD0;
    float2 TexCoord1 : TEXCOORD1;
    float2 TexCoord2 : TEXCOORD2;
    float2 TexCoord3 : TEXCOORD3;
};

// 変更後
struct VSOutput
{
    float4 Position : SV_POSITION;
    float4 TexCoord01 : TEXCOORD01; // TEXCOORD0 and TEXCOORD1
    float4 TexCoord23 : TEXCOORD23; // TEXCOORD2 and TEXCOORD3
};

データ容量は変わっていないですが,これだけで高速化します。過去の経験ではモデル描画のシェーダに対して適用したところ全体で1[ms]程度の高速化が出来た記憶があります。とにかくピクセルシェーダで使っていないアトリビュートがあったら,削る。削れないときはまとめることによって数を減らすということを考えると良いです。


サイズを減らす

計算等に利用するテクスチャサイズを減らすことによってテクスチャキャッシュに載りやすくし,高速化するテクニックがあります。例えば,Deinterleaved RenderingCheckerboard Renderingといったものがこれに当たります。特に縮小バッファにして描画品質があまりにも落ちすぎるのを避けたい場合などに使うと良いです。


テクスチャフェッチをバッチする

NVIDIAのBlog記事にもありますが,レイマーチループのような動的ループ内でテクスチャフェッチをバッチかすることによってTEXのレイテンシーを隠すことができます。
典型的なSSRレイマーチングループのHLSLは次のような感じです。

float MinHitT = 1.0;
float RayT = Jitter * Step + Step;

[loop] for ( int i = 0; i < NumSteps; i++ )
{
     float3 RayUVZ = RayStartUVZ + RaySpanUVZ * RayT;
     float SampleDepth = Texture.SampleLevel( Sampler, RayUVZ.xy, GetMipLevel(i) ).r;

     float HitT = GetRayHitT(RayT, RayUVZ, SampleDepth, Tolerance);
     [branch] if (HitT < 1.0)
     {
          MinHitT = HitT;
          break;
     }

     RayT += Step;
}

ループ内で,隣り合わせで2回テクスチャフェッチするように処理を置き換えます。

float MinHitT = 1.0;
float RayT = Jitter * Step + Step;
[loop] for ( int i = 0; i < NumSteps; i += 2 )
{
     float RayT_0 = RayT;
     float RayT_1 = RayT + Step;

     float3 RayUVZ_0 = RayStartUVZ + RaySpanUVZ * RayT_0;
     float3 RayUVZ_1 = RayStartUVZ + RaySpanUVZ * RayT_1;

     // batch texture instructions to better hide their latencies
     float SampleDepth_0 = Texture.SampleLevel( Sampler, RayUVZ_0.xy, GetMipLevel(i+0) ).r;
     float SampleDepth_1 = Texture.SampleLevel( Sampler, RayUVZ_1.xy, GetMipLevel(i+1) ).r;

     float HitT_0 = GetRayHitT(RayT_0, RayUVZ_0, SampleDepth_0, Tolerance);
     float HitT_1 = GetRayHitT(RayT_1, RayUVZ_1, SampleDepth_1, Tolerance);

     [branch] if (HitT_0 < 1.0 || HitT_1 < 1.0)
     {
          MinHitT = min(HitT_0, HitT_1);
          break;
     }

     RayT += Step * 2.0;
}

ブログ記事では,GTX1080で0.54[ms]だったものが0.42[ms]に高速化したと記載されています。また2回ではなく4回にすることで0.54[ms]から0.33[ms]になるという記述もあります。レイマーチ処理は基本的に重くなりやすいので,アルゴリズム的に最適化出来ない場合は,こうしたバッチ化により高速化するとよさそうです。

読んどけサイト

こんにちわ。Pocolです。
たま~に、会社で「おすすめのサイトとかありますか?」
…って聞かれることがあるので,おすすめサイト紹介しておきます。

論文系

  • https://kesen.realtimerendering.com/sig2020.html
  • http://trowley.org/
  • https://www.youtube.com/channel/UCbfYPyITQ-7l4upoX8nvctg

カンファレンス系

  • https://advances.realtimerendering.com/
  • https://blog.selfshadow.com/
  • https://www.gdcvault.com/
  • https://www.youtube.com/user/DigitalDragonsKrakow
  • https://www.youtube.com/c/GCAPConf
  • https://cedil.cesa.or.jp/
  • http://www.gc-conf.com/

まとめ系

  • https://www.jendrikillner.com/tags/weekly/
  • https://halisavakis.com/category/technically-art/
  • https://hanecci.com/
  • https://3dnchu.com/

チュートリアル or 講義系

  • https://learnopengl.com/
  • https://80.lv/
  • https://www.youtube.com/playlist?list=PLplnkTzzqsZTfYh4UbhLGpI5kGd5oW_Hh
  • https://www.cg.tuwien.ac.at/courses/Rendering/VU

レイトレ関係

  • https://www.pbrt.org/
  • http://graphics.stanford.edu/papers/veach_thesis/
  • http://kagamin.net/hole/something.htm
  • https://rayspace.xyz/
  • https://github.com/hi2p-perim/minpt
  • https://www.youtube.com/channel/UCXU-2zZoL9R6y2Tv8C2tSQw
  • https://raytracing.github.io/
  • https://cg.informatik.uni-freiburg.de/intern/seminar/raytracing%20-%20Dutre%20-%202002%20-%20Advanced%20global%20illumination%20SIGGRAPH.pdf
  • https://cgg.mff.cuni.cz/~jaroslav/papers/2013-ltscourse/index.htm
  • https://dl.acm.org/doi/10.1145/2504435.2504439
  • https://adrien-gruson.com/research/2018_GradientCourse/
  • https://shuangz.com/courses/pbdr-course-sg20/

順調みたいです。

こんばんわ、Pocolです。

お陰様で,今のところは”Direct3D12 ゲームグラフィックス実践ガイド”の売り上げが好調みたいです。
本について,読んでみて良かったら是非他の人にお勧めして欲しいのですが,その際に注意して欲しい点があります。
前々から言っているように,数学についての知識とC++についてついての知識をある程度必要とし,理系大学生の教養レベルの知識は必要となります。
また,自分がよかれと思って本書を奨めても,「全然良く分からないし,何なのこのクソ本!」と逆に自分の評価を下げてしまう可能性もあります。
本書を他者に進める場合も,その人の特性を見て薦めるかどうか判断して頂くと良いかと思います。
判断できない場合は,ちろっと読んで理解できなさそうであれば買うのをやめたが方がいいと言って頂いた方が良いです。専門書ですので…

まぁ,兎にかく合う合わないが個人によって非常に分かれる本だと思いますので,
現物を一回見て判断して頂くのが確実な方法かと思います。

個人的には,専門学校生には積極的におススメできない類の本です。
専門学校生の方は,DirectX12の魔導書などと比較してみて自分に合う方を買われると良いかと思います。

今年もありがとうございました。

こんばんわ。Pocolです。
本年度、最後の更新です。今年一年を振り返ろうと思います。

まずは,自分がシステム構築など行ったゲームが無事に世に出て良かったです。
ほとんどやったのはシステム構築だけで最後まで関われなかったので,
製品まできちんと出来たのは自分以外の人の努力の結晶だと思います。
間接的ながら,少しだけ仕事に関わることが出来て良かったと思います。
また,ミリオンも達成できて良かったと非常に思います。
いつも,エンジンやらライブラリやらしか作っていないので,
製品のスタッフロールに載ることは殆ど無いのですが… 
(XXX Team やら XXX 株式会社なりでまとめられてしまうので,名前が載ることはないのです)
今回はスタッフロールに載せて頂けたので,非常に良い経験となりました。

10月には,皆様に大変お待たせしましたが,ようやく執筆していた本を発売することができました。
レビューアの皆様ならび,編集担当の落合様には大変互助力をしていただきました。
この場で,再度お礼を申し上げます。ありがとうございました。
来年には,皆で打ち上げやりましょう!

後は最近うちのホームページなんか見ている人これっぽっちもいないだろうと思っていて,そろそろpublicなホームページは辞めたいなって思っています。
privateなページにして,のほほんとやっていきたい。自由に書きたい。
Githubに人知れずこっそりと記事をあげたりとか,今あんまり使っていないSlackとかで投稿するのも良いかもしれないなぁって感じています。
…ということで,そのうちページ閉じるかもという予告でした。

来年はどんな一年になるんでしょうか?
楽しい1年にしたいな。
それでは,皆様良いお年を!

Errata : Direct3D12ゲームグラフィック実践ガイド

大変申し訳ございません。
下記の通り、誤記がありましたので謹んでお詫びして訂正いたします。

P.245 中央部の数式
【誤】
\begin{eqnarray}
x &=& r \sin \theta \cos \theta \\
y &=& r \cos \theta \\
z &=& r \sin \theta \sin \theta
\end{eqnarray}

【正】
\begin{eqnarray}
x &=& r \sin \theta \sin \phi \\
y &=& r \cos \theta \\
z &=& r \sin \theta \cos \phi
\end{eqnarray}


P.245 図7.2
【誤】

【正】


P.341 リスト10.16
【誤】

auto invW = 1.0f / float(w - 1);
auto invH = 1.0f / float(h - 1);

【正】

auto invW = 1.0f / float(w);
auto invH = 1.0f / float(h);

サンプルプログラム Chapter.10 IESProfile.cpp
【誤】

auto idx = (w - 1) * y + x;

【正】

auto idx = w * y + x;

お、遅かったじゃないか…。

こんにちわ。Pocolです。
「お、遅かったじゃないか…。」

そう言われても仕方ありませんね。
Direct3D 12 ゲームグラフィックス実践ガイドの正式な発売が決定しました!
10月15日発売です!
三宅さんのAI本の一日後なので,三宅さんの本を買う人は一日遅らせて一緒に買いましょう。

本日より,先行販売している書店さんもあるようです。一日でも早く手に入れたいという方は,先行販売している書店さんに行ってみると良いかもしれません。

また,本日の先行販売に合わせて,書籍のサンプルプログラムのダウンロードページが公開されました。
下記ページの「本書のサポートページ」にアクセスするとzipファイルでサンプルプログラムがダウンロードできる状態になっています。
書籍のページ上の都合で,本にはプログラムコードがフルで書かれていませんので,こちらのサンプルを見ながら,本と照らし合わせて見て頂けると良いかと思います!

あと,ページレイアウト決まった後で,サンプルプログラムで未使用変数があるというビルド警告が出る箇所を見つけてしまったのですが,プログラムが実行できないなどの致命的な問題ではないのと,諸所の事情で修正できませんでした。
該当の箇所は,

Chapter9/Tonemap/Sample/res/TonemapPS.hlsl
107行目のL0
108行目のL1

です。
こちらはプログラム上で使っておりませんので,削除あるいはコメントアウトして頂けるとビルド警告が表示されなくなりますので,お手数ですが各自で修正をお願い致します。

…というわけで,来週発売になりますのでよろしくお願い致します!

私的メモ:モーションブラー

A Fast and Stable Feature-Aware Motion Blur Filterの疑似コード。

float cone(float dist, float r) {
	return saturate(1.0f - abs(dist) / r);
}

float cylinder(float dist, float r) {
		return sign(r - abs(dist)) * 0.5f + 0.5f;
}

// linear depth.
float zCompare(float za, float zb) {
		const float SOFT_Z_EXTENT = 0.1f;
		return saturate(1.0 - (za - ab) / SOFT_Z_EXTENT);
}

float3 MotionBlur(float2 p)
{
	// parameter setting (see. 5. Implementation and Results).
	const auto N		= 35;	// sample count.
	const auto eta		= 0.95; // a larger maximum jitter value (in pixel units). (see p.6)
	const auto phi		= 27;	// user-determined constant which affects the "baseline2 jitter level. (see p.6)
	const auto kappa	= 40;	// use-parameter to bais its importance. (see p.6)
	const auto r		= 40;	// a maximum image-space blur radius. (see p.2)
	const auto gamma	= 1.5;	// minimum user threshold (see p.4)

	auto j = Halton(-1, 1);

	// sOffset jitters a tile lookup (but never into a diagonal tile).
	auto vmax = FetchNeighborMax(p/r + sOffset(p, j));
	auto mag_vmax = length(vmax);
	if (mag_vmax <= 0.5f)
	{
		return FetchColor(p);
	}

	auto wn = vmax / mag_vmax;
	auto vc = FetchVelocity(p);
	auto wp = (-wn.y, wn.x); // vmax⊥.

	if (dot(wp, vc) < 0.0)
	{
		wp = -wp;
	}

	auto mag_vc = length(vc);
	auto wc = normalize(lerp(normalize(vc), wp, (mag_vc - 0.5) / gamma); // Eq. (1).

	// First integration samples: p with center weight
	auto totalWeight = N / (kappa * mag_vc);
	auto result = FetchColor(p) * totalWeight;
	auto j_dash = j * eta * phi / N;

	auto z_p = FetchDepth(p);

	for(int i=0; i<N; ++i)
	{
		auto t = lerp(-1.0, 1.0, (i+j_dash + 1)/(N+1)); // jitter sampler

		// Compute point S; split samples between {vmax, vc}
		auto d = (i & 0x1) ? vc : vmax; // iが奇数なら vc, iが偶数なら vmax.
		auto T = t * mag_vmax;
		auto S = int2(t * d) + p;

		// Compute S's velocity and color
		auto vs = FetchVelocity(S);
		auto colorSmaple = FetchColor(S);

		auto z_S = FetchDepth(S);

		// Fore-vs. background classification Y w.r.t p
		auto f = zCompare(z_p, z_S);
		auto b = zCompare(z_S, z_p);

		// Sample weight and velocity-aware factors (Sec.4.1)
		// The length of v_s is clamped to 0.5 minimum during normalization
		auto weight = 0;
		auto wA = dot(wc, d);
		auto wB = dot(normalize(vs), d);

		auto mag_vs = length(vs);

		// 3 phenomenological cases (Sec. 3, 4.1): Object
		// moving over p, p's blurred motion, & their blending.
		weight += dot(f, cone(T, 1 / mag_vs)) * wB;
		weight += dot(b, cone(T, 1 / mag_vc)) * wA;
		weight += cylinder(T, min(mag_vs, mag_vc)) * max(wA, wB) * 2;

		totalWeight += weight; // For normalization
		result += colorSample * weight;
	}

	return result / totalWeight;
}

McGuireのG3Dエンジンでは論文実装よりも変更がある。以下のように内積計算部分がガッツリなくなっている。
https://sourceforge.net/p/g3d/code/HEAD/tree/G3D10/data-files/shader/MotionBlur/MotionBlur_gather.pix

auto f = zCompare(z_p, z_S);
auto b = zCompare(z_S, z_p);

auto weight = 0.0f;
weight += b * cone(T, 1 / mag_vs);
weight += f * cone(T, 1 / mag_vc);
weight += cylinder(T, min(mag_vs, mag_vc)) * 2.0f;

totalWeight += weight;
result += colorSample * weight;