DREDもどき

こんばんわ。
Pocolです。

WriteBufferImmediate()を使って,DREDもどきを実装するサンプルプログラムを書きました。
https://github.com/ProjectAsura/D3D12Samples/tree/master/D3D12_CustomDRED

実装としては,以前に説明したようにVirtualAlloc()でパンくず用のメモリを作り,これをID3D12Device3::OpenExistingHeapFromAddress()に渡して,ID3D12Heapを作ります。
作った,ヒープからCreatePlacedResource()でID3D12Resourceを作り,GetGPUVirtualAddress()して,WriteBufferImmediate()で書き込むためのベースアドレスを取得します。

    bool Init(ID3D12Device3* pDevice)
    {
        if (pDevice == nullptr)
        {
            ELOG("Error : Invalid Argument");
            return false;
        }

        m_BufferSize = sizeof(D3D12_AUTO_BREADCRUMB_OP) * UINT16_MAX;

        m_pAddress = VirtualAlloc(nullptr, m_BufferSize, MEM_COMMIT, PAGE_READWRITE);
        if (m_pAddress == nullptr)
        {
            ELOG("Error : Out of Memory");
            return false;
        }

        auto hr = pDevice->OpenExistingHeapFromAddress(m_pAddress, IID_PPV_ARGS(&m_pHeap));
        if (FAILED(hr))
        {
            ELOG("Error : ID3D12Device3::OpenExistingHeapFromAddress() Failed. errcode = 0x%x", hr);
            return false;
        }

        D3D12_RESOURCE_DESC desc = {};
        desc.Dimension          = D3D12_RESOURCE_DIMENSION_BUFFER;
        desc.Alignment          = 0;
        desc.Width              = m_BufferSize;
        desc.Height             = 1;
        desc.DepthOrArraySize   = 1;
        desc.MipLevels          = 1;
        desc.Format             = DXGI_FORMAT_UNKNOWN;
        desc.SampleDesc.Count   = 1;
        desc.SampleDesc.Quality = 0;
        desc.Layout             = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
        desc.Flags              = D3D12_RESOURCE_FLAG_ALLOW_CROSS_ADAPTER;

        D3D12_RESOURCE_STATES state = D3D12_RESOURCE_STATE_COPY_DEST;

        hr = pDevice->CreatePlacedResource(m_pHeap, 0, &desc, state, nullptr, IID_PPV_ARGS(&m_pBuffer));
        if (FAILED(hr))
        {
            ELOG("Error : ID3D12Device::CreatePlacedResource() Failed. errcode = 0x%x", hr);
            return false;
        }

        m_GpuAddress = m_pBuffer->GetGPUVirtualAddress();

        m_CurrIndex = 0;
        return true;
    }

あとは,コマンドの前後にWriteBufferImmediate()を呼び出して,コールされたかどうかをフラグを書き込みます。

    void Push(ID3D12GraphicsCommandList2* pCmdList, D3D12_AUTO_BREADCRUMB_OP op)
    {
        Marker marker = {};
        marker.Op  = op;
        marker.In  = 1;
        marker.Out = 0;

        D3D12_WRITEBUFFERIMMEDIATE_PARAMETER param = {};
        param.Dest  = m_GpuAddress + sizeof(D3D12_AUTO_BREADCRUMB_OP) * m_CurrIndex;
        param.Value = bit_cast<UINT>(marker);

        auto mode = D3D12_WRITEBUFFERIMMEDIATE_MODE_MARKER_IN;
        pCmdList->WriteBufferImmediate(1, &param, &mode);

        m_CurrentOp = op;
    }

    void Pop(ID3D12GraphicsCommandList2* pCmdList)
    {
        Marker marker = {};
        marker.Op  = m_CurrentOp;
        marker.In  = 0;
        marker.Out = 1;

        D3D12_WRITEBUFFERIMMEDIATE_PARAMETER param = {};
        param.Dest  = m_GpuAddress + sizeof(D3D12_AUTO_BREADCRUMB_OP) * m_CurrIndex;
        param.Value = bit_cast<UINT>(marker);

        auto mode = D3D12_WRITEBUFFERIMMEDIATE_MODE_MARKER_OUT;
        pCmdList->WriteBufferImmediate(1, &param, &mode);

        m_CurrIndex++;
    }

Push()とPop()挟み方は次のような感じ。

    void DrawInstanced( 
        _In_  UINT VertexCountPerInstance,
        _In_  UINT InstanceCount,
        _In_  UINT StartVertexLocation,
        _In_  UINT StartInstanceLocation) override
    {
        Push(m_pCmdList, D3D12_AUTO_BREADCRUMB_OP_DRAWINSTANCED);
        m_pCmdList->DrawInstanced(VertexCountPerInstance, InstanceCount, StartVertexLocation, StartInstanceLocation);
        Pop(m_pCmdList);
    }

GPUクラッシュが発生したら,VirtualAllocで確保したメモリを見に行って,どこまで書き込みが終わっているかをチェックすれば,DREDもどきが出来ると思います。

    void Print()
    {
        auto marker = reinterpret_cast<Marker*>(m_pAddress);
        if (marker == nullptr)
            return;

        ILOG("BreadcrumbCount : %u", m_LastIndex);

        for(auto i=0; i<m_LastIndex; ++i)
        {
            char state[3] = "  ";
            if (marker[i].Out)
            {
                state[0] = 'O';
                state[1] = 'K';
            }
            else if (marker[i].In)
            {
                state[0] = 'N';
                state[1] = 'G';
            }

            uint32_t opIndex = marker[i].Op;
            ILOG("%04u : [%s] Op = %s", i, state, g_BreadcrumbTags[opIndex]);
        }
    }

というわけで,DREDもどきの実装でした。
PageFaultとかも頑張ろうかなと思ったのですが,MP切れなのと,BindlessResource(DynamicResource)でのアクセスが検知できないので,諦めました。
このあたりはNVIDIA AftermathとかAMD Radeon GPU Dectective使える環境ならそっちを使った方が良いです。

またカスタムビルド

どうしてもエディタとしてVisual Studioを使いたいPocolです。

一番簡単なカスタムビルドのメモです。
.vcxprojをテキストエディタで開いて,一番下のほうに次ような感じでコマンドを挿入します。

  </ImportGroup>
  <Target Name="Build">
      <Exec Command="call build.bat" />
  </Target>
  <Target Name="Clean">
      <Exec Command="call clean.bat" />
  </Target>
