Sunday, January 8, 2017

[Vulkan] VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICが便利

VulkanのUniform Bufferには、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFERとVK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICの二種類がありますが、"DYNAMIC"はオフセットをコマンドバッファに一緒に積めるので便利です。

DirectX12やVulkanのような低レベルAPIでは、Constant buffer/Uniform buffer用に大きなGPU側バッファをまず確保しておいて、自前のアロケータで割り当てながら使うことになると思います。どのDraw Callがどのメモリ領域をConstant buffer/Uniform bufferとして使うかは毎フレーム変化するしかないので、Draw Call毎にGPUアドレスを渡すことになります。

それは、VK_DESCRIPTOR_TYPE_UNIFORM_BUFFERを使っても可能なのですが、GPUアドレスはdescriptor setに書き込んで渡す必要があるので、uniform bufferを使う回数分vkAllocateDescriptorSetsとvkUpdateDescriptorSetsでdescritor setも作ってあげる必要があります。

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICを使うと、descritor setは1個だけ、大きなGPUバッファの先頭アドレスを書き込んだものを用意するだけで済みます。その代わりdraw call毎にvkCmdBindDescriptorSetsでUniform bufferをバインドするときにオフセットを指定します。複数のdescriptor setを管理する手間が省けるので便利です。

NVIDIAのドキュメントによると、NVIDIAのハードウェアで"Uniform Buffer Dynamic Binding"は速いのだそうです。
https://developer.nvidia.com/vulkan-shader-resource-binding

推測に過ぎませんが、NVIDIAのハードウェアではDirectX12の"Root Constant"が"Uniform Buffer Dynamic Binding"に相当するのかもしれません。
DX12 Do's And Don'tsのRoot Signaturesの項で"sit in the root"でバインドしたコンスタント/CBVが特にPixel Shaderで速いとされています。
https://developer.nvidia.com/dx12-dos-and-donts

SetGraphicsRootConstantBufferViewもDescriptor Heapを介さずにコマンドリストに直接GPUアドレスを乗せるという部分も、descriptor set1個を使いまわせるVK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICに似ています。

Wednesday, November 2, 2016

Vertex Array Object(VAO)を使わない理由

OpenGL ES 3.xではVertex Array Object(VAO)が導入され、APIコールが減って効率的ということになっています。ところが、コーディングの特殊さによるVAOの弊害が多く、多分実行速度も変わらない気がしたので、VAOを使わなくてもいい理由を書いてみようと思いました。(ちなみに、Unreal Engine 4もVAOを使っていないようです)プラットフォームは主にAndroidを想定しています。

1. VAOが頂点バッファと結びつくのが困る


頂点バッファをシェーダに結びつけるいわゆる"Input Layout"はAPI毎に格納されるオブジェクトが違います。DX11ではID3D11InputLayout、DX12やVulkanではPSO(pipeline state object)、ES 3.xはVAOに格納されます。

そのうちVAOだけの厄介な点は特定の頂点バッファと結びつく仕様になっていることです。実務でもありそうな例として、モンスターA、B、Cを、Gバッファ生成シェーダー、シャドウマップ生成シェーダ、光学迷彩シェーダで描画するとします。頂点バッファから送るのはGバッファは全情報、シャドウマップは座標だけ送れば十分、光学迷彩はテクスチャマッピングを省くことにします。

VAOを使って必要な情報のみ必要なシェーダに送るためには、モンスター3種とシェーダー3種、3x3=9個のVAOが必要になります。さすがにこのような煩雑なことはしたくないので、実際にはモンスター毎に1つずつVAOを作って、それぞれ頂点の全情報を送るように設定し、あとはドライバレベルでの最適化に託す、という書き方になりそうです。

2. わかりにくいバグを作りやすい


経験上、描画が終わったらglBindVertexArray(0)でVAOのバインドを忘れず解除すべきです。なぜなら、バインドしっぱなしにすると他の描画モジュールがバインド中のVAOを書き換えることが出来てしまい、どこでどう地雷を踏んでいるかわからない難解なバグとなります。

