首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >GDI+ DrawImage在C++ (Win32)中明显慢于C# (WinForms)

GDI+ DrawImage在C++ (Win32)中明显慢于C# (WinForms)
EN

Stack Overflow用户
提问于 2020-03-01 22:40:57
回答 1查看 1.6K关注 0票数 1

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

映像分别在应用程序启动时加载到System.Drawing.ImageGdiplus::Image中。

C#绘图代码(直接以主要形式)如下:

代码语言:javascript
运行
复制
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++实现了它,然后查看了它在.NET源代码中的位置--我可能忽略了一些东西)。当您在Win32中创建一个新的VS2019项目时,程序的其余部分或多或少就是您所得到的。为了可读性,省略了所有错误处理。

代码语言:javascript
运行
复制
// 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 ms提高到21 ms,而且非常麻烦(见下文)。

在这两种情况下,当鼠标移动时,我分别调用Control.InvalidateInvalidateRect (OnMouseMoveWM_MOUSEMOVE)。我们的目标是使用SetTransform实现鼠标的摇摄--只要绘图性能不好,这一点现在就无关紧要了。

NOTE2:https://stackoverflow.com/a/1617930/653473

这个答案表明,使用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):

代码语言:javascript
运行
复制
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 ms提高到21 ms。

对于/* NOTE0 */代码中的C#,只有在转换矩阵不缩放的情况下,代码才是快速的。C#性能在提升(~9ms)时略有下降,在下采样时显著下降(~22 9ms)。

这暗示:如果可能的话,DrawImage可能想要BitBlt。但在我的C++示例中,它不能这样做,因为Bitmap格式(从磁盘加载)与后台缓冲区格式或其他格式不同。如果我创建一个新的更兼容的位图(这一次CreateCompatibleBitmapCreateVeryCompatibleBitmap之间没有明显的区别),然后将原始位图绘制到这个位图上,然后只在DrawImage调用中使用更兼容的位图,那么性能将提高到大约4.5ms。现在,它也具有与C#代码相同的性能特征。

代码语言:javascript
运行
复制
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#一样快呢?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-03-03 20:54:34

最后,我复制了更多的.NET代码。

让它快速发展的神奇调用是GdipImageForceValidation in System.Drawing.Image.FromFile。这个函数基本上没有文档化,甚至不能从C++正式调用。这里只提到:https://learn.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-image-flat

Gdiplus::Image::FromFileGdipLoadImageFromFile实际上并没有将完整的映像加载到内存中。每次绘制磁盘时,它实际上都会从磁盘中复制。GdipImageForceValidation强制将图像加载到内存中,或者看起来.

我最初将图像复制到一个更兼容的位图中的想法是正确的,但我这样做并没有给GDI+带来最好的性能(因为我使用了来自原始HDC的GDI位图)。直接将图像加载到新的GDI+位图中,无论像素格式如何,都会产生与C#实现相同的性能特征:

代码语言:javascript
运行
复制
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的真正作用是什么,但它在内部似乎有效地做了类似的事情。因为微软使得他们不可能从GDI+用户代码中调用C++平台API,所以我只是在Windows中修改了Gdiplus::Image,以包含一个适当的方法。在我看来,将位图显式地复制到PARGB似乎更干净(并产生更好的性能)。

当然,在找到要使用的无文档功能之后,google也会提供一些附加信息:https://photosauce.net/blog/post/image-scaling-with-gdi-part-5-push-vs-pull-and-image-validation

GDI+不是我最喜欢的API。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/60480461

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档