Pull to refresh

Comments 39

А зачем 15 тысяч FPS, у вас есть монитор способный показывать с такой частой?

Дочитал до конца, но почему сразу не задуматься о частоте в 60гц.
Конечно сразу додумался, однако при крайнем, экстремальном тестировании производительности, когда дело касается графики, то критерий — FPS. Но была причина еще важнее — контроль утечек памяти, что особенно актуально для работы с Win32 и маршаллингом из управляемого кода. Чем больше и чаще рисуем тем быстрее увидим проблему. У меня ночь проработала на 15000 и все ОК, только после этого решился публиковать.
Отлично! Я в своё время до этого не додумался)
Да я тоже неделю убил на разные варианты, пока до самого «дна» GDI32 не опустился)
Думал умные люди из Microsoft за эти годы уже давно всё придумали, но, оказалось, они додумались только до контрола D3DImage у которого хостится контрол с DC, к которому можно привязать DirectX. Честно говоря, я еще думал про старый добрый DirectDraw, с ним я тоже много 2D фокусов проделывал, но его ЕМНИП прекратили еще в DirectX8, да и фротэнд контрола получился бы все-равно не таким красивым.
Написали бы этот конкретно кусочек на управляемом С++\CLI — остальная часть проекта осталась бы на вашем .NET, конкретно этот кусок мощно заюзал бы GDI32 без всяких там корявых импортов, еще бы и быстрее было, скорее всего.
Я думал об этом, но в итоге, все-равно каждый кадр, который надо нарисовать на мониторе стал бы методом или unsafe C++ .Net, или вызовом неуправляемого Win32 API неважно откуда.
Я понимаю, что обидел тех, кто из управляемого С++ вызывает GDI32. Парни, я спецом убивался о простоте и изяществе, чтобы портировать решение в 100 строк кода не было проблемой. 15 минут труда и будет у вас управляемый С++ контрол, работающий даже быстрее моего решения в посте для C#.
Ссылка на CodePlex открыта, исходники есть, ссылка на репозитарий сорцов есть, можно подключиться к проекту и «прокачать» его до С++\CLI. Я на хабре этот пост закоммитил не ради «я пиарюсь», а просто показать неожиданный для многих подход к сложным вещам. И напомнить, что Win32 API еще живее всех живых, и чертовски умопомрачительно эффективен.
Как демо ок. Но сам код не ок, в живой проект бы я его взял только в случае крайней нужды. Где мне потом таких кулхацкеров искать, которые в нем баги править будут? :)
У меня слов нет, дожили, 2013 год — народ в шоке от GDI без плюсов.
Если честно то код и правда хромает.

  • Неправильно реализован IDisposable
  • Приложение не закрывается или падает с эксепшеном при попытке закрыть
  • ((System.Drawing.Graphics)hDCRef.Wrapper).ReleaseHdc()
И еще. Почему Bitmap и Grapics лежат вне контролла (хотя бы RazorPainterWPFCtl)?
Потому-что паттерн и сценарий использования не определен, и я сделал как проще (надеюсь) для понимания. Для моей задачи они будут совсем в другом классе в другой либе рисоваться.
ReleaseHdc() возможно и перекрутил на сырую голову, хотя у себя проблем не заметил…
я сделал как проще (надеюсь) для понимания.
Нет вы только код запутали.
паттерн и сценарий использования не определен
Это решается предоставлением либо абстрактного метода, либо события.
IDisposable реализован вроде правильно — в классе нет управляемых ресурсов, и полный паттерн не нужен. Однако нужно наследование от интерфейса, иначе если кто-то начнет бесконечно лупить в цикле рендера
myRazorPainter = new RazorPainter();
то начинаются memory leaks несмотря на деструктор.
Если укажите в чем именно моя ошибка буду благодарен (и не только я).
Во-первых, в конце Dispose должен быть вызов GC.SuppressFinalize(this) чтобы предотвратить вызов деструктора. Во-вторых вы используете Dispose не по назначению (в методе Realloc). В-третьих вы зачем-то обнуляете _width и _height (видимо это вытекает из «во-вторых»).
Вы меня выкупили! ) Действительно, я тут отнесся к Dispose() как к некоему «Clear and Go again» (управляемых ресурсов в классе нет), и сэкономил несколько строк кода. Но, ОК, критику принял, буду допиливать. (Напишу в личку как появится апдейт)
((System.Drawing.Graphics)hDCRef.Wrapper).ReleaseHdc() -> graphics.ReleaseHdc() (graphics это тот же, что используется строчкой ниже)
И вообще ReleaseHdc и так в graphics.Dispose() делается.
Да-да, дружище, увидел уже этот косяк, сейчас переношу BMP и GFX уровнем ниже в контрол, и прикручиваю реакцию на резайз. Жую мозг как бы это сделать не поломав статью.
Я немного поигрался с кодом: все утащил в WinForms контролл. Из него наружу торчит метод Render и свойство GFX, а WPF контролл просто пробрасывает их дальше.