描画の為のglBindBufferやglVertexAttribPointerのみならず、glBindBufferの周辺はVAOを書き換える可能性に気を使います。例えばインデックスバッファを生成したり書き換えたりする時にglBindBufferを呼びますが、これがよそのVAOを壊してしまうかもしれません。


3. VAOでAPIコールを減らしても(多分)速くならない


VAOを採用する動機は、なんとなく速そうという期待ではないでしょうか。しかし、本当に速くなるでしょうか。VAOが無いES2.0は、頂点バッファを切り替える度にglBindBufferとglVertexAttribPointerの複数回に及ぶコールで毎回”Input Layout"に該当する情報をドライバに伝えます。それが省ける点はVAOが有利に見えます。

ところが、DX12やVulkanを見るとVAOは少なくとも最近のハードウェアの実装からかけ離れている事が想像できます。バッファはコマンドリストに乗るGPUアドレスに過ぎず、Input LayoutはPSOの一部に過ぎません。この2つを取り出して1つにまとめる事そのものに最適化的な利点は期待できなさそうです。

OpenGLのドライバの実装を想像してみます。GLも内部でPSOのような物を持っているはずです。PSOの生成は重い処理なので、一回作ったらハッシュ値などで探せるようにして実体をキャッシュしておくでしょう。他のステートが決まらないとPSOが確定できないので、ハッシュ値の計算もPSOの生成もすぐには行われず、ドローコール(glDrawElements等)のタイミングで行われるはずです。

こう考えると、glVertexAttribPointerにしろ、glBindVertexArrayにしろ、ハッシュ値を計算するための元データの提供に過ぎず、バッファのバインドを除いてCPU内で完結しています。VAOを使ったほうがAPIの呼び出しは減るかもしれませんが、無理にVAOを使って得られるものがあるわけではなさそうです。

別の視点から見てみると、また、現状多くのゲームが互換性のためにES2.0で実装されています。ドライバ開発者の立場としても、VAOありきで最適化できないと思われます。

4. ES2.0がまだまだ現役、そしてVulkanの登場


従来はES2.0をベースとして上位機種はES3.x採用という戦略がありました。今後は上位機種はVulkanで互換性のためにES2.0、という組み合わせが増えると思います。対応機種を狭める上に将来的にVulkanに置き換えられる運命にある、ES3.xの必要性が薄れてきました。

現在のようにAPIが移行期にある中では特定APIに依存しないように抽象化を試みることも多いと思いますが、VAOはその特殊さゆえに抽象化がとても難しいです。ES2.0のglVertexAttribPointerも非常に変則的ですが、使うシェーダが決まるまで頂点レイアウトの決定を保留できる分、VAOよりは抽象化はしやすいと言えるかもしれません。

Saturday, September 3, 2016

[DX12] VBV、IBVはAPI呼出し後すぐ破棄してもOK

頂点バッファの定義で、例えば以下のように2つの変数を定義して使うのを見かけると思います。

頂点バッファはID3D12ResourceとD3D12_VERTEX_BUFFER_VIEW、インデックスバッファはID3D12ResourceとD3D12_INDEX_BUFFER_VIEWが必要です。一つのリソースに2つの宣言が必要で煩雑なのですが、実はVIEWの宣言は省略してもよさそうです。

DirectX11ではSRVやDSVなど、GPUから参照するリソースに付加情報を付けてバインドする構造をViewと呼んでいました。 DirectX12になってD3D12_VERTEX_BUFFER_VIEW(VBV)やD3D12_INDEX_BUFFER_VIEW(IBV)という構造体がいわゆるViewの一種に加わりましたが、Viewの概念が若干変わったようです。DX11ではViewはその実体がGPU側にあるのかCPU側にあるのかは隠蔽されていました。それが、DX12ではGPU側なのかCPU側なのかは明確に区別されます。

この辺は、マイクロソフトのYouTubeのチャンネル「Microsoft DirectX 12 and Graphics Education」に登録されている「Resource Binding in DirectX 12 (pt.1)」に詳しいです。