</Project>

ちなみにExecタスクのドキュメントは下記ですので,細かい設定を追加した場合は下記を参考にしてください.
MSBuildリファレンス > タスクリファレンス > Execタスク
https://learn.microsoft.com/ja-jp/visualstudio/msbuild/exec-task?view=vs-2022

Visual Studio上からリビルドを実行した場合は,Clean —> Build の順番で呼び出しされます。
もしリビルド自体をカスタマイズしたいなら

  <Target Name="Rebuild">
    <Exec Command="call rebuild.bat" />
  </Target>

とやってあれば,自前にリビルドコマンドに変更できます。

実行時のコマンドのカスタマイズは,通常のVCと同じようにプロパティから変更すれば良いと思うので,それでやればよいかと思います。
…というわけでカスタムビルドのメモ書きでした。

どっかで

クリックしてtede-msbuild-2.0.pdfにアクセス

とか

あたりを参考にもうちょいちゃんとしたものを今後改造してみたいと思います。

DREDとAftermathのサンプルプログラムを作りました。

こんばんわんわん、Pocolです。

X(旧Tiwtter)でも書いたのですが,DRED(Device Removed Extended Data)のサンプルプログラムを書きました。
サンプルは以下に置いておきました。
https://github.com/ProjectAsura/D3D12Samples/tree/master/D3D12_DRED

DREDなんですが,意外とまともなドキュメントが無いです。ドキュメントあるんですけども,わかりづらい,「この変数の意味は?」みたいな痒いところに手が届くものが無い感じがしますね。(単純に、ドキュメントみて理解できない私がアホなだけなんですが…)
…というわけで,コードを書いてみました。
仕事で使っているのはちゃんと,Push/Popの入れ子とかも考慮しているやつですけども,まぁええでしょ。こまけぇこたぁいいんっすよ。
結局,DREDのサンプルで困るのは「これちゃんとGPUクラッシュ時に出るの?」という所で,故意にGPUクラッシュさせるようなプログラムがなかなかネットで見つからない。
それだと,動作確認に困るので,GPUクラッシュさせるプログラム書きました。
ここ最近,ずっとGPUクラッシュの調査していたので,どうやれば簡単にGPUクラッシュを引き起こせるかなどのノウハウが溜まったので,その知見を活かして書きました。
一番よくある例,実行中にテクスチャを解放しちゃうやつ。これが一番良くあるので,Tボタン押したら,テクスチャをRelease()するようにしました。これで簡単にPageFaultのGPUクラッシュが発生します。TDRはレジストリいじっている場合は,発生までに時間かかるし,意外とGPU側でちゃんと対処してくれちゃったりする場合もあるので,無理やりやろうとしても意外と発生しなかったりします。サクッといかない。
DirectX-Samplesにはhttps://github.com/microsoft/DirectX-Graphics-Samples/tree/master/Tools/DXGIAdapterRemovalSupportTestというやつもあるみたいですが,こっちは触ったことないんで良く分からんっす(詳しい人教えてください)。

…で馬鹿の一つ覚えみたいな感じなんですが,PageFaultを発生できるようになったので,NVIDIA Aftermathのサンプルも書きました。
一応クラッシュ発生時に,ShaderBinaryとShaderPDBを吐き出して,クラッシュログを調査できる感じにしてみました。私のサンプルの場合は,クラッシュが発生している該当シェーダだけを出力するので,そんなにクラッシュダンプ出力に時間はかからないと思います(仕事でつかっているやつは,別の人が既に実装されたやつなんで,全部のシェーダのバイナリとPDBを出力しやがるんで,時間とディスク容量を食いまくって,困るんですよね。直すのは面倒ですし、時間の余裕もないので直す気はサラサラないです)。
サンプルプログラムは下記にあります。
https://github.com/ProjectAsura/D3D12Samples/tree/master/D3D12_NvAftermath
時間があれば,解説書いてもいいんですが,残念ながら,その時間がないのと,若干仕事のせいで鬱気味なのでやる気が起きないっす。(どうせみんなUEやらUnityつかうでしょ?こういう直叩きするひとがもう日本じゃ少数だから,親切に書いてあげても意味が無いんですよ。見る人いないから…)

…というわけで,リリースして精神的に落ち着いたら,のんびりゆったり解説を書こうかなと思います。
まぁ,そんなの期待する人はほぼいないと居ないと思いますが。

GPUクラッシュが激減した…

こんばんわ、Pocolです。

このところ,仕事でずっとGPUクラッシュを追っていたのですが,なんとなくノウハウが溜まってきました。
ほぼ、毎日のようにGPUクラッシュが何十件もあり,色々な人に手伝ってもらいながら,調査していたんですが,ようやくそれがパッタリ収まりました。

いくつか対策を入れていたのですが,結局どれが効いたのかは正直分からないのですが…
groupsharedで,UINT32_MAXでアクセスして,GPU上でメモリ破壊を発生させるコードがあったので,修正した所,謎に発生していたGPUクラッシュが治まりました。
多分,数年レベルで放置されていたバグなんじゃないかと思います。

NVIDIA AftermathとかでGPUクラッシュダンプを調べても,PageFault,さらにシェーダ情報もでない,挙句の果てにはResourceBarrierを実行するとGPUクラッシュする,定数バッファが壊れて無限ループして,TDRで落ちるなど,過去のクラッシュログを見ても,まったく共通性もなく,しかも結構頻発する。でも全然再現性が無い,手元で全く発生しない…という困ったちゃんで,困りまくった挙句何かないのか?と調べてみたら,NVIDIA Aftermathに,GFSDK_Aftermath_FeatureFlags_EnableShaderErrorReportingというフラグがあるのですが,これを有効にしたところ即クラッシュするようになりました。
何で無効だったのか聞いたんですが,こちらはヘッダファイルにも書かれているようにパフォーマンスペナルティがあるということで,ゲームプレイに支障があるとのことで無効化していたとのことでした。
結局,このフラグを有効にしたところ,先ほど述べたgroupsharedのバグを発見できたのと,他にもクラッシュする原因が見つかって,大いに役立ちました。

