Windows 8 移行におけるD3DX11の代替法 その2

はじめに

引き続き、D3DX11からの脱却方法について書きます。 Windows8ではD3DX11は使えないみたいなので。 今回は、スクリーンショットを取る際に用いられるD3DX11SaveTextureToFileの代替方法を記します。
このメモ時点での環境。すでにVisual Studioはインストール済みの環境を想定しています。
  1. OS:Windows 8.1 64bit
  2. C++コンパイラ:Visual Studio 2012 pro
  3. SDK: Windows SDK(特にインストール作業などはいりません)
(2015/06/08: 画像の幅に関する部分を一部修正しました。)

処理の流れ

まずは描画からファイル出力までのだいたい流れを示します。 以下の順で処理することになります。
  1. DirectX11でレンダリング(SwapChainでフリップはまだしない)
  2. SwapChainからバックバッファの情報(フォーマットやサイズ)を取得
  3. CPU読み出し可能なバッファをGPU上に作成
  4. RenderTargetViewを通してレンダリング済み画像をバッファにコピー(GPU上の処理)
  5. CPU上のメモリにバッファを確保
  6. GPU上のバッファからCPU上のバッファへ画像を転送
  7. CPU上のバッファの画像をファイル書き出し
色々と面倒くさいんですが、上記のような手順を踏む必要があります。 通常の描画先となるRenderTargetは速度を優先させるためにCPUでの読み書きは通常不可にします。 その為、CPU読み出し可能なテクスチャバッファを作って、一旦そこへコピーします。 そこからCPU上のメモリへデータ転送して、後は煮るなり焼くなり好きなファイルフォーマットでストレージへ書き出します。

説明

では具体的な説明をします。 まずは、サンプルコードを見てください。 先に示した処理と対応付いた番号と共にコメントで簡易説明がついています。

//GLで描画した内容をビットマップ画像として保存//
void OutputDX11(ID3D11Device* pd3dDevice, ID3D11DeviceContext* pd3dDC, IDXGISwapChain* pSwapChain, ID3D11RenderTargetView* pRenderTargetView)
	

    //(1)DirectX11でレンダリングする関数.フリップはしない//
    RenderingDX11();

    
    //(2)バックバッファのフォーマットを取得//
    ID3D11Texture2D* pBackBuffer = NULL;
    HRESULT hr = pSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pBackBuffer );
    if( FAILED( hr ) ) return;
    
    D3D11_TEXTURE2D_DESC descBackBuffer;
    pBackBuffer->GetDesc(&descBackBuffer);
    pBackBuffer->Release();


    //(3)CPU読み出し可能なバッファをGPU上に作成//
    D3D11_TEXTURE2D_DESC Texture2DDesc;
    Texture2DDesc.ArraySize = 1;
    Texture2DDesc.BindFlags = 0;
    Texture2DDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    Texture2DDesc.Format = descBackBuffer.Format;
    Texture2DDesc.Height = descBackBuffer.Height;
    Texture2DDesc.Width = descBackBuffer.Width;
    Texture2DDesc.MipLevels = 1;
    Texture2DDesc.MiscFlags = 0;
    Texture2DDesc.SampleDesc.Count = 1;
    Texture2DDesc.SampleDesc.Quality = 0;
    Texture2DDesc.Usage = D3D11_USAGE_STAGING;

    ID3D11Texture2D *hCaptureTexture;
    pd3dDevice->CreateTexture2D( &Texture2DDesc, 0, &hCaptureTexture );

    
    //(4)作成したCPU読み込み可能バッファにGPU上でデータをコピー//
    ID3D11Resource *hResource;
    pRenderTargetView->GetResource( &hResource );
    pd3dDC->CopyResource( hCaptureTexture, hResource );
    hResource->Release();

    //(5)GPU上の読み込み可能バッファのメモリアドレスのマップを開く//
	D3D11_MAPPED_SUBRESOURCE mappedResource;
    pd3dDC->Map( hCaptureTexture, 0, D3D11_MAP_READ, 0, &mappedResource);
    
    //(6)CPU上のメモリにバッファを確保//
    double width = descBackBuffer.Width;
    double height = descBackBuffer.Height;
    double src_stride = mappedResource.RowPitch;    //(注)descBackBuffer.Width * 4とは必ずしも一致しない
    size_t buffer_size = src_stride * height;
    BYTE *bmp_buffer = new BYTE[buffer_size];

    //(7)GPU上の読み込み可能バッファからCPU上のバッファへ転送//
    CopyMemory(bmp_buffer, mappedResource.pData, buffer_size);
    pd3dDC->Unmap( hCaptureTexture, 0);
    hCaptureTexture->Release();
    
    //(8)CPU上のバッファの画像をファイル書き出し//
    SaveBMP(bmp_buffer, src_stride, width, height);    //src_strideを考慮して画像を出力すべし
    
    delete [] bmp_buffer;

}
コンパイル確認したソースとは多少異なりますが、まあ大丈夫でしょう。順を追って説明していきます。

