アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

DX12でスクリーンショット保存

こんにちは、みにくい社長です。

今日はスクリーンショット保存機能の実装についてです。
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(&copyDest, 0, 0, 0, &copySrc, 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とのやり取りが無く非常に楽になりましたね。

では、またねー。