Я портирую приложение с 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#?