今後は,確定で発生しないものはGPU上のメモリ破壊を疑った方が良いという知見が得られました。
基本的にはUAVとかSRVとかのLoad()とかoperator []あたりミスっているんじゃないかって思いがちなんですが,これらはAPIドキュメントを見ると、安全に良しなにしてくれそうなことがあるので,メモリ破壊が発生するのは,CPU上での書き換えか,groupsharedのアクセスミスによる2パターンしかないように思えます(他のパターンがあったら教えてください)。CPU上でのメモリ破壊の可能性がほとんど低いことが分かったなら,groupsharedでの破壊が無いかどうかを調べましょう。
今回のバグは,

groupshared g_Variable[XXX][XXX];
みたいなのが定義されていて,
float temp0 = saturate(XXX);
uint temp1 = temp0 * MAX_VALUE – 1;
uint index = min(temp, MAX_VALUE);
g_Variable[index][XXX] = ….;
っぽいような,謎の計算がされていて(uintをマイナス1する時点でぞわぞわしちゃんですが…)
temp0がゼロになったときに,アボンするみたいケースでした。
よくよく見ると「馬鹿か!」って怒鳴りたくなったっちゃうような,不具合なんですが,変数がごちゃごちゃ定義されていたりとか,ジュニアレベルのエンジニアだとこういうチェックがおざなりになりがちなんで,気を付けた方が良いよ!…という良い事例になりました。

…というわけで,社内でも共有したんですが,この場でも共有してみました。
謎バグに困っている方は,groupsharedで変なことしていないかどうか確認してみると良いかもです。
(※ちなみ1個あったら,他にも絶対あるだろうと思って全シェーダをチェックしてみたのですが,確認した所発生しているのは該当シェーダの1個だけでした。)

パンくずリスト

こんばんわ。Pocolです。

最近ずっとGPUクラッシュ調査の仕事をやっていて疲弊しています。
基本的には,DREDとAftermathを使っています。
DREDを使うと,自動でパンくずリストを作ってくれます。
これ,基本的にはマーカー名とどこまで進んだかを教えてくれます。

GPUクラッシュは大半がTDRかPageFaultだと思います。
TDRでよくあるのは,ループ終了条件を定数バッファのメンバとして渡すパターン。
バッファがどっかで壊れて,終了条件値が想定外の値になって,無限ループ扱いになってクラッシュするとかが良くあります。

で、クラッシュ調査時に欲しいのはどのシェーダ?設定されているのはどのバッファ?中身どうなっている?
…あたりの情報が知りたくなります。

そこで,思いました。
DREDの自動パンくずリストをやめて,自前でカスタムのパンくずリストを作った方が便利なのでは?
そう思ったので,パンくずリストについて調査してみます。

Githubやググったんですが,まともな情報がほぼありません。
FidelityFX Breadcrumbs 1.0が唯一信じられるまともな実装です。
これを調査してみます。
ドキュメントは下記です。
https://gpuopen.com/manuals/fidelityfx_sdk/fidelityfx_sdk-page_techniques_breadcrumbs/

ソースコードは

…あたりを見ると良いです。

基本的な仕組みとしては,
D3D12の場合は,WriteBufferImmedidate() で実行済みフラグを立てていくだけみたいです。
Vulkanの場合は,AMD拡張が使える場合は,vkCmdWriteBufferMaker2AMD()やvkCmdWriteBufferMarkerAMD(),そうでない場合はvkCmdFillBuffer()を使って実行済みフラグを立てていくようです。
現在困っているのは,D3D12環境なので以下D3D12として説明します。

で、WriteBufferImmediate()でどこに書き込むか?なのですが,次のような感じで書き込むメモリを用意するようです。
・VirtualAlloc(nullptr, bufferSize, MEM_COMMIT, PAGE_READWRITE)でメモリを用意。— (A)
・(A)で用意したメモリを引数として,ID3D12Device3::OpenExistingHeapFromAddress() をコールして,ID3D12Heapを取得し,CreatePlacedResource()でID3D12Resourceを生成。 — (B)
・(B)に失敗した場合は,CreateCommittedResource()でID3D12Resourceを生成し,メモリはMap()して取る — (C)
・(B)または(C)にてID3D12Resourceが出来上がるので,ID3D12Resource::GetGPUVirtualAddress()して,BaseAddressを取得 — (D)
・(D)で取得したBaseAddressを開始点として,uint(4byte)で,フラグをWriteBufferImmedidate()で書き込んでいく。

これでコマンドリストに記録されるようになるので,あとはクラッシュした際に(B)または(C)で用意してあるメモリをReadしていきます。これで書き込まれていればフラグが立っていくはずなので,どこまでコマンドが進んだかどうかが判定できます。
マーカー名などのデータはCPU側で管理して,Readしたデータと照合して一致させて,デバッグログなどに表示させれば良いようです。

細かい実装は,FidelityFX Breadcrumbsのソースコードを参照してみてください。

…というわけで,パンくずリストを自前実装する際の基本的な仕組みが分かりました。
あとは,CPU側で管理するデータをリッチにしていけば,色々とデータが取れそうです。

まずは、これらの情報を元にカスタムパンくずリストの実装を始めてみようかなと思いました。
そんなわけで、パンくずリストの話でした。
もし、ノウハウを色々とお持ちの方は是非教えてください。

———————
同僚の方にノウハウを教えてもらいました。
メモリが直接見えるコンソール機では,WriteBufferImmedidate()に対応するような命令が無いことが多く,その場合はタイムスタンプを使って,コマンドがどこまで進んだかを調べると良いそうです。
これは確かに良いなと思いました。

パフォーマンス反省点メモ

120FPS対応を仰せつかった,その時のパフォーマンスについての反省点のメモです。

* ComputeSkinningはEarlyDepthと並列で動作させるべき。
非スキニングメッシュをグラフィックスパイプで描画し,その裏でコンピュートパイプでスキニング処理を実行。完了後にグラフィックスパイプに投入できるように変更すべき。つまり,SkinningメッシュとStaticメッシュはコマンドを分けておかなければならない。

* コンテキストロール回避をあらかじめ仕組みとして用意しておく
シェーダのハッシュ値をもとにソートして,コンテキストロールが発生しづらいように設計しておいたほうが良い。PC版DXCでもハッシュ値が公開されていたはずだと思うので,それを調べてソートする感じ。

* プラットフォームによってコンピュートシェーダが速い
ピクセルシェーダを使うとWaveを使いきれないプラットフォームがあるので,フルスクリーン描画系はすべてコンピュートにしておいたほうが良い。これはとあるカンファレンスでも説明されているので,そちらの資料を参照されたし。どの資料かはお教えできません。

