GDI + DrawImage заметно медленнее в C ++ (Win32), чем в C# (WinForms) - PullRequest
1 голос
/ 02 марта 2020

Я портирую приложение с C# (WinForms) на C ++ и заметил, что рисование изображения с использованием GDI + намного медленнее в C ++, даже если оно использует тот же API.

Изображение загружается в запуск приложения в System.Drawing.Image или Gdiplus::Image соответственно.

Код чертежа C# (непосредственно в основной форме):

public Form1()
{
    this.SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
    this.image = Image.FromFile(...);
}

private readonly Image image;

protected override void OnPaint(PaintEventArgs e)
{
    base.OnPaint(e);
    var sw = Stopwatch.StartNew();
    e.Graphics.TranslateTransform(this.translation.X, this.translation.Y); /* NOTE0 */
    e.Graphics.DrawImage(this.image, 0, 0, this.image.Width, this.image.Height);
    Debug.WriteLine(sw.Elapsed.TotalMilliseconds.ToString()); // ~3ms
}

Относительно SetStyle: AFAIK эти флаги (1) заставляют WndProc игнорировать WM_ERASEBKGND, а (2) выделяют временные HDC и Graphics для рисования с двойной буферизацией.


Код рисования на C ++ более раздутый , Я просмотрел справочный источник System. Windows .Forms.Control, чтобы увидеть, как он обрабатывает HD C и как он реализует двойную буферизацию.

Насколько я могу судить, моя реализация так близко соответствует (см. NOTE1) (обратите внимание, что сначала я реализовал это в C ++, а , а затем посмотрел, как это в исходном коде. NET - возможно, я упустил некоторые вещи). Остальная часть программы более или менее соответствует тому, что вы получаете при создании проекта fre sh Win32 в VS2019. Вся обработка ошибок опущена для удобства чтения.

// In wWinMain:
    Gdiplus::GdiplusStartupInput gdiplusStartupInput;
    Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    gdip_bitmap = Gdiplus::Image::FromFile(...);

// In the WndProc callback:
case WM_PAINT:
    // Need this for the back buffer bitmap
    RECT client_rect;
    GetClientRect(hWnd, &client_rect);
    int client_width = client_rect.right - client_rect.left;
    int client_height = client_rect.bottom - client_rect.top;

    // Double buffering
    HDC hdc0 = BeginPaint(hWnd, &ps);
    HDC hdc = CreateCompatibleDC(hdc0);
    HBITMAP back_buffer = CreateCompatibleBitmap(hdc0, client_width, client_height); /* NOTE1 */
    HBITMAP dummy_buffer = (HBITMAP)SelectObject(hdc, back_buffer);

    // Create GDI+ stuff on top of HDC
    Gdiplus::Graphics *graphics = Gdiplus::Graphics::FromHDC(hdc);

    QueryPerformanceCounter(...);
    graphics->DrawImage(gdip_bitmap, 0, 0, bitmap_width, bitmap_height);
    /* print performance counter diff */ // -> ~27 ms typically

    delete graphics;

    // Double buffering
    BitBlt(hdc0, 0, 0, client_width, client_height, hdc, 0, 0, SRCCOPY);
    SelectObject(hdc, dummy_buffer);
    DeleteObject(back_buffer);
    DeleteDC(hdc); // This is the temporary double buffer HDC

    EndPaint(hWnd, &ps);

/* NOTE1 */: В исходном коде. NET они не используют CreateCompatibleBitmap, но CreateDIBSection вместо этого. Это повышает производительность с 27 мс до 21 мс и очень громоздко (см. Ниже).


В обоих случаях я звоню Control.Invalidate или InvalidateRect соответственно, когда мышь движется (OnMouseMove, WM_MOUSEMOVE). Цель состоит в том, чтобы реализовать панорамирование с помощью мыши с помощью SetTransform - пока что это не имеет значения, если производительность отрисовки плохая.


ПРИМЕЧАНИЕ2: { ссылка }

Этот ответ предполагает, что использование Gdiplus::CachedBitmap - хитрость. Однако в исходном коде C# WinForms я не могу найти доказательств того, что он каким-либо образом использует кэшированные растровые изображения - код C# использует GdipDrawImageRectI, который сопоставляется с GdipDrawImageRectI, который сопоставляется с Graphics::DrawImage(IN Image* image, IN INT x, IN INT y, IN INT width, IN INT height).


Что касается /* NOTE1 */, вот замена для CreateCompatibleBitmap (просто заменить CreateVeryCompatibleBitmap):

bool bFillBitmapInfo(HDC hdc, BITMAPINFO *pbmi)
{
    HBITMAP hbm = NULL;
    bool bRet = false;

    // Create a dummy bitmap from which we can query color format info about the device surface.
    hbm = CreateCompatibleBitmap(hdc, 1, 1);

    pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);

    // Call first time to fill in BITMAPINFO header.
    GetDIBits(hdc, hbm, 0, 0, NULL, pbmi, DIB_RGB_COLORS);

    if ( pbmi->bmiHeader.biBitCount <= 8 ) {
        // UNSUPPORTED
    } else {
        if ( pbmi->bmiHeader.biCompression == BI_BITFIELDS ) {
            // Call a second time to get the color masks.
            // It's a GetDIBits Win32 "feature".
            GetDIBits(hdc, hbm, 0, pbmi->bmiHeader.biHeight, NULL, pbmi, DIB_RGB_COLORS);
        }
        bRet = true;
    }

    if (hbm != NULL) {
        DeleteObject(hbm);
        hbm = NULL;
    }
    return bRet;
}

