こんにちは、みにくい社長です。
今日はスクリーンショット保存機能の実装についてです。
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とのやり取りが無く非常に楽になりましたね。
では、またねー。