VS2015でDirectX12プログラミング

投稿日:

DirectX12が公開されていたので、プログラミングしてみました。
11とはかなり変わっていて、GPUを効率よく使用できる代わりにリソース管理の一部を自分で行う必要があります。便利な関数やDirectXTexやDirectXTKも用意されていません(2016年4月現在)。
Microsoftから公開されているサンプルプログラムを参考にしながらポリゴンモデルの表示とアニメーションさせるプログラムを作成してみました(以前作成したDirectX11プログラムの移植)。
開発環境はVisual Studio Community 2015、Windows10です。

ss_DX12Prog0


インストールと開発環境構築

VS2015とWindowsSDKのインストールで開発環境構築完了。
サンプルプログラムをコンパイルするときに「SDKバージョンの再ターゲット」がわからずに少し悩んだ程度で、すぐにDirextX12プログラミングをすることができました。
Visual Studio Community 2015は無料で使用できますが、登録が必要です。

DirectX12の特徴

DirectX12プログラミングでの苦労したところとその解決策です。本当に解決しているかどうか怪しいですが参考にしてください(ご利用は自己責任で!)。

画像ファイルからテクスチャ作成

最新(2016年4月)のDirectXTK、DirectXTexはDirectX11用です。自作すると時間がかかりそうなのでサンプルプログラムなどから移植することにしました。DDSとTGAファイルは、MicrosoftのサンプルminiEngine(GitHubで公開)から、jpg、pngなどの画像はサンプルがなかったためDirectXTex(DX11)のWICTextureLoaderを移植しました。
テクスチャリソースの管理が変わっただけで、ほかのパラメータなどはDirectX11とほぼ同じでした。
下で公開しているソースコード(DX12Prog0/Textureフォルダ)に、テクスチャ作成関連のソースを置いています。単独でコンパイルできるようにしているので、とりあえずDirectX12で画像ファイルを読み込みたい時にでも使ってください。

TextureLoader.h

こんな感じの関数を用意しています。画像データを書き込むには、一旦データコピー用の中間リソースが必要になるため、そのリソースのポインタを受け取るように引数を追加しています。

class TextureLoaderX12
{
public:
  TextureLoaderX12(ID3D12Device* dev, ID3D12CommandQueue* cmdq);
   ~TextureLoaderX12();

  // ファイルからテクスチャリソース構築 GPUコマンド実行&待ち
  // textureにテクスチャリソース viewにSRV作成
  HRESULT LoadDDSFileWait(const wchar_t* szFileName, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view);
  HRESULT LoadWICFileWait(const wchar_t* szFileName, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view);
	
	
  //テクスチャ読み込み static関数版
  //テクスチャリソースはD3D12_HEAP_TYPE_DEFAULTで作成
  //アップロード用の中間リソースを作成、GPUの転送処理終了まで保持しておく
  static HRESULT LoadDDSFile(ID3D12Device* dev, ID3D12GraphicsCommandList* cmd, const wchar_t* szFileName, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view, ID3D12Resource** upload);
  static HRESULT LoadWICFile(ID3D12Device* dev, ID3D12GraphicsCommandList* cmd, const wchar_t* szFileName, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view, ID3D12Resource** upload);
  static HRESULT LoadTGAFile(ID3D12Device* dev, ID3D12GraphicsCommandList* cmd, const wchar_t* szFileName, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view, ID3D12Resource** upload);
  static HRESULT LoadTGAMemory(ID3D12Device* dev, ID3D12GraphicsCommandList* cmd, const void* data, UINT size, ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view, ID3D12Resource** upload);

  //簡易テクスチャ作成
  //フォーマットと画像データからテクスチャリソース生成
  struct CInfo{
    UINT width;
    UINT height;
    DXGI_FORMAT format;
    UINT rowpitch;
    UINT slicepitch;
    const void* image;
    UINT image_size;
  };
  static HRESULT CreateTexture(ID3D12Device* dev, ID3D12GraphicsCommandList* cmd, const CInfo& cinfo,ID3D12Resource** texture, D3D12_SHADER_RESOURCE_VIEW_DESC* view, ID3D12Resource** upload);
};
ScreenGrab.h

あと、画面キャプチャの関数も機能限定ですがDirectXTex(DX11)から移植しています。

namespace DirectX
{
    HRESULT SaveDDSTextureToFile(_In_ ID3D12CommandQueue* cmdq,
                    _In_ ID3D12Resource* pSource,
                    _In_ D3D12_RESOURCE_STATES SourceState,
                    _In_z_ LPCWSTR fileName );

    HRESULT SaveWICTextureToFile(_In_ ID3D12CommandQueue* cmdq,
                    _In_ ID3D12Resource* pSource,
                    _In_ D3D12_RESOURCE_STATES SourceState,
                    _In_ REFGUID guidContainerFormat, 
                    _In_z_ LPCWSTR fileName,
                    _In_opt_ const GUID* targetFormat = nullptr,
                    _In_opt_ std::function<void(IPropertyBag2*)> setCustomProps = nullptr );
}
リソース削除