Там нормально все переносится, и на статью особо не влияет. Заодно получается WinForms контролл, который тут в комментариях просили.
Мысли сходятся. Пока зачекинил исправления с Dispose в исходниках на CodePlex.
Можно и на феррари картошку с дачи возить, кто мешает-то.
Прошу прощения, вы о чем? Выше вы сказали, что код автора в продакшен бы не взяли, не будем касаться подхода и оформления, судя по всему, речь шла об использовании «низкоуровневого» (lol) GDI в принципе. Правильно понял?
А что вам так смешно тут? Да, тот же WPF предлагает высокоуровневые интерфейсы для работы с графикой, которые знает любой, работающий с WPF. Никакого внятного обоснования, зачем нужно это менять, кроме 15 тысяч кадров (зачем?) и совместимости с устаревшими платформами (зачем??). Гос. органы — это не аргумент. Выгоднее переставать поддерживать устаревающие платформы, чем делать хаки на все случаи жизни. Простота и стандартные методы всегда > скорости. Особенно там, где скорость не нужна.
К сожалению некоторым областям еще далеко до этих прописных истин. Когда в них работаешь, то формула Технология=(текущее поколение — 1) это еще из разряда «тебе повезло, братан». Приходится что-то делать, облизываясь на то, что сейчас для этого есть…
Отчасти согласен с вами, только как раз и смешно, что GDI не является низкоуровневым интерфейсом, его использование где угодно — вполне в рамках. Он совместим вообще со всеми платформами Microsoft Windows, начиная с Win98 (если не раньше) и заканчивая Win8 — даже его API не менялся. Это вполне себе стандарт.

Если говорить про .Net, то, да, конечно, есть куча более высокоуровневых возможностей для рисования и вывода (GDI+). Только вот выполнить ими на должном уровне производительности визуализацию нескольких тысяч полигонов (софтварно), к примеру, не получится. И тут решение автора (слова про 15к фпс — это какой-то полумаркетинговый бред) вполне себе ничего, ибо вам придется либо выносить код в native и дергать его оттуда, либо юзать GDI из-под managed кода, что автор и проделал.
Не ругайте за занудство с подробностями, но ЕМНИП, WIn32 API и GDI появились еще в Windows NT4.0. К чести Microsoft он так и не менялся (ради обратной совместимости), лишь добавлялись новые методы. А если старые «прокачивались» функционалом, то оставался старый как был, а к «прокаченому» варианту метода добавлялось окончание «Ex» (extended), напр. CreateWindowEx().
Хардкорные трюки ч.2 как раз будет этому посвящена, и там тоже будут печеньки для дотнетчиков.
.
P.S. Что касается выпиленых API в WP8 и WinRT. Камрады, не будьте такими наивными. Там все-еще старый добрый NT4, который компилился со своим Win32 API под кучу процов еще многие лета назад. Вспомнили, пересобрали на ARM и нахлобучили сверху WPF. «WinRT doesn't completely replace Win32 API but internally can call and use Win32 API and subsets of .Net.»
Вот только использовать Win32 теперь запрещено волевым усилием и политикой компании, и модерацию в магазине приложений не пройдете. Отчасти напоминает политику Apple по отношению к API. Огромная просьба, не реагировать и не начинать холивар на эту тему. Статья выше совсем о другом.
Кстати, еще момент. В том же WP8 пространство System.Drawing более не поддерживается. Вот вам и совместимость.
Уважаемый, в статье была все-таки отсылка на WriteableBitmap с его сферой применения. Но, давайте честно скажем, что и на андроиде мой подход тоже не будет работать. Ок, я принимаю эту критику.
Вероятно, вы просто далеки от задач, в которых нужно именно то, о чем в статье написано, и если вы с такими задачами не сталкивались, и не понимаете о чем речь, значит вам в какой-то степени повезло, и эта статья не для вас. Просто поставьте в закладки (на всякий будущий случай и сюрпризы жизни кодера) и не забивайте пока себе голову.
Спасибо, как раз есть похожее приложение на WinForms, где это можно заюзать :)
Голосуешь за готовый WinForms контрол на CodePlex? Не знаю есть ли смысл, фронтэнд для такого контрола прост, его смастырить несложно.
Но, пожалуй, завтра таки добавлю WF вариант контрола, хотя он на порядок проще WPF-варианта, но многим пригодится как стартовая точка.
Эх, а я в свое время радовался полученному 123 fps на 1280x1024… (полностью софтверный рендер)
Лет через будем 5 вместе с теплотой вспоминать 15K FPS, смастырив на новых проце, шине и памяти контрол на 1M FPS )
Зачем в методе Paint выделять память для _pArray, потом в него копировать данные из bitmap? Почему бы не передать сразу в функцию SetDIBitsToDevice поле Scan0 класса BitmapData? Т.е. вместо