8:30付近で出る表と13:00の解説によると、CBV、SRV、UAV、SAMPLERはGPUに配置され、それ以外のIBV、VBV、SOV、RTV、DSVはCPU側にのみ存在し、更にはドライバもVIEW(descriptor)の複製をコマンドリストに積むのでVIEWへの参照を保持しないとのことです。

MicrosoftのDirectX-Graphics-Samplesサンプル中、D3D12HelloWorldソリューションでは頂点バッファのID3D12Resourceに加えてD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを保持していますが、1つのバッファに対して2つの変数を管理するのは二度手間に感じます。

この二度手間を解決するため、上の動画から得られた事実を利用します。アプリケーションはID3D12Resourceのみ保持し、VIEWはスタック上に毎回作ることにします。例えばインデックスバッファをコマンドリストに積むためにこんな関数を作れば、今後D3D12_INDEX_BUFFER_VIEW構造体の存在を忘れてしまうことができます。

同じDirectX-Graphics-SamplesでもMiniEngineの中では、スタック上に作ったD3D12_VERTEX_BUFFER_VIEWやD3D12_INDEX_BUFFER_VIEWを直接コマンドリストに流すようになっているようです。

ところで、D3D12_INDEX_BUFFER_VIEWとD3D12_VERTEX_BUFFER_VIEWは構造体でしたが、RTVやDSVはID3D12DescriptorHeapの形をとっています。これは実に奇妙に見えます。なぜなら、同じID3D12DescriptorHeapを使うSRVやCBVの場合、DescriptorがGPUメモリ上に存在し、シェーダーから参照されるため、プログラマはフェンスを駆使してID3D12DescriptorHeapの寿命管理を行わなければいけません。ところが、RTVやDSVはAPI呼び出し後はコマンドリストが実行前でもID3D12DescriptorHeapを破棄してもRTVやDSVを書き換えても構わないという事です。

内部の実装がも異なるオブジェクトが同じID3D12DescriptorHeapの形を取っているため、混乱の元になりそうです。

ともかく、この事実を利用して管理するオブジェクトを減らすことができます。以下はID3D12DescriptorHeapをスタック上に作ってOMSetRenderTargetsした後バッファをクリアする例です。ComPtrなのでID3D12DescriptorHeapは関数を抜けると消滅します。

残念ながら以下のコードはDebug Layerがエラーを出します。コマンドリスト実行終了前にID3D12DescriptorHeapを解放していないか検出するためにDebug Layerが参照しているようです。Debug Layerを外せば問題無さそうですが、まともにデバッグできないのでやめたほうがよさそうです。ただし、ID3D12DescriptorHeapを使わないVBVやIBVは前述の通りMiniEngineでもスタックに作って即破棄いるのでAPI呼出し後の破棄が合法と見て間違いなさそうです。


使い捨てにするとDebug Layerでエラーが出ますが、ID3D12DescriptorHeapをそれぞれ1個だけ作って毎回書き換えるように使うのはエラーにはなりません。上の動画の13:00に大丈夫と書いてあります。(もちろんSRVやCBVでこれをやるとアウトです)


DX12はアプリケーションがローレベルを意識するAPIなので、RTVやDSVもD3D12_VERTEX_BUFFER_VIEWのようにただの構造体にしてもよかったのではないでしょうか。そうすればGPU側はID3D12DescriptorHeap、CPU側はただの構造体、という風に住み分けができて分かりやすくなると思うからです。この辺は、おそらくはDX11時代のID3D11RenderTargetViewやID3D11DepthStencilView等の歴史的な経緯も関係がありそうです。

Saturday, August 27, 2016

[DX12] HLSLにRoot Signatureを定義する

HLSLにRoot Signatureをattributeとして書く方法は、MSDNに詳しい説明があります。

Specifying Root Signatures in HLSL

MSDNのサンプルは機能紹介のためか「全部入り」の複雑なものになっていますが、ここではもっとシンプルなHLSLにRoot Signatureを書いてみました。キューブマップを空とみなして画面全体にレンダリングしています。


Vertex ShaderとPixel Shaderの両方に同じ定義をするために#defineで文字列を定義します。文字列1つで短く書けて、C++側で構造体と列挙体を駆使して書くより圧倒的に読みやすくで直感的です。

