GDI+のビットマップ転送速度

GDI+ はとても強力な描画命令を持っていて便利だけども、使ってみるとかなり遅い。
かなり単純な図形しか描画しなくても遅い。ということで、描画自体の速度はともかく、描画したGDI+のオフスクリーンサーフェスから、ウインドウへの転送速度が足を引っ張っていそうだ。
webで探してみてもやはりオフスクリーンサーフェスからの転送速度が遅いということを書いてあるページがいくつか見つかった。(このへんとか)
ということで、自分でもいろいろ調査してみた。

まずは一番シンプルで一般的と思われるコードは以下であり、これが遅い。
// オフスクリーンサーフェスの生成
Gdiplus::Bitmap bitmap(rect.Width(), rect.Height(), PixelFormat24bppRGB);

// オフスクリーンサーフェスに Graphicsクラスを使って描画
Gdiplus::Graphics g(&bitmap);
...

// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
Gdiplus::Graphics g2(pDC->GetSafeHdc());
g2.DrawImage(&bitmap, 0, 0);

真っ先に疑ったのはDrawImage関数の効率が悪いのではないかということだ。以前にもDirectXのD3DXLoadSurfaceFromMemory関数がとても遅くて、自前で書いたほうが遥かに速かったことがあり、かなり疑わしい。
ということで、オフスクリーンサーフェスからHBITMAPを取得して、BitBltでウインドウへ流し込んでみた。
// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
HBITMAP hBitmap = NULL;
bitmap.GetHBITMAP(0, &hBitmap);
{
  CDC dcMemory;
  dcMemory.CreateCompatibleDC(NULL);
  dcMemory.SelectObject(hBitmap);
  pDC->BitBlt(0, 0, bitmap.GetWidth(), bitmap.GetHeight(), &dcMemory, 0, 0, SRCCOPY);
}
DeleteObject(hBitmap);
残念ながら速度に違いは見られなかった。

つづいて、オフスクリーンサーフェスのバッファのポインタを直接取得して、StretchDIBitsで転送してみた。
// オフスクリーンサーフェスからウィンドウへの転送。pDCはウインドウのデバイスコンテキスト
Gdiplus::BitmapData bitmapData;
bitmap.LockBits(NULL, Gdiplus::ImageLockModeRead, PixelFormat24bppRGB, &bitmapData);

BITMAPINFOHEADER bmpInfoHeader;
memset(&bmpInfoHeader, 0, sizeof(BITMAPINFOHEADER));
bmpInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfoHeader.biWidth = bitmapData.Width;
bmpInfoHeader.biHeight = bitmapData.Height;
bmpInfoHeader.biPlanes = 1;
bmpInfoHeader.biBitCount = 24;

StretchDIBits(pDC->GetSafeHdc(),
    0, 0, bmpInfoHeader.biWidth, bmpInfoHeader.biHeight,
    0, 0, bmpInfoHeader.biWidth, bmpInfoHeader.biHeight,
    bitmapData.Scan0, (BITMAPINFO*)&bmpInfoHeader,
    DIB_RGB_COLORS, SRCCOPY);

bitmap.UnlockBits(&bitmapData);

おおっ、速い。でも、画像の上下がさかさまになってしまった...GDI+のオフスクリーンサーフェスはWindows従来のDIBとは上下が反転しているようだ。
まあ、GDI+にはSetTransform()という便利なメソッドがあり、ここでY座標をミラーする行列を仕込んでおけばあまりコードを汚さずに上下さかさまに描画できそうだ。
あとオフスクリーンサーフェスのピクセルフォーマットは当然24bitRGBのときに効率が一番よさそうだが、それ以外のピクセルフォーマットのときはもしかしたら一番最初のシンプルなコードでも問題ないかもしれない(実験してない)。

ということで結局、GDI+のオフスクリーンサーフェスには上下さかさまに描画して、StretchDIBitsで転送するという方法にしたら、 1600x1200程度のウインドウを描画するのに、もともと40fps程度だったのが80fps程度まで向上した。

と、ここまでやったところで気づいたんだが、GDI+のGraphicsはHDCからコンストラクトすることもできるので、実はDIB section ビットマップから作れば上下さかさまにせずとも速いのではなかろうか。
まずDIBSectionビットマップとGraphicsの生成。DIB Sectionビットマップにはオリジナルのラッパクラスを使ってもいいけど、ATLのCImageを使うと便利。
// DIB section と Graphicsの生成
CImage image;
image.Create(rect.Width(), rect.Height(), 32);
HDC hDC = image.GetDC();
Gdiplus::Graphics g(hDC);
image.ReleaseDC();
オフスクリーンサーフェスからウインドウへの転送はとてもシンプル。
image.BitBlt(pDC->GetSafeHdc(), 0, 0);

速度自体は上下さかさまに描画したものとそんなに差はないようだけど、これが最善っぽいね。あとDIBSectionの色深度を24ビットにすると遅かった。これはアライメントが影響してるんだろうか。でもBitmapクラスに上下さかさまに描画したときは24ビットでも速かったんだよなあ。ちょっと不思議だな。暇なときにまた調査してみよう。


とても長いエントリになってしまったが、結論。

オフスクリーンサーフェスに GDI+の Bitmapクラスを使うと遅い。DIBセクションビットマップを使うべし。

1/1