DirectxX12では、GPUリソース(メモリ)の削除管理を自分で行う必要があります。リソースを削除する場合、GPUの処理終了を確認してから削除しないと、削除済みのリソースにGPUがアクセスしてしまいます。これの怖いところは、メモリが上書きされない限り正常に動作しているように見えることです。ただし、この不正なメモリアクセスは、DirectX12のデバッグ機能を有効にするとリソース削除時にエラーメッセージを出力してくれます。また、DirectX12は引数の値や組み合わせが制限されていて、間違ったパラメータを渡すと、エラーメッセージと対処法が出力されます。プログラミングを始めたころは、このエラーメッセージとの戦いでした。

面倒ですが、リソース(メモリ)管理の自由度が増して、効率の良いメモリ管理が可能です。
miniEngine(LinearAllocatorクラス)では動的な定数バッファリソースを作成するとき、64kbのリソースを作成して先頭から順番にメモリ確保するような構造になっています。数十~数百回のリソース作成が1回で済むため、かなり効率がよくなっているはずです。

GPUの処理終了確認は、フェンスという同期処理を使用します。GPU処理終了時に指定した64bitの整数値をフェンスに出力、CPUからその値を読み込みことで、GPU処理が終了したかどうか調べることができます。今回作成したプログラムでは、リソース削除するときは、一旦削除リストに追加して、削除可能かフェンスの値を調べて一括削除するという仕様にしました(定期的に掃除関数呼び出し)。

リソースバリア

リソースには状態(D12_RESOURCE_STATES)があり、使用方法に沿った状態を指定(ResourceBarrier関数)する必要があります。GPUメモリアクセスは、GPU内部のスレッドやキャッシュなどで複雑になっているため、読み書きや使用方法を間違うとメモリアクセスの不整合が発生します。そのため、ResourceBarrierを使用前後に実行し、メモリアクセスの不整合を防止(たぶん、スレッドの同期、キャッシュ操作など)します。

リソースのMap、Unmapの仕様変更

リソース作成時にメモリの種類(D3D12_HEAP_TYPE_DEFAULT、D3D12_HEAP_TYPE_UPLOADなど)を指定します。DEFAULTで作成するとVRAM(などGPUから高速アクセス可能なメモリ)に配置されMap、Unmapでのアクセスが禁止されてしまいます。UPLOADではCPUからアクセス可能なメモリ(GPUからのアクセスは遅い)に配置され、Map、Unmapが実行可能です。DEFAULTのリソースを更新するには
・UPLOADでリソース作成、Map、Unmapで更新
・更新先リソースの状態をD3D12_RESOURCE_STATE_COPY_DESTに変更
・コピー命令発行(CopyBufferRegionなど)
・更新先リソースの状態を元に戻す
・コピー終了後UPLOAD用リソースを削除
のようになります。これは今までのMap、Unmap関数内部で実行されていた処理です(たぶん)。

D3D12_HEAP_TYPE_DEFAULTリソースへのアクセス

直接CPUからアクセスできないため、D3D12_HEAP_TYPE_READBACKのバッファリソースを作成、そこにアクセスしたいリソースをコピー、Map、Unmapでアクセスします。
テクスチャの場合、CopyBufferRegion関数は使用できないため、CopyTextureRegion関数を使用します。

画面キャプチャのソースコードの一部です。

D3D12_RESOURCE_DESC desc = pTexture->GetDesc();

D3D12_PLACED_SUBRESOURCE_FOOTPRINT fp;
UINT nrow;
UINT64 rowsize, size;
d3dDevice->GetCopyableFootprints(&desc,0,1,0, &fp,&nrow,&rowsize,&size);

//一旦READBACK用リソースにコピーしてから
D3D12_HEAP_PROPERTIES HeapProps;
HeapProps.Type = D3D12_HEAP_TYPE_READBACK;
HeapProps.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
HeapProps.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
HeapProps.CreationNodeMask = 1;
HeapProps.VisibleNodeMask = 1;
ComPtr<ID3D12Resource> res;
hr = d3dDevice->CreateCommittedResource(&HeapProps, D3D12_HEAP_FLAG_NONE
	, &CD3DX12_RESOURCE_DESC::Buffer(size),
	D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&res));

D3D12_TEXTURE_COPY_LOCATION td,ts;
td.pResource = res.Get();
td.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
td.PlacedFootprint = fp;
ts.pResource = pTexture.Get();
ts.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
ts.SubresourceIndex = 0;
cmdList->CopyTextureRegion(&td,0,0,0,&ts,nullptr);

//GPU処理終了を待ってからres->Map()
RootSignatureとPipelineState