文字列の全体が、D3D12_ROOT_SIGNATURE_DESCに該当します。

StaticSamplerが、D3D12_STATIC_SAMPLER_DESCに該当する記述になります。フィルタリングの方法なども書けますが、レジスタ以外は省略可能です。嬉しいのはHLSL内でサンプラーの定義から使用まで完結することです。C++側から何もする必要がありません。

今回は書いていませんが、RootFlagsがD3D12_ROOT_SIGNATURE_FLAGSに該当します。ハマりそうな点としては、ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTを書き忘れるとおそらく頂点入力が出来なくなるので気をつけます。今回は頂点はシェーダー内で生成しているので不要です。

RootFlagsとStaticSamplerを除いたそれ以外が、D3D12_ROOT_PARAMETERの配列と同等になります。この並びがC++側から参照するインデックス、つまりSetGraphicsRootConstantBufferViewやSetGraphicsRootDescriptorTableの引数RootParameterIndexに該当します。

こうしてHLSLのattributeに書いたRoot Signatureはシェーダのバイナリに含まれます。MSDNではfxcを使っていますが、D3DCompileFromFile等でID3DBlobを生成している場合にもD3DGetBlobPartでRoot Signatureを取り出せます。Vertex ShaderとPixel Shaderのどちらから取り出しても構いません。


D3DGetBlobPartにD3D_BLOB_ROOT_SIGNATUREを渡すことでシェーダーバイナリからroot signatureを取り出します。 これは、以前はD3D12SerializeRootSignatureで作っていたroot signatureのバイナリを置き換えるもので、どちらも最後はCreateRootSignatureでID3D12RootSignatureを生成します。

ところで、MSDNによるとDX11でもRoot Signature入りのシェーダを問題なく使えるようで、Root Signatureを単に無視すると書いてあります。別の視点から見ると、DX12でのみ必要だったコードをC++から追い出して、プラットフォームの差を吸収するのにも役立ってくれそうです。

Sunday, August 21, 2016

DX12でDDSファイルからテクスチャ生成

DirectX11でのDDSロードの記事はこちらです。

以前、GetCopyableFootprintsの記事では手抜きで生成していたテクスチャですが、今回はミップマップやキューブマップも考慮して真面目に生成してみます。

ところで、テクスチャのアップロードは既にMicrosoftのD3DX12.hのUpdateSubresources関数があります。D3DX12.hが優れている所としては、必要なリソースはUpdateSubresourcesの引数を通してのみ取得しており、透明であること、マルチスレッド化が容易であることが挙げられます。反面、やや使いにくい点としては、D3DX12.hはDX12特有の概念であるリソースバリアやアップロードバッファの管理を呼び出し側に委ねています。使用者はDX12のアーキテクチャを理解した上で使わなければいけないので、手軽に使えるものではないです。また、DX12を隠蔽するものではないので、マルチプラットフォーム化を目指した設計の中では使いにくそうです。

今回はD3DX12.hを使用しないで実装します。

DDSのロード


DDSファイルをメモリ上にまるごとロードして解析します。以前やったDX11版とほぼ同じです。

フォーマット、幅、高さ、配列数、ミップマップ、キューブマップかどうかの情報を取得してD3D12_RESOURCE_DESCを作り、CreateCommittedResourceを呼びます。また、サブリソース毎のピクセルデータの先頭位置とサイズをAFTexSubresourceData配列にまとめておきます。

テクスチャメモリへの転送


ここの処理の詳細は、以前やったGetCopyableFootprintsの記事を見てもらうと分かり易いと思います。違いは、今回の実装はミップマップやキューブマップもサポートします。

GetCopyableFootprintsでアップロードバッファの大きさと各サブリソースの配置方法を取得します。forループ内ではサブリソースをアップロードバッファに配置し、CopyTextureRegionで転送させます。リソースバリアはCopyTextureRegionのforループの前後で全てのサブリソースに対して一括で発行します。その為にD3D12_RESOURCE_BARRIERを配列で作っておきます。forループ内でサブリソース毎に個別にResourceBarrierを呼び出しても動きますが、マイクロソフトの開発者の動画 https://youtu.be/Db2TaG49SRgによるとまとめて一回呼ぶほうが良いとのことです。ここは、日本語で要約した記事も参考にしてください。

