Вводная: приложение типа осциллографа. Ссылка на готовый проект с фронтэндом в конце статьи.
Как же быстро рисовать его на экран? WriteableBitmap хорош, быстр, и он лучшее решение для WP, WinRT, WPF. Но занудного старпёра-кодера также волнует WinForms, .Net 2.0, 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/
Добавить комментарий