こんにちは、みにくい社長です。
今日はスクリーンショット保存機能の実装についてです。
Nintendo SwitchやPlaystationでは、1ボタンでスクリーンショットを保存する機能がありますね。
windowsにも1ボタンでスクリーンショットを撮る(クリップボードにコピーする)機能がありますが、自分で作ったゲームの画面のスクリーンショットを撮りたい場合、意外と面倒だったりします。PCの場合はグラフィックカードのVRAMとメインメモリが分かれており、HEAP_TYPEによってデータの読み書きに制限があります。
具体的にはこのような流れになります。
1. RenderTargetを別のREAD_BACKメモリにコピー
2. READ_BACKメモリからMap(VRAM→メインメモリ)
3. PNGデータに変換
4. ファイルに保存
1. RenderTargetを別のREAD_BACKメモリにコピー
RenderTargetはMapすることができないため、D3D12_HEAP_TYPE_READBACKのバッファにコピーします。
CopyTextureRegionでコピーしますが、backBufferのステートをD3D12_RESOURCE_STATE_COPY_SOURCEに変更するのを忘れないように。
コピー後はGPUの完了待ちをしてください。
// コピー元のバッファを作成
ComPtr<ID3D12Resource> copySource(getBackBuffer()->getD3DTexture2D());
if (desc.SampleDesc.Count > 1)
{
auto descCopy = desc;
descCopy.SampleDesc.Count = 1;
descCopy.SampleDesc.Quality = 0;
descCopy.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT;
ComPtr<ID3D12Resource> pTemporary;
const CD3DX12_HEAP_PROPERTIES defaultHeapProperties(D3D12_HEAP_TYPE_DEFAULT);
auto hResult = getDevice()->CreateCommittedResource(&defaultHeapProperties, D3D12_HEAP_FLAG_NONE, &descCopy, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(pTemporary.ReleaseAndGetAddressOf()));
ASSERT(SUCCEEDED(hResult), "CreateCommittedResource");
D3D12_FEATURE_DATA_FORMAT_SUPPORT format = { desc.Format, D3D12_FORMAT_SUPPORT1_NONE, D3D12_FORMAT_SUPPORT2_NONE };
hResult = getDevice()->CheckFeatureSupport(D3D12_FEATURE_FORMAT_SUPPORT, &format, sizeof(format));
ASSERT(SUCCEEDED(hResult), "CheckFeatureSupport");
for (u32 item = 0; item < desc.DepthOrArraySize; ++item)
{
for (u32 level = 0; level < desc.MipLevels; ++level)
{
const u32 index = D3D12CalcSubresource(level, item, 0, desc.MipLevels, desc.DepthOrArraySize);
pCommandList->ResolveSubresource(pTemporary.Get(), index, getBackBuffer()->getD3DTexture2D().Get(), index, desc.Format);
}
}
copySource = pTemporary;
}
// コピー先のバッファを作成
pitch = static_cast<u32>(desc.Width) * sizeof(u32);
D3D12_RESOURCE_DESC bufferDesc = {};
bufferDesc.DepthOrArraySize = 1;
bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
bufferDesc.Format = DXGI_FORMAT_UNKNOWN;
bufferDesc.Height = 1;
bufferDesc.Width = pitch * desc.Height;
bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
bufferDesc.MipLevels = 1;
bufferDesc.SampleDesc.Count = 1;
ComPtr<ID3D12Resource> pStaging = nullptr;
const CD3DX12_HEAP_PROPERTIES readBackHeapProperties(D3D12_HEAP_TYPE_READBACK);
auto hResult = getDevice()->CreateCommittedResource(&readBackHeapProperties, D3D12_HEAP_FLAG_NONE, &bufferDesc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(pStaging.ReleaseAndGetAddressOf()));
ASSERT(SUCCEEDED(hResult), "CreateCommittedResource");
{
// バックバッファのバリアを変更
D3D12_RESOURCE_BARRIER outputBufferResourceBarrier
{
CD3DX12_RESOURCE_BARRIER::Transition(getBackBuffer()->getD3DTexture2D().Get(), getBackBuffer()->getD3DResourceStates(), D3D12_RESOURCE_STATE_COPY_SOURCE)
};
pCommandList->ResourceBarrier(1, &outputBufferResourceBarrier);
}
D3D12_PLACED_SUBRESOURCE_FOOTPRINT bufferFootprint = {};
bufferFootprint.Footprint.Width = static_cast<u32>(desc.Width);
bufferFootprint.Footprint.Height = desc.Height;
bufferFootprint.Footprint.Depth = 1;
bufferFootprint.Footprint.RowPitch = static_cast<u32>(pitch);
bufferFootprint.Footprint.Format = desc.Format;
const CD3DX12_TEXTURE_COPY_LOCATION copyDest(pStaging.Get(), bufferFootprint);
const CD3DX12_TEXTURE_COPY_LOCATION copySrc(copySource.Get(), 0);
// テクスチャをコピー
pCommandList->CopyTextureRegion(©Dest, 0, 0, 0, ©Src, nullptr);
{
// バックバッファのバリアを戻す
D3D12_RESOURCE_BARRIER outputBufferResourceBarrier
{
CD3DX12_RESOURCE_BARRIER::Transition(getBackBuffer()->getD3DTexture2D().Get(), D3D12_RESOURCE_STATE_COPY_SOURCE, getBackBuffer()->getD3DResourceStates())
};
pCommandList->ResourceBarrier(1, &outputBufferResourceBarrier);
}
pCommandList->Close();
pCommandQueue->ExecuteCommandLists(1, CommandListCast(pCommandList.GetAddressOf()));
pCommandList->Reset(getCommandAllocatorCopy().Get(), nullptr);
// GPU完了待ち
waitForGPU();
2. READ_BACKメモリからMap
Mapしてメインメモリにコピーします。
// メインメモリにコピー
void* pMappedMemory = nullptr;
D3D12_RANGE readRange = { 0, static_cast<size_t>(pitch * height) };
hResult = pStaging->Map(0, &readRange, &pMappedMemory);
ASSERT(SUCCEEDED(hResult), "Map");
pMemory = reinterpret_cast<u32*>(malloc(width * height * sizeof(u32)));
for (s32 i = 0; i < height; ++i)
{
memcpy(&pMemory[i * width], POINTER_ADD(pMappedMemory, i * pitch), pitch);
}
D3D12_RANGE writeRange = { 0, 0 };
pStaging->Unmap(0, &writeRange);
3. PNGデータに変換
mapでコピーしたデータはRGBAの並びになっていない場合がありますので、その場合は並び替えが必要です。 その後PNGのデータに変換する必要がありますが、さすがにここは大変なのでlibpngなどのライブラリを使用して変換しましょう。
4.ファイルに保存
後はメモリをファイルに書き込むだけなので省略します。
簡単そうな機能ですが、結構大変ですよね。 昨今のコンシューマー機はCPUとGPUでメモリが共有されている為、VRAMとのやり取りが無く非常に楽になりましたね。
では、またねー。