コマンドリスト実行前のID3D12Resourceの保護


最後に、アップロードバッファからテクスチャへの転送を行うコマンドリストの実行が終わるまで、転送元であるアップロードバッファと転送先であるテクスチャの双方が間違って解放されないように保護しておく必要があります。

AddIntermediateCommandlistDependentResourceがそれをやっていて、何をするかというとAddRefしてコンテナに追加しておくだけで、コマンドリストの終了を確認したら全てReleaseします。

アップロードバッファが保護されるべきであるのは分かり易い所ですが、「生成したばかりのテクスチャが即不要になる」というのは無さそうで有り得るケースです。GPUがピクセルを転送しようとしたら転送先のテクスチャがなくなっていて不正なメモリアクセスというケースはDX12では発生します。コマンドリストから参照されている以上、テクスチャも忘れずに保護しておきます。

ちなみに、GitHubにあるマイクロソフトのDirectX-Graphics-Samplesではその場でコマンドリストを実行してフェンスで待つようになっています。その為アップロードバッファを生成した同じ関数内で使用済みになりその場で解放できるメリットがありますが、CPUはGPUがテクスチャを作る間ブロックされています。

まとめと課題


DX12のテクスチャの生成で分かり難いのは、アップロードバッファやリソースバリアなどの存在があると思いますが、この辺を隠蔽してかつてのDirectX SDKの感覚でテクスチャを生成できるようにしてみました。ただし今回の実装は、関数の引数が簡潔な反面、コマンドリストやコマンドリストから参照中のアップロードバッファの保持などを外部のモジュールに依存しているので、透明ではなく、そのままマルチスレッド化できないものになってしまいました。テクスチャのアップロード1つ取ってもどんな設計を選択するか、これもまたDX12時代にゲームエンジン開発者に委ねられた課題となりました。

Monday, August 15, 2016

DirectX 12: Resources Barriers and You を要約

MicrosoftのYouTubeチャンネル「Microsoft DirectX 12 and Graphics Education」より、「DirectX 12: Resources Barriers and You」を要約してみました。



リソースバリアの3つの役割

  1. リソースのステートの変更(例:RT=レンダーターゲットからSRVへの変更等)
  2. キャッシュコヒーレンシの確保
  3. パイプラインストール(例:書き込みの後に読み込みをする場合、順序が前後しないことを保証)


パフォーマンスの為の3つのルール

  1. D3D12_RESOURCE_STATE_COMMONとD3D12_RESOURCE_STATE_GENERIC_READステートは避けます。

    GENERIC_READは、D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER、D3D12_RESOURCE_STATE_INDEX_BUFFER、D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE、D3D12_RESOURCE_STATE_INDIRECT_ARGUMENT、D3D12_RESOURCE_STATE_COPY_SOURCEというフラグを全て含みます。これはリソースがあらゆる場所で使われる可能性があると見なされて多くのパイプラインストールを発生させます。GENERIC_READを使うのは"Upload heap"、すなわち、CPUからGPU側のバッファを更新する時に限るべきです。

    D3D12_RESOURCE_STATE_COMMONを使うべき場面はCPUがテクスチャにアクセスする場合と、"Copy engine"※に渡すCopy queueにリソースを渡す場合に限ります。

    SRVはD3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCEとD3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCEの二種類あります。通常は本当に使われるもの一つを指定したほうがよいですが、両方で使われることがわかっていればor演算子で両方指定します。1つだけ指定するのがよいと思ってRT→PS→NON PSのようにステートを渡り歩くのは良くないです。 
  2. 不必要なトランジションは避ける。例えば実装の都合で任意に中間ステートなどを置いたりしないことです。高くつく可能性があります。
  3. 複数リソースのトランジションを纏めて発行してパフォーマンスアップ  ID3D12GraphicsCommandList::ResourceBarrierには複数のバリアを引数として渡せます。例えば DS=depth stencilとRTをそれぞれSRVにするなら2つ同時に指定するべきです。また、複数リソースをSRVからD3D12_RESOURCE_STATE_COPY_DESTに変更して書き換える場合も、個別にResourceBarrierを呼ぶのではなくResourceBarrierの一度のコールで複数のリソースを指定すべきです。

