GDI+ DrawImage 在 C++ (Win32) 中的速度明显低于在 C# (WinForms) 中的速度
GDI+ DrawImage notably slower in C++ (Win32) than in C# (WinForms)
我正在将一个应用程序从 C# (WinForms) 移植到 C++ 并注意到在 C++ 中使用 GDI+ 绘制图像要慢得多,即使它使用相同的 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的参考源,看看它是如何处理HDC的,它是如何实现双缓冲的。
据我所知,我的实现与它非常匹配(请参阅注 1)(请注意,我首先在 C++ 中实现了它,然后 然后 研究了它在 .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 毫秒并且非常麻烦(见下文)。
在这两种情况下,当鼠标移动时(OnMouseMove
、WM_MOUSEMOVE
),我分别调用Control.Invalidate
或InvalidateRect
。目标是使用 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 毫秒。
关于 C# 代码中的 /* NOTE0 */
-- 如果变换矩阵 不 缩放,代码 仅 快. C# 性能在放大时略有下降 (~9ms),在缩小采样时显着下降 (~22ms)。
这暗示:DrawImage
可能想要 BitBlt 如果可能的话。但它不能在我的 C++ 案例中,因为 Bitmap
格式(从磁盘加载)与后台缓冲区格式或其他格式不同。
如果我创建一个新的 more compatible 位图(这次 CreateCompatibleBitmap
和 CreateVeryCompatibleBitmap
之间没有明显区别),然后将原始位图绘制到上面,然后只在 DrawImage
调用中使用 more compatible 位图,然后性能增加到大约 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
不会 在加载时转换位图格式。
为什么 C++ 代码中的 DrawImage
调用仍然较慢,我需要做什么才能使其与 C# 中的调用一样快?
我最终复制了更多疯狂的 .NET 代码。
System.Drawing.Image.FromFile
中的 GdipImageForceValidation
使它变快的魔法调用。这个函数基本上根本没有记录,甚至不能从 C++ [官方] 调用它。这里只是提到:https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-image-flat
Gdiplus::Image::FromFile
和 GdipLoadImageFromFile
实际上并没有将完整图像加载到内存中。每次绘制时,它都会有效地从磁盘中复制。 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 headers 中修改 Gdiplus::Image
以包含适当的方法.将位图显式复制到 PARRGB 对我来说似乎更干净(并产生更好的性能)。
当然,在找到使用哪个未记录的函数后,google也会提供一些额外的信息:https://photosauce.net/blog/post/image-scaling-with-gdi-part-5-push-vs-pull-and-image-validation
GDI+ 不是我的最爱API。
我正在将一个应用程序从 C# (WinForms) 移植到 C++ 并注意到在 C++ 中使用 GDI+ 绘制图像要慢得多,即使它使用相同的 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的参考源,看看它是如何处理HDC的,它是如何实现双缓冲的。
据我所知,我的实现与它非常匹配(请参阅注 1)(请注意,我首先在 C++ 中实现了它,然后 然后 研究了它在 .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 毫秒并且非常麻烦(见下文)。
在这两种情况下,当鼠标移动时(OnMouseMove
、WM_MOUSEMOVE
),我分别调用Control.Invalidate
或InvalidateRect
。目标是使用 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 毫秒。
关于 C# 代码中的 /* NOTE0 */
-- 如果变换矩阵 不 缩放,代码 仅 快. C# 性能在放大时略有下降 (~9ms),在缩小采样时显着下降 (~22ms)。
这暗示:DrawImage
可能想要 BitBlt 如果可能的话。但它不能在我的 C++ 案例中,因为 Bitmap
格式(从磁盘加载)与后台缓冲区格式或其他格式不同。
如果我创建一个新的 more compatible 位图(这次 CreateCompatibleBitmap
和 CreateVeryCompatibleBitmap
之间没有明显区别),然后将原始位图绘制到上面,然后只在 DrawImage
调用中使用 more compatible 位图,然后性能增加到大约 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
不会 在加载时转换位图格式。
为什么 C++ 代码中的 DrawImage
调用仍然较慢,我需要做什么才能使其与 C# 中的调用一样快?
我最终复制了更多疯狂的 .NET 代码。
System.Drawing.Image.FromFile
中的 GdipImageForceValidation
使它变快的魔法调用。这个函数基本上根本没有记录,甚至不能从 C++ [官方] 调用它。这里只是提到:https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-image-flat
Gdiplus::Image::FromFile
和 GdipLoadImageFromFile
实际上并没有将完整图像加载到内存中。每次绘制时,它都会有效地从磁盘中复制。 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 headers 中修改 Gdiplus::Image
以包含适当的方法.将位图显式复制到 PARRGB 对我来说似乎更干净(并产生更好的性能)。
当然,在找到使用哪个未记录的函数后,google也会提供一些额外的信息:https://photosauce.net/blog/post/image-scaling-with-gdi-part-5-push-vs-pull-and-image-validation
GDI+ 不是我的最爱API。