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セクションビットマップを使うべし。

Visual Studio 2005 移行時の注意

仕事で使ったほうがよいのかどうか判定するため、Hamanaの環境をVS2003からVS2005に移してみた。
コンパイルはほとんど一発で通ったが、ものすごい数の警告がでる。原因は strcpy() などのセキュアでない古い関数を使っているためで、親切に 「strcpy_s()使ったほうがいいじゃない?」と警告をだしてくれるからであった。
もちろん pragmaで表示しないようにもできるが、これには素直に従ったほうが良さそうなので、大部分はセキュアなバージョンに置き換えた。

で、VS2005移行時に一番危険だと思ったのは以下のコードがコンパイルはできるが、実行するとファイルのオープンに失敗してしまい、正しく動かなくなることだ。
int main() {
  std::ofstream ofs("日本語.txt", ios_base::out | ios_base::trunc);
  if (ofs.is_open()) {
    ofs << "OK";
  }
  return 0;
}
日本語のファイル名を ofstream::open()関数に渡すとアウトらしい。英語のファイル名だと問題ないのでかなり危険だ。
デバッガで追ってみると open()関数内部で マルチバイト文字列からワイド文字列への変換を行っているのだが、そこが正しく変換してくれないことが原因だった。
どうやら ofstream::open()関数の実装が変わり、VS2003ではマルチバイト文字列のまま扱っていたのが、VS2005ではワイド文字に変換してから扱うようになったため、変換がうまく動くようにロケールを設定してあげないといけないようだ。

てことで、open()を呼び出す前に setlocale(LC_ALL, "Japanese"); を呼び出してロケールを日本語に設定することでうまく動作するようになった。多言語対応のアプリケーションの場合は以下のようにシステム標準のロケールを設定するといいと思われる。
int main() {
  setlocale(LC_ALL, "");
  std::ofstream ofs("日本語.txt", ios_base::out | ios_base::trunc);
  if (ofs.is_open()) {
    ofs << "OK";
  }
  return 0;
}

もう1点動かしてみて初めて発覚した問題は time_t のサイズが32bit → 64bitに増えていることだ。 susieプラグインのインターフェースに time_t型の変数を含む構造体があるのだが、見事にここでコケていた。
time_t型をシリアライズしたり、他DLLに渡している場合は注意が必要だ。

1/1