* 深度バッファのダウンサンプルを使いまわせる設計にすべき
何度もダウンサンプルすると遅いので,SSAO, RayMarchShadow, SSR等すべてのスクリーンスペースエフェクトで深度バッファを使いまわせるようにあらかじめ設計すべき。さらにいうとAMD SPDとかみたいな実装でダウンサンプル自体も1パスで実行するべき。

* 非同期コンピュートをフル活用できるようにする
できるだけ非同期コンピュートで実行できるようにレンダリングパスを考えるべき。特にフレームの中盤以降にRTVやUAVの依存関係で,動かせるものがなくなる傾向がある。でも,非同期コンピュートは空いているので,この時間を有効活用できるようにフレーム全体を設計すべき。結局使いどころとしては,コンピュートスキニング,VFX,タイルライトの事前処理や,カリング処理関連になるかと…。

* 半透明重い
おそらく次世代機でも半透明が重いのは変わらないと思われる。縮小バッファやTranslucencyLightingVolumeは先に対応しておき,ON/OFFできるように用意しておくのがよろし。Publisherさんによっては,Hardware-VRSは使わないでとか言われるので,Software-Based Variable Rate Shadingを実装しておくのが良いかもしれない。

* FSR重い
開発終盤にFSRパス全体がボトルネックになりやすい。特に深度を作るパスなどは先行して処理することができるので,FSRのパスの一部を切り分けできるようにしておいたほうが良い。また,とある界隈では最適化されたものが配布されているので,そちらをベースにして各プラットフォーム用に動作するようにカスタマイズするのがおススメ。自分で組み込んだ感じだと0.1ms程度通常版よりも高速化した。

* 16bit専用命令を駆使する
レジスタプレッシャー下げや高速化のために,16bitのhalf floatにすることはよくあると思うのだが,普段そこまでカリカリにチューニングしなくても済んでしまっているので,ノウハウがなかったが,できるだけfloatに戻さずに専用命令で演算し続けたほうが良い。単にhalfにするだけで四則演算するだけじゃだめ。ハードウェア命令を駆使するように。先ほど述べたFSRの最適化もこのあたりを使いまくっている。

* wave32のほうが速いケースがある
PS4なんかはWave64モードしかないので,スレッド数が64になるようにコンピュートシェーダとかで組んでいたと思いますが,最近のハードはwave32ベースで作られていて,wave64で実行する場合はエミュレーションされる命令などもあるっぽい。実際にとあるプラットフォームではシェーダコンパイラがちゃんと警告を出してくれて,wave32で実行したほうが良いことを促してくれる。また,Divergenceの多い複雑なシェーダなどではwave32にするだけで,高速化する場合もある。特にフォーワードレンダリングのシェーダなんかは複雑になりやすい傾向があるので,そういうものはwave32にしたほうが良い。またコンピュートシェーダで起動スレッド数が少ないものもwave32モードにしたほうが良い。

* バリア処理はまとめる
いうまでもないがバリア処理はまとめてバッチングするように。過去にカプコンさんだったりの資料で,そうしたほうが高速化するという実例が出ているので,面倒だけどもちゃんとまとめるように。…というかレンダーグラフとかパスグラフみたいなシステムがちゃんと作ってあるなら,そこで吸収するように作ってあるはず。もしつくっていないなら,今すぐにそうなるようにプログラムを組んだほうが良い。

* 何でもかんでもバインドレスにしない!
レンダーターゲットにもテクスチャにも使用されていない的な警告がグラフィックスデバッガで出ることがあるが,バインドレステクスチャとして使っている場合に,使用していることがスルーされることがあるっぽい。実際にあった事例だと,使っていないから消してOK的な指示があるので,遠慮なく削除したところ,見た目がバグることがあった。で、コードを追って調べたところバインドレステクスチャとして使っている箇所だった。なので,基本的にはバインドレステクスチャにしなくてもいいところは無理にしないほうが,ツールのアシストが効くので最適化に役立つことがある。何でもかんでもバインドレスにしないこと!

* 出力シェーダアトリビュートは4つ以下にする
出力パラメータが4つまでなら,処理負荷がかかることはないが,4つを超えると処理負荷がかかって遅くなる。そのため,頂点シェーダやメッシュシェーダからの出力パラメータは可能な限り4つ(float4が4つ)以内に収まるようにパッキングを行う。とあるプラットフォームだけが特定キーワードを付け足すと自動でパッキングしてくれるが,そのような機能が用意されていないプラットフォームもあるのでfloat16_tを使ってできる限り詰め込むと良い。

* 帯域を下げる
R9G9B9E5フォーマットなどを使い,R16G16B16A16_FLOATなどのフォーマットは避ける。R11G11B10_FLOATがあるが,こちらは過去タイトルで絵的なバグが出た事例を聞いたことがあるのと,あんまり精度がよくないため,お勧めできない。前述のR9G9B9E5フォーマットを使うようにする。この際に,アルファブレンドしているものがあると置き換えに苦労するので,デバッグ系描画などはシェーダ上で手動ブレンドして3チャンネルしか使わないようにするなど,あらかじめ最適化を見越した作りにしておいたほうが良い。

* 頂点シェーダは可能な限り使わない
頂点シェーダを使うだけで重いので,できる限り使わない。メッシュシェーダやコンピュートシェーダで処理できるようにしておく。

* コマンドジャンプを使う
奥の手としてコマンドジャンプを使って色々なことを実装することが可能なので,PipelineStateをGPU上で設定したりなど,PCでは出来ない,コンソールならではのことをやって最適化を行う。こうすることによってCPUのドローコールは減らせるので,CPU処理がネックだった場合は高速化ができる。

* バッファのアロケータはちゃんと作る
CommittedResourceはアライメントが64KiBになるので,例えば16Byteの定数バッファをアロケートしても64KiBでメモリが取られてしまうので,小さいバッファ用に自前でちゃんとアロケータは作っておいたほうが良い。作っていないとメモリ周りで嫌な思いをすることになる。
自分が作ったやつだと,VB or IB or (!UAV && ByteAddressBuffer) or (!UAV && StructuredBuffer)の条件を満たして,64KiBなら同一リソースから切り出して使うという実装にした。!UAVにしているのは1個のResourceの場合,リソースステートが部分的にWriteになったりさせることができないので,基本的にはReadしかしない対象に限定するため。これで数百MB減ったケースがある。