GPUの描画処理に必要な設定、情報などをまとめて管理します。これまでの少しずつ設定をして最後にDrawという方法は廃止されています。これは前後の状態に依存しない構造にすることで、マルチスレッド(コア)での描画処理の同時実行に対応するためだと思います。また、構築時にGPU固有の命令を生成できるため描画実行時のCPU負荷の軽減になります。

使用するには、手続きが多く大変ですが、ドキュメントやネットの情報を頼りに何とか描画できるようになりました。まだよくわかっていない部分が多く、正しい使い方や効率の良い方法を模索していこうと思っています。

DescriptorHeap

GPUにテクスチャや定数バッファ、サンプラーの情報を渡すための機能です(たぶん)。一番よく分かっていない部分です。とりあえず動いている状態です。
テクスチャと定数バッファは、1つのDescriptorHeapにしか設定できません。それぞれDescriptorHeapを作成し、指定するとエラーになります。

頂点バッファのようにD3D12_VERTEX_BUFFER_VIEWを設定してコマンドにセットするだけでよさそうな気がします。どういう理由でこのような構造になっているのか、よくわかっていません。構築や設定の仕方で処理効率が大きく変わりそうなので、今後いろいろな構造や使用法を試してみようと考えています。

GPU実行管理

GPU処理を開始したり、CPUとの並列処理の実行管理も自分で行う必要があります。Mirosoftが公開しているサンプルのいくつかは、プログラムを簡単にするためにGPU処理実行後すぐに終了を待つ構造になっています(サンプルのコメントにも注意書き)。CPUとGPUを同時実行するためには、GPUが実行中のコマンドやリソースを終了するまで保持する仕組みが必要になります。一番簡単なのは、ダブルバッファ構造(CPUでのコマンド生成用とGPU実行用を交互に切り替え)です。

簡単なプログラムならダブルバッファで十分ですが、下で公開しているサンプルは、大量の描画処理にも対応できるリングバッファ構造にしてみました。CPUが設定したコマンドリストをGPUが追っかけていく構造です。GPU実行管理の一例として参考にしてください(サンプルソースコードzg_renderx12.h)。

コマンドリストとアロケータでの注意点

毎フレーム、ID3D12GraphicsCommandListとID3D12CommandAllocatorを生成、削除していると、削除に時間がかかるフレームが数百フレームに1回発生していました(環境によっては発生しないかも)。数ms~数十msと無視できる時間ではなかったため、生成、使用後に削除せずに次の処理で再利用するようにすると発生しなくなりました。生成、削除を繰り返すとなにか内部処理(ガベージコレクション的なもの?)が発生しているようです。元々生成削除を繰り返すものではない?。
リソースの生成、削除にもそれなりの時間がかかっているようなので、できるだけ生成、削除の回数をできる限り減らしたほうがよさそうです。

マルチサンプリング

SwapChain作成時にマルチサンプルを指定するとエラーになります。マルチサンプルを使うには、別にレンダーターゲットを作成、描画結果をResolveSubresource関数を使用し通常のテクスチャに変換する必要があります(変換先をSwapChainのフレームバッファに)。またマルチサンプルのレンダーターゲットをテクスチャにしてSwapChainのフレームバッファに描画する方法もあります(たぶん、ResolveSubresourceの内部処理?)。
ResolveSubresourceを使用するには、テクスチャの状態をD3D12_RESOURCE_STATE_RESOLVE_DEST、D3D12_RESOURCE_STATE_RESOLVE_SOURCEに変更します。

またシェーダを使った方法では、このようにマルチサンプルのデータを変換しています。どうすればいいかわからないので、単純に平均しています。サンプリング距離で重み付けしたりするのかもしれません。

float4 PSMain(PSInput input) : SV_TARGET
{
	float4 col;
	int2 wh;
	int nsample;
	texMSAA.GetDimensions(wh.x,wh.y, nsample);
	int2 uv = input.uv*float2(wh)+float2(0.0001f,0.0001f);
	col = float4(0,0,0,0);
	float sum = 0.0;
	for (int s = 0; s<nsample; ++s) {
		col += texMSAA.Load(uv, s);
	}
	//これでいいのか?
	return col / nsample;
}
シェーダ

以前作成したエフェクトシステムをDirectX12用に移植しました。


サンプルプログラム

とりあえず、DirectX12プログラミングしてみました。
手探りで作成したため、いろいろと間違っていると思いますが、DirectX12プログラミングの参考になれば幸いです。

“DirectX12 Programming Part0” をダウンロード DX12Prog0.zip – 1156 回のダウンロード – 425 KB


リンクなど

MMDモデル Tda式初音ミク デフォ服ver

【MMD】Tda式初音ミク デフォ服ver/ニコニコ静画

「VS2015でDirectX12プログラミング」への1件のフィードバック

  1. directx12のテクスチャ生成の関数頂きます
    他の関数でなかなかうまくいきませんでしたが
    こちらの関数で先に進めそうです
    ありがとうございました

コメントを残す

メールアドレスが公開されることはありません。

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.