おまけ
Depth bufferはハードウェア内部で圧縮されていて、SRVに変更をするとその時点から解凍作業が始まってすぐに使えません。 そこで、D3D12_RESOURCE_BARRIER_FLAG_NONEの替わりに、D3D12_RESOURCE_BARRIER_FLAG_BEGIN_ONLYとD3D12_RESOURCE_BARRIER_FLAG_END_ONLY の2つに分けて発行して、BEGINとENDの間にGPUに別の計算をさせると、計算と解凍を並行に走らせることができます。

※ "Copy engine"が何なのかわからなくて調べてみたのですが、どうやらここが解説ページです。

https://msdn.microsoft.com/en-us/library/windows/desktop/dn899217(v=vs.85).aspx

モダンなGPUハードウェア内に実装された3つのエンジン"Copy engine"、"3D engine"、"Compute engine"のうちの1つであり、D3D12_RESOURCE_STATE_COMMONステートはその"Copy engine"内部で行われる全ての内部的なステートを内包している、ということのようです。

Thursday, July 28, 2016

[DX12] GetCopyableFootprintsの謎に迫る

DirectX12ではテクスチャを作る時にメインメモリでもテクスチャでもない、中間バッファ(UPLOAD heap)を経由します。その際、ID3D12Device::GetCopyableFootprintsという関数で取得したレイアウトに従ってピクセルを中間バッファに格納します。

GetCopyableFootprints関数と共にいまいち存在意義がわかりにくい中間バッファがなぜ必要なのでしょうか。

答えはMSDNにありました。
https://msdn.microsoft.com/en-us/library/windows/desktop/dn899215(v=vs.85).aspx

テクスチャはキャッシュ効率を上げるためGPU上ではDDSファイルのように一直線(linear)に配置されておらず、non-linearな未知のレイアウト(unknown layout)として隠蔽されています。中間バッファの内容をそのGPUに都合の良い形式に変換してくれるのが、ID3D12GraphicsCommandList::CopyTextureRegionというわけです。

中間バッファに配置するピクセルもまたGPUに都合の良いようにアラインメントを揃える必要があり、そのための定数が定義されています。

  • D3D12_TEXTURE_DATA_PITCH_ALIGNMENT 
  • D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT

MSDNによると、サブリソース(Subresource、ミップマップやキューブマップなどのテクスチャの構成単位)ごとに512バイト境界、テクスチャの各Row(行、つまりX方向一列)は256バイト境界に揃っている必要があります。

GetCopyableFootprintsを呼び出すと、各サブリソースをどのように一次元のバッファに格納すればいいのかD3D12_PLACED_SUBRESOURCE_FOOTPRINT構造体で教えてくれます。つまり、GetCopyableFootprintsはただのヘルパー関数であって、デバイスを操作したりデバイスに問い合わせたりしません。また、GetCopyableFootprintsを使わず自力で配置しても構いません。

中間バッファは二次元や三次元ではない、Constant Bufferなどと同じただの一次元バッファ(D3D12_RESOURCE_DIMENSION_BUFFERで、D3D12_HEAP_TYPE_UPLOAD)として生成するのですが、その中にGPUが読めるようにサブリソースを間違いなく配置する必要があります。

ところで、DDSなどから読み込んだテクスチャは多くの場合既にRowが256バイト境界に揃っています。そこに注目するとこんな手抜きアップローダーも書けます。

assert(rowSizeInBytes == footprint.Footprint.RowPitch) の行でひっかかるテクスチャは横が256バイト単位でない場合です。実はある程度のX方向の大きさがあって、サイズがPower of Twoで、ミップマップを無視するならひっかからないはずです。

(AddIntermediateCommandlistDependentResourceは、コマンドバッファの実行が終わるまで中間バッファを保持しておくためのものです。今回の主題ではないので説明は省略します)