* Decompress処理をしないようにする
Decompress処理が走ると重いので,最近のやつだとDecompressしなくていいようにするためのフラグがあったりするので,そいつを付けておく。

* 最初からWave Instrinsicsを使って最適化しておく
終盤に命令最適化すると,バグで困ることが多々あるので,あらかじめ最適はできるところは最初からやっておく。終盤にしかできない場合は,ちゃんとデグレを検知するための単体テスト環境など,機械的なチェック環境を用意しておくこと。人力によるチェックはすり抜けることが当たり前にある。

* GPUネックになりやすいものは一部処理をCPUに逃がす。
今世代でもGPUネックになりやすい。そのため,CPU側に逃がせるものは逃がすようにする。例えば,オクルージョンカリングはソフトウェアラスタライズのもの使うとか,VFXの計算の一部をCPUでやるとか,メッシュのLOD判定をGPUからCPUに移すとか…。

* 非同期解放処理システムを作っておく
同期させるとやっぱりスパイクなどが発生しやすいので,1フレームでの処理対象を制限するか,そもそも大丈夫なように非同期解放の仕組みをつくっておくかしないと,ロード時・ロード直後に痛い目に会うことになる。

* その場しのぎの適当なコードを書かない
w成分空いているからここに高さデータいれちゃえーとか適当な実装をしていると,前述したパッキングによる最適化を行う際に,都合が悪いケースが出てきて,困ることがある。どこにどのデータが入っているかは,誰から見ても分かるようにしておくこと・そしてきちんと管理しておくこと。そうしないとエンバグが発生しまくって,自分の首を絞めることになる。

* ループ処理を見直す
早い段階で,continueできれば重い処理を回避し,最適化できるケースはちゃんと最適化する。メッシュ描画などは何度も呼ばれるため,意外と大したことがない変更でもめちゃくちゃ速くなることがある。

* デバッグツールは用意する
プログラマー的なグラフィックスデバッガーを使いこなせるアーティストは少ないので,ゲーム上にデバッグツール・可視化ツールは用意しておく。大量生産前に用意しないと,そのあとは忙しさで作られることないと思ったほうが良い。特にオーバードローやシェーダの複雑度あたりは必須。

Direct3D 12 ゲームグラフィックス実践ガイド 増刷決定!!

こんにちわ。Pocolです。
技術評論社様より販売させていただいております『Direct3D 12 ゲームグラフィックス実践ガイド』ですが,おかげさまで増刷が決定しました!

Uneral EngineやUnity等が台頭する時代に逆行する内容の書籍なので,すぐに絶版するのではないか?という不安。
また,DirectX12の魔導書なども既に発売されていて,私の書籍など見向きもされないのではないか?
発売しても売れるのだろうか,そもそも買おうとすら思ってもらえないのではないか?
…など色々な不安を抱えながら,なんとか発売までたどり着いた書籍です。

この本は本当にいろいろな方にご迷惑をお掛けしながら,何とか出せた書籍ですので,増刷が決定したと連絡を受けて嬉しかったです。
本当に皆様のおかげです。本当にありがとうございます。

ただ正直,もっと批判とか苦情に近い意見が多いんだろうなと覚悟はしていたのですが,想像していたよりも少なく,また良かったという意見も全くない状態でして,著者としては書いてよかったのか,書いて悪かったのかが何にも分からない。受け入れられているのか,そうでもないのか判断に困るという状態です。
分かりづらいところがあれば,SNS等で遠慮なく書いていただきたいですし,逆にダメなところは今後の執筆に活かせるチャンスとなるので,忌憚のないご意見を書いていただけると幸いです。また,もっと書いて欲しいものとかあるのであれば,応援の意味を込めて「良かったよ!」など肯定的な意見を頂けると,励みになります。肯定的な意見が無ければ,「これはもう書かないほうがいいな」という執筆を辞める決断にもなってしまいますので,応援していただけるのであれば,応援の声も頂けるとありがたいです。

書籍のほうですが,12月ころから増刷版が市場流通する見込みと伺っておりますので,現在手に入れられていない方はもうしばらくお待ちいただければと思います。
今後ともDirect3D 12ゲームグラフィックス実践ガイドをよろしくお願いいたします。

タイル分類化による最適化(1)

こんちゃわ。Pocolです。
相も変わらず最適化でヒーヒーいっています。

ライティングシェーダって複数のマテリアルをサポートするとために,大体Uber Shaderになると思うのですが…
それだとやっぱりswtich-caseなどの分岐で重くなりがちです。
分岐を除くと,占有率の改善がみられ,速くなったりすることがあります。
そのため「分岐を無くそう!」というのが今回のネタで,それを実現するための資料について紹介します。


Deferred Lighting in Uncharted 4

まず,1つ目は「Deferred Lighting in Uncharted 4」です。
これはSIGGRAPH 2016のAdvances in Real-Time Rendering Courseで発表されています。
資料は下記からダウンロードできます。
https://advances.realtimerendering.com/s2016/index.html
もう8年前の資料なんですね。びっくり!


ディファードシェーディングすぐに肥大化します。
スキン,布,植物,メタル,髪など…をサポートする必要があります。すべてにライトタイプについて言及はしません。


マテリアル”ID”テクスチャを保存します。
– 実際のマテリアルIDではありません。単にシェーダの使用されるシェーダ機能のビットマスクです。
– 12bitを8bitへ圧縮(機能の相互排他性を考慮)


・各16×16タイルについて、タイル全体のマテリアルマスクを使用してルックアップテーブルにインデックスを付けます。
・ルックアップテーブルは事前に計算されています。タイル内のすべての機能をサポートする、可能な限りシンプルなシェーダーを保持します。


・アトミックにタイル座標を、そのシェーダーがライティングするタイルのリストにプッシュします。
・アトミック整数は dispatchIndirect 引数バッファのディスパッチカウントにもなります。


・既に大きな改善です。
・類似したテクニックは[1]で使用されています。
[1] SPU-Based Deferred Shading in Battlefield 3, http://www.dice.se/news/spu-based-deferred-shading-battlefield-3-playstation-3/


