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

от автора

Сразу уточнение: Bitmap 200×100 на компе с быстрой памятью и 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)

ссылка на оригинал статьи http://habrahabr.ru/post/164705/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *