WPF, WinForms: рисуем Bitmap c >15000 FPS. Хардкорные трюки ч.1

в 11:29, , рубрики: .net, bitmap, fast 2D, graphics, WinForms, wpf, высокая производительность, хардкор, метки: , , , , , ,

Сразу уточнение: Bitmap 200x100 на компе с быстрой памятью и 3930K на 1366. Но, это System.Drawing.Bitmap.
Вводная: приложение типа осциллографа. Ссылка на готовый проект с фронтэндом в конце статьи.
Как же быстро рисовать его на экран? WriteableBitmap хорош, быстр, и он лучшее решение для WP, WinRT, WPF. Но занудного старпёра-кодера также волнует WinForms, .Net 2.0, Win2K (да-да, в некоторых гос.органах до сих пор теплый ламповый Win2K).
Далее, я обратил внимание на DirectX, тем более у нас для WPF появился полезный контрол D3DImage. Я перепробовал много движков, но ни один из них не давал удобного изящного способа рисовать GDI+ Bitmap из памяти. Некоторые работали и вовсе только с DX10-11. Ближе всех к цели оказался SlimDX. В любом случае, фронтэнд для контрола оказывался некрасивым. Все эти движки… мягко говоря избыточны, для моей простой задачи.

И, к моему удовольствию оно получилось достаточно простым и универсальным, именно как надо, будет работать даже на Win2K и .Net 2.0.
Когда я был молодым, и у меня кажется еще был 5-ти дюймовый дисковод, я пользовался BitBlt и SetDIBitsToDevice. Потом, с переходом на .Net я все еще пользовался ими и Win32 GDI BITMAP, поскольку пользовался старыми наработками, потом всё забылось. Но вдруг, сейчас мне понадобился нестандартный контрол с попиксельной графикой, да плюс с быстрой отрисовкой. Вот так я и попал в небольшой тупик.
GDI+ Bitmap чертовски удобен со своими градиентами, антиалиасингом, и альфой. Очень вкусные картинки получаются. Нетрудно подготавливать нужный Bitmap в памяти, и даже делать это быстро, если кешировать большую часть изображения, но быстро их отрисовывать очевидного способа нет.

Пришлось вспоминать не очевидный:

[DllImport("gdi32")]
extern static int SetDIBitsToDevice(HandleRef hDC, int xDest, int yDest, int dwWidth, int dwHeight, int XSrc, int YSrc, int uStartScan, int cScanLines, ref int lpvBits, ref BITMAPINFO lpbmi, uint fuColorUse);

И ключевой метод в итоге получился таким:

public void Paint(HandleRef hRef, Bitmap bitmap)
{
	if (bitmap.Width != _width || bitmap.Height != _height)
		Realloc(bitmap.Width, bitmap.Height);
	//_gcHandle = GCHandle.Alloc(pArray, GCHandleType.Pinned);
	BitmapData BD = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), 
									ImageLockMode.ReadOnly, 
									PixelFormat.Format32bppArgb);
	Marshal.Copy(BD.Scan0, _pArray, 0, _width * _height);
	SetDIBitsToDevice(hRef, 0, 0, _width, _height, 0, 0, 0, _height, ref _pArray[0], ref _BI, 0);
	bitmap.UnlockBits(BD);
	//if (gcHandle.IsAllocated)
	//	_gcHandle.Free();
}

По поводу закомменченых строк. Вообще, они должны быть раскомментированы, чтобы облегчить жизнь GC, но ради хардкорных FPS, если размер _pArray не менялся, GCHandle у меня пинится один раз в Realloc(). Хотя… когда их у нас 15000, плюс-минус пара сотен FPS роли уже не играют. Если раскомментить в Paint() не забудьте закомментить в Realloc().
Вот так, ценой всего 100 строк кода (полностью код в прилагаемом проекте ниже) мы решили проблему FPS, и никаких монструозных движков. Возможен гнев евангелистов Microsoft «Так делать нельзя, это против принятых практик программирования», но что поделать.
Весь фронтэнд для нужного контрола изящно сводится к нескольким строкам:

RazorPainter RP = new RazorPainter();
graphics = Control1.CreateGraphics();
hDCRef = new HandleRef(graphics, graphics.GetHdc());

public void Render()
{
    RP.Paint(hDCRef, BMP);
}

((System.Drawing.Graphics)hDCRef.Wrapper).ReleaseHdc();
RP.Dispose();
graphics.Dispose();

А теперь печеньки! Дело в том, что с таким подходом мы абсолютно равнодушны к UI Thread и Invoke его. Ради рекордных FPS смело создаем отдельный полноценный Thread и в нем жестоко:

renderthread = new Thread(() =>
{
	while (true)
		Render();
});
renderthread.Start();

Есть еще небольшая деталь. Поскольку ОС не в курсе нашего хулиганства с памятью, то в оконной WndProc она упрямо затирает наш контрол Background Color. Избавим ОС от лишний мучений (и немножко повысим FPS) таким образом:

public RazorBackend()
{
	InitializeComponent();

	SetStyle(ControlStyles.DoubleBuffer, false);
	SetStyle(ControlStyles.UserPaint, true);
	SetStyle(ControlStyles.AllPaintingInWmPaint, true);
	SetStyle(ControlStyles.Opaque, true);
}

Вообще, конечно FPS у всех будет разный, и дело совсем не в видеокарте — тут роялят частота шины, процессора, памяти, их пропускная способность.

Итак, я добился 15600 FPS, приложение занимает ~30Мб памяти, а вот утилизация процессора 8% меня совсем не порадовала. Мягко выражаясь, это много для 3930K. И тут в моей голове «возвопил» видеодрайвер: «Хозяин, у меня кажется эпилепсия!», и монитор: «А я вообще только 60Hz умею!». Разумеется нам такой FPS не нужен, и правильный цикл рендеринга будет что-то вроде этого:

rendertimer = new DispatcherTimer();
rendertimer.Interval = TimeSpan.FromMilliseconds(15); /* ~60 FPS on my PC */
rendertimer.Tick += (o, args) => Render();
rendertimer.Start();

Ну или по-другому, на ваш вкус. Утилизация процессора в районе нуля.
Далее WPF. Всё сложно и просто одновременно. «Контролы» WPF собственно контролами не являются (а иначе бы мы не могли их крутить и плющить), и у них нет DC. Все решается хостингом WindowsForms контрола в WPF при помощи WindowsFormsHost. В прилагаемом проекте именно WPF пример использования, но легко переделывается в WindowsForms, благо фронтэнд прост как сапог.
Цикл рендеринга Bitmap в демо-проекте состоит всего из одной строчки:

GFX.Clear((drawred = !drawred) ? System.Drawing.Color.Red : System.Drawing.Color.Blue);

Разумеется FPS цикла рендеринга по большей части зависит от сложности рисуемого Bitmap, простая очистка в демо-проекте — это дико быстрая блочная операция.

Пользуйтесь на здоровье, если понимаете что делаете и зачем. Исходники и билд, выложил на CodePlex:
http://razorgdipainter.codeplex.com/
(Заранее прошу прощения за несерьезное оформление проекта на CodePlex, если интересно, дооформлю до нормального OpenSource)

Автор: Lincoln6Echo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js