Saturday, August 27, 2016

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

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

Specifying Root Signatures in HLSL

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

cbuffer perObject : register(b0)
{
row_major matrix invVP;
}
TextureCube texCube : register(t0);
SamplerState samplerState : register(s0);
struct VsToPs
{
float4 pos : SV_POSITION;
float3 dir : DIR;
};
#define RSDEF "CBV(b0), DescriptorTable(SRV(t0)), StaticSampler(s0)"
[RootSignature(RSDEF)]
VsToPs VSMain(uint id : SV_VertexID)
{
VsToPs ret;
ret.pos = float4(id & 2 ? 1 : -1, id & 1 ? -1 : 1, 1, 1);
ret.dir = normalize(mul(ret.pos, invVP)).xyz;
return ret;
}
[RootSignature(RSDEF)]
float4 PSMain(VsToPs inp) : SV_Target
{
return texCube.Sample(samplerState, inp.dir);
}

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のどちらから取り出しても構いません。

...
ComPtr<ID3DBlob> vertexShader = afCompileHLSL(shaderName, "VSMain", "vs_5_0");
ComPtr<ID3DBlob> pixelShader = afCompileHLSL(shaderName, "PSMain", "ps_5_0");
ComPtr<ID3DBlob> rootSignatureBlob;
ComPtr<ID3D12RootSignature> rootSignature;
if (S_OK == D3DGetBlobPart(vertexShader->GetBufferPointer(), vertexShader->GetBufferSize(), D3D_BLOB_ROOT_SIGNATURE, 0, &rootSignatureBlob))
{
device->CreateRootSignature(0, rootSignatureBlob->GetBufferPointer(), rootSignatureBlob->GetBufferSize(), IID_PPV_ARGS(&rootSignature));
}
...
ComPtr<ID3DBlob> afCompileHLSL(const char* name, const char* entryPoint, const char* target)
{
char path[MAX_PATH];
sprintf_s(path, sizeof(path), "hlsl/%s.hlsl", name);
#ifdef _DEBUG
UINT flags = D3DCOMPILE_ENABLE_STRICTNESS | D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#else
UINT flags = D3DCOMPILE_ENABLE_STRICTNESS;
#endif
ComPtr<ID3DBlob> blob, err;
WCHAR wname[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, path, -1, wname, dimof(wname));
HRESULT hr = D3DCompileFromFile(wname, nullptr, nullptr, entryPoint, target, flags, 0, &blob, &err);
if (err) {
MessageBoxA(nullptr, (const char*)err->GetBufferPointer(), name, MB_OK | MB_ICONERROR);
}
return blob;
}

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版とほぼ同じです。