・タイル内のすべてのピクセルが同じマテリアルマスクを持つ場合に使用される、事前に計算されたもう1つのテーブル、「ブランチレス」のpermutationテーブルを作成します。
・クラス分けの際にその条件をチェックし、適切なテーブルを使用します。
・分岐をなくすだけでなく、グローバルなコンパイラ最適化の機会を開きます。


・最も悪い場合である高価なカット―シーンにおけるパフォーマンス改善
ー 4.0ms 最適化無し(“uber shader”)
ー 3.4ms (-15%) 最も良いシェーダを選択することによる
ー 2.7ms (-20%, -30% 全体的に) ブランチレスシェーダを使用することによる
・平均して、ブランチレス・シェーダーは、わずかなコストで、さらに10~20%の改善をもたらします。一方、最適なシェーダーを選ぶと、平均して20~30%の改善が得られます。


・基本性能に影響を与えることなく、マテリアルの複雑さやバリエーションを持たせることができます。
 ー1つのシェーダー(例えばシルクシェーダー)に複雑さを加えても、ゲームの他の部分には影響しません。
・インターフェイスはクリーンかつ透過的に実装されています。
 ー何度か繰り返した後
・ボーナス:分類コンピュートシェーダーは非同期コンピュートで実行され、ランタイムにはほとんど影響しません。


・システムをさらに進化させることができる。
 ーライトタイプに基づいて、異なるコンピュートシェーダーをディスパッチすることもできる。少数派のライトタイプは、複雑さとコストの大部分を追加します。
・イテレーションは難しい
 ー本当に1ビットの価値を学ぶ。
 ー最終的には良いシステムに到達した。
・よりシンプルなものは常に良いです。わずかな性能向上のために、ある機能の犠牲を避けられたと思います。

該当スライドは以上です。
上記で述べられているように,タイルごとに必要なシェーダを分類分けを行います。
非同期コンピュートで実行し,処理時間を隠蔽します。
Uncharted 4では16×16ピクセルのタイルにして,分類分けを実行し,groupId.xを下位16ビット,group.yを上位16ビットとして32bitにパッキングし,バッファに格納します。
同時に,Shader Permutationごとにカウンタをアトミックにインクリメントしますし,dispatchのカウントバッファとして利用します。
こうすることで,必要な数だけコンピュートシェーダを起動することができます。


Grappling With Performance: Rendering Optimization Strategies In Rumbleverse

つづいて,”Grappling With Performance: Rendering Optimization Strategies In Rumbleverse”という資料で,GDC 2023で発表された資料です。
こちらは昨年なので,比較的に最近の資料ですね。
下記に資料がアップされています。
https://gdcvault.com/play/1028790/Grappling-with-Performance-Rendering-Optimization

こちらはライティングではなくReflectionとSubsurfaceが重いという話に焦点が当てられています。

・オリジナルアイデア:タイル分類を使用することで、リフレクションを適用する際の占有率を向上させます。
・Ramy EI Garawanyのプレゼンテーション:Deferred Lighting in Uncharted 4にインスパイアされています。
・アルゴリズム:
1. G-Buffer解析します。
2. マテリアルプロパティに基づいてタイルのリストを構築する。
3. 異なるshader permutations + DispatchIndirect を用いてそれぞれ描画します。

リフレクションに費やされる重い時間は、ここでも同じように最適化できると思いました。8×8のピクセルグループのGバッファプロパティを見て、存在するマテリアルに基づいてリストを構築するタイル分類シェーダを書きます。そして、各リストを DispatchIndirect を使って、各ディスパッチに異なるshader permutationsをバウンドしてレンダリングします。


例えば、このフレームでは、すべてデフォルトでライティングしているピクセルや、両面フォリッジでライティングしているピクセルをはっきりと見ることができます。


そしてここで、実際のタイル分類の視覚化を見ることができます。緑のタイルはデフォルトのライティング、青はすべてのフォリッジ、そして赤は「複雑」でフルシェーダーを実行するシェーディングパスを含んでいます。

しかし、この処理で最も重要だったのは、このタイルまでで、タイルは完全にカリングされ、実行時間に最も大きな影響を与えました。このことから、タイルの分類をSSR+SSSからのカリングワークロードに対しても使用し、分類を実行するコストをレンダリングの複数のステップで共有する方法について考えるようになりました。

最速のウェイブフロントは、決して起動しないウェイブフロントであることを忘れないでください!


すべてのパスからライティングのないタイルをカリングし、すべてのピクセルがSSRトレースをトリガーするには粗すぎるかどうかの分類を追加し、スキンマテリアルがないタイルのSSSをスキップし、クリアする必要がありますが,完全なSSSセットアップを必要としないタイルの簡略化クリアを実行します。


Tile Classifyシェーダーのコードで何が起こっているかを少し見てみましょう。分類は8×8タイルで行われますが、サブサーフェススキャッタリングは半分の解像度で行われるため、各グループは16×16のエリアをカバーします。UE4 の GetScreenSpaceDataUnit 関数で gbuffer プロパティをサンプリングした後、wave ops を使用して各 8×8 タイルのビットマスクをマージします。

コードでは、UE4シェーダーAPIコマンドの WaveAllBitOr と WaveAllBitAnd で起こっていることがわかります。これらのウェーブ操作の後、ウェーブフロントの各スレッドは MergedResult に同じマスク値を保持します。

ウェーブ操作を使用する利点の1つは、コンパイラがMergedResultがwave全体で均一であることを知っているため、waveコマンドに続くロジックがすべてスカラーALUになることです。


次に、ウェーブ全体にわたってMergedResultに保持されているビットに基づいてタイルのshader permutationが選択され、結果が最初のスレッドによって書き込まれますinterlocekdされた加算がカウントで発生し、タイル位置バッファにマップされるタイルの一意のインデックスを取得します。タイル位置バッファは、画面上の特定のタイルのピクセル位置を保持し、適用シェーダーで各タイルのピクセル位置を再構築するために使用されます。

なお、8×8タイルを選んだのは、GCNで1ウェーブフロント(64スレッド)のサイズだからです。アンチャーテッド4では16×16タイルを使用し、タイルリストに必要なメモリを25%削減した。タイルロケーションリストは、最大タイル数*permutation数に等しいメモリを必要とします。8×8タイルは、高価なマテリアルパスの境界をより厳しくすることができます。私が8×8を選んだのは、permutation数がより限られているからでもあります。たとえば、フォリッジを含むタイルのパスを追加してみたり、default litしているタイルのパスを追加してみたり。