まず、手順1として、DirectX11で何かしらのレンダリングをします。RenderingDX11関数は任意のレンダリングの関数へ置き換えて下さい。この時、SwapChainのPresentメソッドを呼ぶことで画像をバックバッファからフリップしますが、スクリーンショットを取る場合は、フリップしないでください。バックバッファの画像データをCPUへ転送する為です。フロントバッファから取得する方法もあるようですが、今回は省略します。

手順2として、バックバッファからスクリーンのサイズや色フォーマット等の情報を取得します。事前に分かっていれば、ここで取得する必要は無いかもしれません。方法としては、SwapChainのGetBufferメソッドを使用し、取得したバックバッファからGetDescメソッドで情報を取得します。

手順3として、バックバッファと同等のフォーマットを持つテクスチャリソースを作成します。ここで作成するテクスチャリソースはGPU上に作られます。バックバッファとの夕いつの違いは、CPUAccessFlagsにD3D11_CPU_ACCESS_READを指定して、CPUからの読み出しを可能にしておくことです。

手順4でバックバッファから作成したテクスチャリソースへ画像データをコピーします。DeviceContextのCopyResourceメソッドを使用します。 ここでのコピーはGPU上でのコピーであることに注意してください。

手順5でGPU上のメモリをアドレスへのマップを開きます。 今回はDeviceContextのMapとUnmapメソッドを使いました。

手順6でCPU上のメモリにバッファを作成します。サンプルコードでは色情報が32bitであることを仮定していますが、バックバッファのフォーマットに合わせて適宜変えてください。 一般に、GPUのフォーマットと最終的な画像ファイルのフォーマットは異なりますが、ここではGPUのフォーマットに合わせてメモリを確保します。 (注)また、GPU上ではテクスチャーの幅と、一行分のメモリstride(pitch)が異なる場合がありますので、注意する必要があります。一行分のメモリstrideはMapで開いたmappedResource.RowPitchとして取得できます。

手順7にて、GPU上の読み出し可能なテクスチャリソースから、CPU上のバッファへデータを転送します。

ここまでで、目的の画像データをCPUのメモリ上に取り出せました。最後に手順7として、お好みのファイルフォーマットへ変換してファイル書き出しを行ってください。SaveBMP関数はその為の関数として適当に作ったものです。各自でjpgやpngなどへ変換して保存しましょう。この作業が実は一番面倒くさいので、D3DX11SaveTextureToFileを使いたくなるんですよね。



これで、D3DX11SaveTextureToFileに関しては使用を回避できました。

余談

次回こそはD3DX11CreateShaderResourceViewFromFileの代替法を書きます。まあぶっちゃけると、自前で画像ファイルをメモリ上に読み出し、CreateTexture2Dメソッドの第二引数に初期データとして指定するだけなんですけどね。 とか言ってしまうと、もうメモを書き気が無くなる。。。

余談2

(2015/06/08)mappedResource.RowPitchの取得には気づかずにかなり悩みました。結局DirectXTexのソースコードを参考にさせてもらいました。