struct DDSHeader {
uint32_t h3[3];
int h, w;
uint32_t h2[2];
int mipCnt;
uint32_t h13[13];
uint32_t fourcc, bitsPerPixel, rMask, gMask, bMask, aMask, caps1, caps2;
bool IsCubeMap() const { return caps2 == 0xFE00; }
int GetArraySize() const { return IsCubeMap() ? 6 : 1; }
int GetMipCnt() const { return std::max(mipCnt, 1); }
};
struct TexDesc {
IVec2 size;
int arraySize = 1;
bool isCubeMap = false;
};
struct AFTexSubresourceData
{
const void* ptr;
uint32_t pitch;
uint32_t pitchSlice;
};
static ComPtr<ID3D12Resource> LoadDDSTexture(const char* name, TexDesc& texDesc)
{
int size;
void* img = LoadFile(name, &size);
if (!img) {
aflog("LoadDDSTexture failed! %s", name);
return nullptr;
}
const DDSHeader* hdr = (DDSHeader*)img;
AFDTFormat format = AFDT_INVALID;
int(*pitchCalcurator)(int, int) = nullptr;
switch (hdr->fourcc) {
case 0x31545844: //'1TXD':
format = AFDT_BC1_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 8; };
break;
case 0x33545844: //'3TXD':
format = AFDT_BC2_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
case 0x35545844: //'5TXD':
format = AFDT_BC3_UNORM;
pitchCalcurator = [](int w, int h) { return ((w + 3) / 4) * ((h + 3) / 4) * 16; };
break;
default:
ArrangeRawDDS(img, size);
format = AFDT_R8G8B8A8_UNORM;
pitchCalcurator = [](int w, int h) { return w * h * 4; };
break;
}
texDesc.size.x = hdr->w;
texDesc.size.y = hdr->h;
texDesc.arraySize = hdr->GetArraySize();
texDesc.isCubeMap = hdr->IsCubeMap();
int arraySize = hdr->GetArraySize();
int mipCnt = hdr->GetMipCnt();
std::vector<AFTexSubresourceData> r;
int offset = 128;
for (int a = 0; a < arraySize; a++) {
for (int m = 0; m < mipCnt; m++) {
int w = std::max(1, hdr->w >> m);
int h = std::max(1, hdr->h >> m);
int size = pitchCalcurator(w, h);
r.push_back({ (char*)img + offset, (uint32_t)pitchCalcurator(w, 1), (uint32_t)size });
offset += size;
}
}
ComPtr<ID3D12Resource> tex = afCreateTexture2D(format, texDesc, mipCnt, &r[0]);
assert(tex);
free(img);
return tex;
}
static const D3D12_HEAP_PROPERTIES defaultHeapProperties = { D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
ComPtr<ID3D12Resource> afCreateTexture2D(AFDTFormat format, const struct TexDesc& desc, int mipCount, const AFTexSubresourceData datas[])
{
D3D12_RESOURCE_DESC resourceDesc = { D3D12_RESOURCE_DIMENSION_TEXTURE2D, 0, (UINT64)desc.size.x, (UINT)desc.size.y, (UINT16)desc.arraySize, (UINT16)mipCount, format, {1, 0} };
ComPtr<ID3D12Resource> tex;
HRESULT hr = deviceMan.GetDevice()->CreateCommittedResource(&defaultHeapProperties, D3D12_HEAP_FLAG_NONE, &resourceDesc, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, nullptr, IID_PPV_ARGS(&tex));
afWriteTexture(tex, desc, mipCount, datas);
return tex;
}
フォーマット、幅、高さ、配列数、ミップマップ、キューブマップかどうかの情報を取得してD3D12_RESOURCE_DESCを作り、CreateCommittedResourceを呼びます。また、サブリソース毎のピクセルデータの先頭位置とサイズをAFTexSubresourceData配列にまとめておきます。

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


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

void afWriteTexture(ComPtr<ID3D12Resource> tex, const TexDesc& desc, int mipCount, const AFTexSubresourceData datas[])
{
const int maxSubresources = 100;
const UINT subResources = mipCount * desc.arraySize;
assert(subResources <= maxSubresources);
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprints[maxSubresources];
UINT64 rowSizeInBytes[maxSubresources], uploadSize;
UINT numRows[maxSubresources];
D3D12_RESOURCE_BARRIER transitions1[maxSubresources], transitions2[maxSubresources];
deviceMan.GetDevice()->GetCopyableFootprints(&tex->GetDesc(), 0, subResources, 0, footprints, numRows, rowSizeInBytes, &uploadSize);
ComPtr<ID3D12Resource> uploadBuf = afCreateBuffer((int)uploadSize);
assert(uploadBuf);
uploadBuf->SetName(__FUNCTIONW__ L" intermediate buffer");
D3D12_RANGE readRange = {};
BYTE* ptr;
HRESULT hr = uploadBuf->Map(0, &readRange, (void**)&ptr);
assert(ptr);
for (UINT i = 0; i < subResources; i++)
{
transitions1[i] = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE,{ tex.Get(), i, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST } };
transitions2[i] = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE,{ tex.Get(), i, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE } };
}
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
list->ResourceBarrier(subResources, transitions1);
for (UINT i = 0; i < subResources; i++)
{
assert(datas[i].pitch == rowSizeInBytes[i]);
assert(datas[i].pitch <= footprints[i].Footprint.RowPitch);
for (UINT row = 0; row < numRows[i]; row++) {
memcpy(ptr + footprints[i].Offset + footprints[i].Footprint.RowPitch * row, (BYTE*)datas[i].ptr + datas[i].pitch * row, datas[i].pitch);
}
D3D12_TEXTURE_COPY_LOCATION uploadBufLocation = { uploadBuf.Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, footprints[i] };
D3D12_TEXTURE_COPY_LOCATION nativeBufLocation = { tex.Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, i };
list->CopyTextureRegion(&nativeBufLocation, 0, 0, 0, &uploadBufLocation, nullptr);
}
list->ResourceBarrier(subResources, transitions2);
uploadBuf->Unmap(0, nullptr);
deviceMan.AddIntermediateCommandlistDependentResource(uploadBuf);
deviceMan.AddIntermediateCommandlistDependentResource(tex);
}
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"内部で行われる全ての内部的なステートを内包している、ということのようです。