[GDI + DrawImage在C ++(Win32)中比在C#(WinForms)中明显慢

问题描述 投票:0回答:1

[我正在将应用程序从C#(WinForms)移植到C ++,并注意到在C ++中,即使使用相同的API,使用GDI +绘制图像也要慢得多。

[图像在应用程序启动时分别加载到System.Drawing.ImageGdiplus::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)为双缓冲绘图分配临时的HDCGraphics


C ++绘图代码更加肿。我浏览了System.Windows.Forms.Control的参考源,以了解其如何处理HDC以及如何实现双重缓冲。

据我所知,我的实现与之紧密匹配(请参阅NOTE1)(请注意,我首先是在C ++中实现的,然后then

考察了它在.NET源代码中的作用-我可能忽略了这些内容) 。程序的其余部分或多或少是您在VS2019中创建新的Win32项目时所得到的。为了便于阅读,省略了所有错误处理。
// 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.InvalidateInvalidateRect)时,我分别调用OnMouseMoveWM_MOUSEMOVE。目的是使用SetTransform用鼠标来实现平移-只要绘制性能不好,目前就无关紧要。


注意2:https://stackoverflow.com/a/1617930/653473

此答案表明使用Gdiplus::CachedBitmap是诀窍。但是,在C#WinForms源代码中找不到任何证据表明它以任何方式使用了缓存的位图-C#代码使用GdipDrawImageRectI映射到GdipDrawImageRectI,而C0映射到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毫秒。

关于C#代码中的/* NOTE0 */-如果变换矩阵

缩放,则该代码很快。向上扩展(〜9ms)时,C#性能略有下降;而向下采样时,C#性能则显着下降(〜22ms)。

这暗示:如果可能,DrawImage可能想要

到BitBlt。但是在我的C ++情况下却不能,因为Bitmap格式(从磁盘加载)与后台缓冲区格式有所不同。如果我创建一个新的[[morecompatible位图(这次CreateCompatibleBitmapCreateVeryCompatibleBitmap之间没有明显的区别),然后在其上绘制原始位图,然后仅在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#中(同一图像和同一台机器)需要<< not >>?据我所知,Image.FromFile不会在加载时转换位图格式。
为什么C ++代码中的DrawImage调用仍然较慢,我需要怎么做才能使其与C#一样快?

我正在将应用程序从C#(WinForms)移植到C ++,并注意到在C ++中使用GDI +绘制图像要慢得多,即使它使用相同的API。图像在应用程序启动时加载...

我最终复制了更多的.NET代码疯狂。

使其快速运行的魔术呼叫是GdipImageForceValidation中的System.Drawing.Image.FromFile。基本上根本没有记录此函数,甚至不能从C ++正式调用。这里仅提及:https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-image-flat
[Gdiplus::Image::FromFileGdipLoadImageFromFile]实际上并未将完整图像加载到内存中。每次绘制时都会有效地从磁盘复制它。 GdipImageForceValidation强制将图像加载到内存中,否则似乎...
我将图像复制到更兼容的位图的最初想法是正确的,但是我的方式并不能为GDI +带来最佳性能(因为我使用了原始HDC的GDI位图)。将图像直接加载到新的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进一步提高了性能-当重复绘制图像时,预乘alpha会得到回报(无论源图像是否具有alpha通道)。

似乎调用GdipImageForceValidation内部有效地执行了类似的操作,尽管我不知道它的真正作用。由于Microsoft尽可能使他们无法从C ++用户代码中调用GDI +扁平API,因此我刚刚在Windows SDK标头中修改了Gdiplus::Image以包含适当的方法。将位图显式复制到PARGB对我来说似乎更干净(并产生更好的性能)。

当然,

之后会找到要使用的未记录功能,谷歌还会提供一些附加信息:https://photosauce.net/blog/post/image-scaling-with-gdi-part-5-push-vs-pull-and-image-validation

GDI +不是我最喜欢的API。

c# c++ windows gdi+ gdi
1个回答
0
投票
我将图像复制到更兼容的位图的最初想法是正确的,但是我的方式并不能为GDI +带来最佳性能(因为我使用了原始HDC的GDI位图)。将图像直接加载到新的GDI +位图中,无论像素格式如何,都会产生与C#实现相同的性能特征:
© www.soinside.com 2019 - 2024. All rights reserved.