...
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);
...


сделать

...
BitmapData BD = bitmap.LockBits(
  new Rectangle(0, 0, bitmap.Width, bitmap.Height), 
  ImageLockMode.ReadOnly, 
  PixelFormat.Format32bppArgb);

SetDIBitsToDevice(hRef, 0, 0, _width, _height, 0, 0, 0, _height, BD.Scan0, ref _BI, 0);

bitmap.UnlockBits(BD);
...


Тем самым из кода уйдут две переменные (_pArray и _gcHandle), а также FPS чуток подрастет.
Если вы задаетесь и заморачиваетесь такими вопросами (правильными), то вы станете отличным .net-кодером, и MVP вас не минует.
Я отвечу попроще, иронично метафорично, не пинайте (подробнее и точнее лучше конечно MSDN читать). У нас, если грубо выражаться, есть два менеджера памяти — виндовый, и .net-нетовский CLR. И они между собой не совсем дружат. Каждый из них время от времени дефрагментирует свой кусок памяти, очищает мусор, перекидывает что-то в своп из физической памяти, но каждый как-бы сам по себе. Когда мы должны обменяться блоком памяти между управляемой и неуправляемой архитектурой, то стоит сделать Lock и GCHandleType.Pinned — это типа как прибить гвоздем к холодильнику записку «этот кусок памяти никому не трогать и не перемещать». Иначе, пока физической памяти много, 100 раз, даже 1000 раз всё будет ОК, а на 1001-й раз винда решит что-то свопнуть, про что не в курсе, и по нужному адресу вместо блока памяти нужного битмапа окажется кусок порнофильма, что я вчера скачал, и мне придется краснеть.
Судя по всему (исходники и описание на MSDN) использовать Scan0 как предложил Fen1xL безопасно — похоже, что LockBits уже пинит память.
Если вопрос о самом смысле существования _pArray — он для предварительного композинга из нескольких битмапов перед отправкой на DC. Тут это пока не используется.
И, честно говоря, не знаю, стоит ли использовать, потому-что во фронтэнде есть честные BMP и GFX где мы можем это делать, а тут будут заморочки с терминами «stride» и «line byte align» и «alpha compose type», что только усложнит. Поэтому да, в простом варианте в текущей интерации можно выкинуть _pArray, но я бы оставил ради будущей возможной гибкости, а одно низкоуровневое блочное копирование памяти это чертовски быстрая штука и на FPS не роялит. И еще интереснее, что можно сделать цикл рендера синхронизирвоанный с VSync монитора тут, с _pArray, не ожидая перерисовок BMP от юзера.
Никакой кусок порнофильма из другого процесса/файлового кеша оказаться по этому адресу не может (не даст изоляция процессов друг от друга). CLR ничего в физической памяти не перемещает и в своп не скидывает.

А вот кусок других данных из нашего же процесса вполне может — т.к. виндовый слой ничего не знает о том, что CLR может переместить объект в виртуальной памяти, поменяв его виртуальный адрес.
Sign up to leave a comment.

Articles