現在、10個のシェーダーパーミュテーションがあり、その結果、1080の8×8タイルで1.296MBのタイルロケーションバッファになります。ハーフ解像度タイルリストに過剰に割り当てなければ48kbを取り戻すことができるはずですが、メモリのほとんどはSSR+Reflection Applyに使用される8つのpermuationから来ています。

[hlsl]
uint bAnySSSProfile = 0;

// loop over each 8×8 tile within the 16×16 pixel area
uint2 PixelOffsets[4] = { uint2(0, 0), uint2(1, 0), uint2(0, 1), uint2(1, 1) };
UNROLL
for(int i=0; i<4; ++i)
{
uint2 PixelPos = (DispatchThreadId.xy * 2 + ViewDimensions.xy);
FScreenSpaceData ScreenSpaceData = GetScreenSpaceDataUint(PixelPos + (PixelOffsets[i] * 8));
FGBufferData InGBufferData = ScreenSpaceData.GBuffer;

uint bIsDefaultLit = (InGBufferData.ShadingModelID == SHADINGMODELID_DEFAULT_LIT) ? 1 : 0;
uint bIsFoliageLit = (InGBufferData.ShadingModelID == SHADINGMODELID_TWOSIDED_FOLIAGE) ? 1 : 0;
uint bIsComplexLit = (InGBufferData.ShadingModelID > SHADINGMODELID_DEFAULT_LIT) ? 1 : 0;
uint bIsSSSProfile = UseSubsurfaceProfile(InGBufferData.ShadingModelID) ? 1 : 0;

float Roughness = InGBufferData.Roughness;
float RoughnessFade = GetRoughnessFade(Roughness);
uint bSkipSSR = (RoughnessFade <= 0.0 || InGBufferData.ShadingModelID == SHADIGNMODELID_UNLIT) && InGBufferData.ShadingModelID != SHADINGMODELID_CLEAR_COAT;

// OR results
uint MergedResult = (bIsSSSProfile << 2) | (bIsComplexList << 1) | bIsDefaultLit;
MergedResult = WaveAllBitOr(MergedResult);
uint bAnyDefaultLit = MergedResult & (1 << 0);
uint bAnyComplexLit = MergedResult & (1 << 1);
bAnySSSProfile = bAnySSSProfile | (MergedResult & (1 << 2));

// AND result.
MergedResult = (bSkipSSR << 2) | (bIsFoliageList << 1) | bIsDefaultLit;
MergedResult = WaveAllBitAnd(MergedResult);
uint bAllDefaultLit = MergedResult & (1 << 0);
uint bAllFoliageLit = MergedResult & (1 << 1);
uint bAllSkipSSR = MergedResult & (1 << 2);

// select which permutation
uint PermutationIndex = NUM_PREMUTATIONS;
if (bAllFliageList)
{
PermutationIndex = 4;
}
else if (bAllDefaultLit)
{
PermutationIndex = 6;
}
else if (bAllComplexLit)
{
PermutationIndex = 0;
}
else if (bAnyDefaultLit)
{
PermutationIndex = 2;
}

// odd half of permutations lacks SSR completely
if (bAllSkipSSR)
{
PermutationIndex += 1;
}

// write out the 8×8 data
// first thread does atomic increment and write, fully unlit tiles are skipped entirely
if (GroupIndex == 0 && PermutationIndex < NUM_PERMUTATIONS)
{
uint TileIndex;
InterlockedAdd(RWTileDispatchCounts[PermutationIndex * 3], 1, TileIndex);

uint TileLocationID = TileIndex + (PermutationIndex * NumTiles);
uint2 TileLocation = (GroupId * 2) + PixelOffsets[i];
RWTileLocationsBuffer[TileLocationID] = TileLocation.x | (TileLocation.y << 16);
}

// SSS permutations go beyond the end of the normal reflection tile permutations
uint SSSPermutationIndex = NUM_PERMUATIONS + 1;
if (bAnySSSProfile)
[/hlsl]


それは注目する価値があります – このシェーダの本当に素晴らしい点の1つは、GBufferプロパティをサンプリングするこれらの呼び出しがすべて1つのテクスチャ読み取りにマップされることです。Epicは便利なことに、すべての情報を1つのGBufferターゲットに既にパックしています。このターゲットには、ラフネスとマテリアルIDの両方が保持されています。


Razorに戻って、この分類シェーダーの実行コストを見てみましょう。ベースPS4で1080pの場合、0.18ミリ秒と控えめで、デカールがGBufferを変更し終わるとすぐに実行できます。


この分類処理は、非同期コンピュートを使用したシャドウ深度レンダリングと非常にうまく重なり、ここでは、マスクされたマテリアルのピクセルシェーダーウェイブが実行される前に、いくつかの頂点シェーディング処理と重なっているのがわかります。これはフレームに依存しますが、一般的に非同期で実行することでフレーム時間が約0.1ミリ秒短縮され、PS4では約0.08ミリ秒のコストになります。


適用ステップのパフォーマンスを確認する前に、環境ライティングの適用に適用するコンピュートシェーダだけを見てみましょう。これは元の実装のフルスクリーンピクセルシェーディングパスにすぎず、このコンピュートシェーダーパスは、さまざまなshader permutationを持つDispatchIndirectの繰り返し呼び出しを使用して実行されます。

シェーダは、GroupIdを使用してタイルロケーションバッファを検索し、GroupThreadIdに基づいて個々のピクセル位置にアンパックすることによって始まります。GBuffer が読み込まれた後、ShadingModelID が上書きされることで、オプティマイザがshader permuationに基づいて定義されたプリプロセッサマクロに基づいてデッドコードの除去を実行することができます。

[hlsl]
// compute version of reflection and skylighting for dispatching tiles classified by shader featuress needed
[numthreads(8, 8, 1)]
void ReflectionEnvironmentSkyLightingCS(
uint3 GroupId : SV_GroupId,
uint3 DispatchThreadId : SV_DispatchThreadID, // DispatchThreadId = GroupId * int2(dimx, dimy) + GroupThreadId
uint3 GroupThreadId : SV_GroupThreadID, // 0 … THREADGROUP_SIZEX 0… THREADGROUP_SIZEY
uint GroupIndex : SV_GroupIndex) // SV_GroupIndex = SV_GroupThreadID.z * dimx * dimy + SV_GroupThreadID.y * dimx + SV_GroupThreadId.x
{
// lookup into tile data with gorup ID
uint TileLocationData = TileLocationBuffer[GroupId.x + TILE_PERMUTATION * NumTiles];
// unpack tile location
uint2 PixelPos = 0;
PixelPos.x = (TileLocationData & 0xFFFF) * 8 + GroupThreadId.x;
PixelPos.y = (TileLocationData >> 16) * 8 + GroupThreadId.y;
PixelPos += ViewDimensions.xy;

float3 UVAndScreenPos;
UVAndScreenPos.xy = (float2(PixelPos.xy + .5f) / (ViewDimensions.zw – ViewDimensions.xy);
UVAndScreenPos.zw = float2(2.0f, -2.0f) * UVANdScreenPos.xy + float2(-1.0f, 1.0f);

float4 SvPosition = float2(PixelPos.x, PixelPos.y, 0.f, 1.f);
float2 BufferUV = UVAndScreenPos.xy;
float2 ScreenPosition = UVAndScreenPos.zw;

// Sample scene textures.
FGBufferData GBuffer = GetGBufferDataFromSceneTextures(BufferUV);

// Sample the ambient occlusion that is dynamically generated every frame.
float AmbientOcclusion = AmbientOcclusionTexture.SampleLevel(AmbientOcclusionSampler, BufferUV, 0).r;

// override GBuffer Data if all pixels have same type
#if ALL_DEFAULT_LIGHTING
GBuffer.ShadingModelID = SHADINGMODELID_DEFAULT_LIT;
#elif ALL_FOLIAGE_LIGHTING
GBuffer.ShadingModelID = SHADINGMODELID_TWOSIDED_FOLIAGE+
#elif !HAS_COMPLEX_LIGHTING
// if no complex lighting pixels we can do this clamp as a hint that everything is either unlit or default lit
GBuffer.ShadingModelID = clamp(SHADINGMODELID_UNLIT, SHADINGMODELID_DEFAULT_LIT, GBuffer.ShadingModelID);
#endif
[/hlsl]


では、この0.08ミリ秒が、SSR、反射環境、SSSの適用において何を意味するのかを見ていく必要があります。これが、以前お見せしたシェーディングのオリジナルシーケンスです。


そして,これが我々の新しいフレームです。


ここでは、Tiled Reflection適用シェーダーが1.08msで、0.13ms向上しています。このメリットの約半分は、このフレームでスカイピクセルをカリングしたことによるものなので、スカイピクセルのないフレームではあまり意味がありません。ここで私が指摘したい1つのマイクロ最適化は、最初のバリアの後に、占有率の低い遅いウェーブを最初に並べ、最後に最も速いウェーブを並べるということです。これは、占有率の低いウェーブが、より多くのレジスタが使用可能になるのを待っているためだと思います。また、稼働率の低いウェーブほど稼働時間が長くなる傾向があるため、最速のバッチを最後に置くことで、次のバリアまでに仕事がすぐになくなるようにしています。


TiledReflectionの適用がわずかな利益を得ているのに対し、Screen Space Reflectionsは逆に実に大きな利益を得ています。0.3ms向上していますが、これはDFAOの履歴更新でウェーブがうまく重なっているためで、実際には控えめな改善です。これは、ウェーブがDFAOの履歴更新とうまく重なるようになったためです。これらは別々のバッファに書き込まれ、両方とも反射の適用に送られるため、バリアは必要ありません。SSRを使ったこれらの結果は、これが価値ある最適化になるという確信を最初に与えてくれました。


そして反射を適用した後のサブサーフェスも、0.58msと大幅に改善されています。


ここでは、セットアップとタイルクリアが実にきれいに重なり、タイルクリアはフルセットアップシェーダーよりもはるかに短いウェーブを持っているのがわかる。ブラーステップは純正のタイル分類化と同様で、スキンのあるタイルだけが実行されるため、再結合は非常に高速です。


さて、ここまで説明したところで……このパスが以前はどうだったのか、もう一度思い出しましょう。


そして結果に戻りましょう。これらのパスにより、合計で~1msの節約になりますが、これらの利点はシーンの構図によって異なるため、分類コストを差し引くと、このショットでは合計0.92msになります。


おわりに

今回は,最適化ネタの一つしてタイル分類化の資料を紹介してみました。
ライティング・SSR・Subsurfaceあたりにも適用できるので,かなり最適化に効きそうです。実際にPS4で1ms程度の改善があるという実績があるのも良いですね。
最近だとUE5のNaniteによる描画とかでも使われていますよね。
実装自体は,Wave64モードにしてタイルサイズを8×8にしてWave組み込み命令を駆使するのが個人的には妥当な気がします。
次回は,実装方法について紹介できるといいなと思っています。
他にもいい資料をご存じの方は,是非コメント等でご紹介ください。

アルファテストの改良

こんばんみん。
Pocolです。

ネットで記事を漁っていたら,アルファテストの品質向上の手法についての記事を見つけました。
https://asawicki.info/articles/alpha_test.php5

以前ゲーム開発をしていた際に,キャラクタの髪の毛や動物の毛周りで困ることがあったので,ハッシュ化アルファテストなどを試してみたのですが,結構ちらつきがやっぱりきになっちゃうなーと思っていましたし,意外と計算量多いんですよね。もっと手軽でそれっぽい方法無いかなーって常々思っていたのですが,記事で紹介している手法はかなりシンプルなので,個人的には「これでよくね?」って感じています。
アルファ値を次のように変えるのと,事前準備としてPhotoShopなどでSolidifyフィルタを使用してテクスチャエッジ部分の色を引き延ばしてテクスチャを作成しておけばよいみたいです。

[hlsl]
float alphaNew = max(alpha, (1.0/3/0) * alpha + (2.0/3.0) * threshold);
if (alphaNew < threshold)
discard;
[/hlsl]

Wave組み込み命令トリック

こんばんわ。
Pocolです。

Angry Tomato!さんという方が,“Compute shader wave intinsics tricks”
という記事を書いているので紹介です。
この記事では,以下のテクニックを紹介しています。

  • Branch optimization
  • Calculate on one lane, read on all
  • Serialization of Writing Data
  • Scalarization
  • Multiple wave parallelization
  • Indirect dispatch thread group count calculation
  • Dividing the work between lanes

非常に面白い内容だと思うので,見ていない方は是非見るとよいでしょう。