HBITMAP CreateVeryCompatibleBitmap(HDC hdc, int width, int height)
{
    BITMAPINFO *pbmi = (BITMAPINFO *)LocalAlloc(LMEM_ZEROINIT, 4096); // Because otherwise I would have to figure out the actual size of the color table at the end; whatever...
    bFillBitmapInfo(hdc, pbmi);
    pbmi->bmiHeader.biWidth = width;
    pbmi->bmiHeader.biHeight = height;
    if (pbmi->bmiHeader.biCompression == BI_RGB) {
            pbmi->bmiHeader.biSizeImage = 0;
    } else {
        if ( pbmi->bmiHeader.biBitCount == 16 )
            pbmi->bmiHeader.biSizeImage = width * height * 2;
        else if ( pbmi->bmiHeader.biBitCount == 32 )
            pbmi->bmiHeader.biSizeImage = width * height * 4;
        else
            pbmi->bmiHeader.biSizeImage = 0;
    }
    pbmi->bmiHeader.biClrUsed = 0;
    pbmi->bmiHeader.biClrImportant = 0;

    void *dummy;
    HBITMAP back_buffer = CreateDIBSection(hdc, pbmi, DIB_RGB_COLORS, &dummy, NULL, 0);
    LocalFree(pbmi);
    return back_buffer;
}

Использование очень совместимого растрового изображения в качестве обратный буфер повышает производительность с 27 мс до 21 мс.

Относительно /* NOTE0 */ в коде C# - код является только быстрым, если матрица преобразования не ' т шкала. Производительность C# немного снижается при увеличении масштаба (~ 9 мс) и значительно снижается (~ 22 мс) при понижении частоты дискретизации.

Это указывает на то, что DrawImage, вероятно, хочет для BitBlt, если это возможно. Но в моем случае C ++ это невозможно, потому что формат Bitmap (который был загружен с диска) отличается от формата заднего буфера или чего-то еще. Если я создам новое более совместимое растровое изображение (на этот раз нет явной разницы между CreateCompatibleBitmap и CreateVeryCompatibleBitmap), а затем нарисую на нем исходное растровое изображение, а затем использую только более совместимое растровое изображение в вызове DrawImage, затем производительность увеличивается примерно до 4,5 мс. Он также обладает такими же характеристиками производительности при масштабировании, что и код C#.

if (better_bitmap == NULL)
{
    HBITMAP tmp_bitmap = CreateVeryCompatibleBitmap(hdc0, gdip_bitmap->GetWidth(), gdip_bitmap->GetHeight());
    HDC copy_hdc = CreateCompatibleDC(hdc0);
    HGDIOBJ old = SelectObject(copy_hdc, tmp_bitmap);
    Gdiplus::Graphics *copy_graphics = Gdiplus::Graphics::FromHDC(copy_hdc);
    copy_graphics->DrawImage(gdip_bitmap, 0, 0, gdip_bitmap->GetWidth(), gdip_bitmap->GetHeight());
    // Now tmp_bitmap contains the image, hopefully in the device's preferred format
    delete copy_graphics;
    SelectObject(copy_hdc, old);
    DeleteDC(copy_hdc);
    better_bitmap = Gdiplus::Bitmap::FromHBITMAP(tmp_bitmap, NULL);
}

НО он все еще неизменно медленнее, должно быть, чего-то еще не хватает. И возникает новый вопрос: почему это не необходимо в C# (то же изображение и тот же компьютер)? Image.FromFile делает не преобразование растрового формата при загрузке, насколько я могу судить.

Почему вызов DrawImage в коде C ++ все еще медленнее, и что мне нужно сделать так быстро, как в C#?

1 Ответ

2 голосов
/ 03 марта 2020

Я закончил тем, что повторил больше. NET кода безумия.

Магический c вызов, который делает его go быстрым - GdipImageForceValidation в System.Drawing.Image.FromFile. Эта функция вообще не документирована и даже [официально] не вызывается из C ++. Здесь просто упоминается: https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-image-flat

Gdiplus::Image::FromFile и GdipLoadImageFromFile фактически не загружают полное изображение в память. Он эффективно копируется с диска каждый раз, когда он рисуется. GdipImageForceValidation заставляет изображение загружаться в память, или, кажется, так ...

Моя первоначальная идея скопировать изображение в более совместимое растровое изображение была на правильном пути, но я так и сделал не дает наилучшей производительности для GDI + (потому что я использовал растровое изображение GDI из оригинального HD C). Загрузка изображения непосредственно в новое растровое изображение GDI +, независимо от формата пикселя, дает те же характеристики производительности, что и в реализации C#:

better_bitmap = new Gdiplus::Bitmap(gdip_bitmap->GetWidth(), gdip_bitmap->GetHeight(), PixelFormat24bppRGB);
Gdiplus::Graphics *graphics = Gdiplus::Graphics::FromImage(better_bitmap);
graphics->DrawImage(gdip_bitmap, 0, 0, gdip_bitmap->GetWidth(), gdip_bitmap->GetHeight());
delete graphics;

Еще лучше, использование PixelFormat32bppPARGB значительно повышает производительность - предварительно умноженное альфа-значение окупается, когда изображение многократно рисуется (независимо от того, имеет ли исходное изображение альфа-канал).

Кажется, что вызов GdipImageForceValidation эффективно делает что-то подобное внутри, хотя я не знаю, что это действительно так. Поскольку Microsoft сделала невозможным вызов API-интерфейса GDI + из пользовательского кода C ++, я просто изменил Gdiplus::Image в моих Windows SDK-заголовках, добавив соответствующий метод. Явное копирование растрового изображения в PARGB кажется мне более чистым (и дает лучшую производительность).

Конечно, после можно узнать, какую недокументированную функцию использовать, Google также даст некоторую дополнительную информацию: https://photosauce.net/blog/post/image-scaling-with-gdi-part-5-push-vs-pull-and-image-validation

GDI + не мой любимый API.

...