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バイト境界に揃っています。そこに注目するとこんな手抜きアップローダーも書けます。

void afWriteBuffer(const ComPtr<ID3D12Resource> res, const void* buf, int size)
{
void* p;
D3D12_RANGE readRange = {};
res->Map(0, &readRange, &p);
memcpy(p, buf, size);
D3D12_RANGE wroteRange = {0, (SIZE_T)size};
id->Unmap(0, &wroteRange);
}
ComPtr<ID3D12Resource> afCreateBuffer(int size, const void* buf)
{
D3D12_HEAP_PROPERTIES prop = { D3D12_HEAP_TYPE_UPLOAD, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 1, 1 };
D3D12_RESOURCE_DESC desc = { D3D12_RESOURCE_DIMENSION_BUFFER, 0, (UINT64)size, 1, 1, 1, DXGI_FORMAT_UNKNOWN, { 1, 0 }, D3D12_TEXTURE_LAYOUT_ROW_MAJOR, D3D12_RESOURCE_FLAG_NONE };
UBOID o;
deviceMan.GetDevice()->CreateCommittedResource(&prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&o));
afWriteBuffer(o, buf, size);
return o;
}
void UploadTexture(ComPtr<ID3D12Resource> tex, const void* buf)
{
D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint;
UINT64 uploadSize, rowSizeInBytes;
const D3D12_RESOURCE_DESC destDesc = tex->GetDesc();
deviceMan.GetDevice()->GetCopyableFootprints(&destDesc, 0, 1, 0, &footprint, nullptr, &rowSizeInBytes, &uploadSize);
assert(rowSizeInBytes == footprint.Footprint.RowPitch); // It is safe in most case!
ComPtr<ID3D12Resource> uploadBuf = afCreateBuffer((int)uploadSize, buf);
D3D12_TEXTURE_COPY_LOCATION uploadBufLocation = { uploadBuf.Get(), D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, footprint }, nativeBufLocation = { tex.Get(), D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, 0 };
ID3D12GraphicsCommandList* list = deviceMan.GetCommandList();
D3D12_RESOURCE_BARRIER transition1 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, D3D12_RESOURCE_STATE_COPY_DEST } };
list->ResourceBarrier(1, &transition1);
list->CopyTextureRegion(&nativeBufLocation, 0, 0, 0, &uploadBufLocation, nullptr);
D3D12_RESOURCE_BARRIER transition2 = { D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, D3D12_RESOURCE_BARRIER_FLAG_NONE, { tex.Get(), 0, D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE } };
list->ResourceBarrier(1, &transition2);
deviceMan.AddIntermediateCommandlistDependentResource(uploadBuf); // keep until the command queue has been flushed
}
assert(rowSizeInBytes == footprint.Footprint.RowPitch) の行でひっかかるテクスチャは横が256バイト単位でない場合です。実はある程度のX方向の大きさがあって、サイズがPower of Twoで、ミップマップを無視するならひっかからないはずです。

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

No comments:

Post a Comment