Глибоке занурення в систему візуалізації WPF

На переклад цієї статті мене підштовхнуло обговорення записів «Чому WPF живіший за всіх живих?», «Сім років WPF: що змінилося?» Вихідна стаття написана в 2011 році, коли Silverlight ще був живий, але інформація з WPF не втратила актуальності.

Спочатку я не хотів публікувати цю статтю. Мені здавалося, що це неввічливо — про мертвих треба говорити або добре, або нічого. Але кілька бесід з людьми, чия думка я дуже ціную, змусили мене передумати. Вклали багато зусиль в платформу Microsoft розробники повинні знати про внутрішні особливості її роботи, щоб, зайшовши в глухий кут, вони могли розуміти причини події і більш точно формулювати побажання до розробників платформи. Я вважаю WPF і Silverlight хорошими технологіями, але… Якщо ви стежили за моїм Twitter останні кілька місяців, то деякі висловлювання могли здатися вам безпідставними нападками на продуктивність WPF і Silverlight. Чому я це писав? Адже, врешті-решт, я вклав тисячі і тисячі годин мого власного часу протягом багатьох років, пропагуючи платформу, розробляючи бібліотеки, допомагаючи учасникам спільноти і так далі. Я однозначно особисто зацікавлений. Я хочу, щоб платформа стала краще.



Продуктивність, продуктивність, продуктивність

При розробці, затягує, орієнтованого на споживача, користувальницького інтерфейсу, продуктивність для вас важливіше всього. Без неї все інше не має сенсу. Скільки разів вам доводилося спрощувати інтерфейс, тому що він лагал? Скільки разів ви придумували «нову, революційну модель користувацького інтерфейсу», яку доводилося викинути на смітник, оскільки наявна технологія не дозволяла її реалізувати? Скільки разів ви говорили клієнтам, що для повноцінної роботи потрібен чотирьохядерний процесор з частотою 2,4 ГГц? Клієнти неодноразово запитували мене, чому на WPF і Sliverlight вони не можуть отримати такий же плавний інтерфейс, як в додатку для iPad, навіть маючи вчетверо більше потужний PC. Ці технології може і підходять для бізнес-додатків, але вони явно не підходять для користувача додатків наступного покоління.

Але ж WPF використовує апаратне прискорення. Чому ж ви вважаєте його неефективним?

WPF дійсно використовує апаратне прискорення і деякі моменти його внутрішньої реалізації зроблені дуже непогано. На жаль, ефективність використання GPU набагато нижче, ніж могла б бути. Система візуалізації WPF використовує дуже грубу силу. Я сподіваюсь пояснити це твердження трохи нижче.

Аналізуємо одиничний прохід візуалізації WPF

Для аналізу продуктивності нам треба зрозуміти, що насправді відбувається всередині WPF. Для цього я використовував PIX, профайлер Direct3D, що поставляється разом з DirectX SDK. PIX запускає вашу D3D-додаток і впроваджує ряд перехоплювачів у всі виклики Direct3D для їхнього аналізу і моніторингу.

Я створив просте WPF-додаток, в якому зліва направо виводяться два еліпса. Обидва еліпса одного кольору (#55F4F4F5) з чорним контуром.



І як WPF рендерить це?

Насамперед WPF очищає (#ff000000) брудну область, яку він збирається перемалювати. Брудні області потрібні для скорочення числа пікселів, що посилаються на фінальну стадію злиття (output merger stage) в конвеєрі GPU. Ми навіть може припустити, що це скорочує обсяг геометрії, яку доведеться перетесселировать (be re-tessellated), докладніше про це трохи пізніше. Після очищення брудної області наш кадр виглядає ось так



Після цього WPF робить щось незрозуміле. Спочатку він заповнює верховий буфер (vertex buffer), після чого малює щось, що виглядає як прямокутник поверх брудної області. Тепер кадр виглядає ось так (захоплююче, чи не правда?):



Після цього він тесселирует еліпс на GPU. Тесселяція, як ви можливо вже знаєте — це перетворення геометрії нашого 100х100 еліпса в набір трикутників. Робиться це з наступних причин: 1) трикутники є природною одиницею рендеринга для GPU 2) тесселяція еліпса може вылится всього в декілька сотень трикутників, що набагато швидше растеризації 10 000 пікселів з антиалиасинг засобами CPU (що робить Silverlight). На скріншоті нижче видно, як виглядає тесселяція. Знайомі з 3D графікою читачі могли помітити, що це трикутні смуги (triangle strip). Зверніть увагу, що в тесселяції еліпс виглядає незавершеним. В якості наступного кроку WPF бере тесселяцію, завантажує її в верховий буфер GPU і робить ще один виклик відтворення (draw call) з використанням піксельного шейдера, який налаштований для використання «кисті» налаштованої в XAML.



Пам'ятайте, що я зазначив незавершеність еліпса? Це дійсно так. WPF генерує те, що програмісти Direct3D знають як «набір ліній» (line list). GPU розуміє лінії так само добре, як і трикутники. WPF заповнює верховий буфер цими лініями і вгадайте що? Правильно, виконує ще один виклик відтворення (draw call)? Набір ліній виглядає ось так:



Тепер-то WPF закінчив малювати еліпс, чи не так? Ні! Ви забули про контурі! Контур теж є набором ліній. Його теж посилають в верховий буфер і виконують ще один виклик відтворення. Контур виглядає ось так



До цього моменту ми намалювали один еліпс, так що наш кадр виглядає ось так:



Всю процедуру необхідно повторити для кожного еліпса на сцені. У нашому випадку два рази.

Я не зрозумів. Чому це погано для продуктивності?

Перше, що ви могли помітити — для візуалізації одного еліпса нам потрібні були три виклику відтворення і два звернення до вершинних буферу. Щоб пояснити неефективність цього підходу мені доведеться трохи розповісти про роботу GPU. Для початку, сучасні GPU працюють ДУЖЕ ШВИДКО і асинхронно з GPU. Але при деяких операціях відбуваються дорогі перемикання з режиму в режим ядра (user-mode to kernel mode transitions). При заповненні вершинного буфера він повинен бути заблокований. Якщо буфер в цей момент використовується GPU, це змушує GPU синхронізуватися з CPU і різко знижує продуктивність. Верховий буфер створюється з D3DUSAGE_WRITEONLY | D3DUSAGE_DYNAMIC, але коли він блокується (що трапляється нерідко), D3DLOCK_DISCARD не використовується. Це може викликати втрату швидкості (синхронізацію GPU і CPU) у GPU, якщо буфер вже використовується GPU. У разі великої кількості викликів відтворення у нас є висока ймовірність отримати безліч переходів в режим ядра і велике навантаження в драйверах. Для підвищення продуктивності нам треба послати на GPU настільки багато, наскільки можливо, інакше ваш CPU буде зайнятий, а GPU буде простоювати. Не забувайте, що в цьому прикладі мова йшла тільки про одному кадрі. Типовий інтерфейс на WPF намагається виводити 60 кадрів в секунду! Якщо ви коли-небудь намагалися з'ясувати, чому ваш потік рендеринга так сильно завантажує процесор, то швидше за все виявляли, що велика частина навантаження йде від вашого драйвера GPU.

А що з кешованих побудовою (Cached Composition)? Адже воно підвищує продуктивність!

Поза всяких сумнівів, підвищує. Кешований побудова або BitmapCache кешує об'єкти текстури GPU. Це означає, що вашому CPU не треба перетесселировать, а GPU не треба перерастрировать (re-rasterize). При виконанні одного проходу візуалізації WPF просто використовує текстуру з відеопам'яті, збільшуючи продуктивність. Ось BitmapCache еліпса:



Але у WPF є темні сторони і в цьому випадку. Для кожного BitmapCache він виконує окремий виклик відтворення. Не буду брехати, іноді вам дійсно треба виконувати виклик відтворення для візуалізації одиничного об'єкта (visual). Всяке буває. Але давайте уявимо собі сценарій, в якому у нас є <Canvas/> з 300 анімованими BitmapCached-эллипсами. Просунута система зрозуміє, що їй треба отрендерить 300 текстур і всі вони z-впорядковані (z-ordered) одна за одною. Після цього вона збере їх пакети максимального розміру, наскільки я пам'ятаю, DX9 може приймати до 16 вхідних елементів (sampler inputs) за раз. В цьому випадку ми отримаємо 16 викликів відтворення замість 300, що помітно знизить навантаження на CPU. У термінах 60 кадрів в секунду ми зменшимо навантаження з 18 000 викликів відтворення в секунду до 1125. В Direct 3D 10 кількість вхідних елементів набагато вище.

Ладно, я дочитав до цього місця. Розкажіть мені, як WPF використовує піксельні шейдери!

У WPF є розширюваний API піксельних шейдерів і деякі вбудовані ефекти. Це дозволяє розробникам додавати по-справжньому унікальні ефекти в їх користувальницький інтерфейс. При примернии шейдера до існуючої текстурі в Direct 3D зазвичай використовується проміжна мета відтворення (intermediate rendertarget)… і врешті-решт ви не можете використовувати як зразок (sample from) текстуру, в яку пишете! WPF теж робить це, але, до нещастя, він створює повністю нову текстуру КОЖЕН КАДР і знищує її. Створення і знищення ресурсів GPU — це одна із самих повільних речей, які тільки можна робити при обробці кожного кадру. Я зазвичай не роблю так навіть з виділенням системної пам'яті схожого обсягу. При повторному використанні цих проміжних поверхонь можна було б досягти дуже значного підвищення продуктивності. Якщо ви коли-небудь задавалися питанням, чому ваші апаратно-прискорені шейдери створюють помітну навантаження на CPU, то тепер знаєте відповідь.

Але може бути саме так і треба рендери векторну графіку на GPU?

Microsoft доклала чимало зусиль для виправлення цих проблем, до нещастя це було зроблено не в WPF, а в Direct 2D. Подивіться на цю групу з 9 еліпсів, відрендерених Direct2D:



Пам'ятаєте, як багато викликів відтворення вимагалося WPF для візуалізації одного еліпса з контуром? А блокувань вершинного буфера? Direct2D робить це за ОДИН виклик відтворення. Тесселяція виглядає ось так



Direct 2D намагається намалювати якомога більше за один раз, максимізуючи використання GPU і мінімізуючи завантаження CPU. Прочитайте Insights: Direct2D Rendering наприкінці ось цієї сторінки, там Марк Лавренс (Mark Lawrence) пояснює багато внутрішні деталі роботи Direct 2D. Ви можете помітити, що незважаючи на всю швидкість Direct 2D є ще більше областей, де вона буде покращено у другій версії. Цілком можливо, що версія 2 Direct 2D буде використовувати апаратне прискорення тесселяції DX11.

Дивлячись на API Direct 2D цілком можна припустити, що значна частина коду була взята з WPF. Подивіться це старе відео про Avalon, в ньому Майкл Воллент (Michael Wallent) розповідає про розробку заміни GDI на основі цієї технології. У нього дуже схожий геометричний API і термінологія. Всередині він схожий, але дуже оптимізований і сучасний.

А що з Silverlight?

Я міг би зайнятися Silverlight, але це буде зайвим. Продуктивність рендеринга в Silverlight теж низька, але причини інші. Він використовує для відтворення CPU (навіть для шейдерів, наскільки я пам'ятаю, вони частково написані на асемблері), але CPU як мінімум в 10-30 разів повільніше GPU. Це залишаємо вам набагато менше процесорної потужності для візуалізації інтерфейсу і ще менше для логіки застосунку. Його апаратне прискорення дуже слабко розвинене і майже в точності повторює кешований побудова WPF і веде себе аналогічним чином, здійснюючи виклик відтворення для кожного об'єкта з BitmapCache (BitmapCached visual).

І що ж нам тепер робити?

Це питання дуже часто задають мені клієнти, які зіткнулися з проблемами WPF і Silverlight. На жаль, у мене немає однозначної відповіді. Ті хто можуть, роблять власні фреймворки, заточені під свої специфічні потреби. Іншим доводиться змиритися, так як альтернатив WPF і SL в їх нішах немає. Якщо мої клієнти просто розробляють бізнес-додатки, то у них не так багато проблем з швидкістю і вони просто насолоджуються продуктивністю роботи програмістів. Справжні проблеми у тих, хто хоче будувати дійсно цікаві інтерфейси (тобто програми для споживачів або кіосків (consumer apps or kiosk apps)).

Вже після початку перекладу з'явилися новини про заплановану оптимізацію продуктивності і використання DX10-11 в WPF 4.6. Будуть вирішені описані в статті проблеми з новин не зовсім зрозуміло.


Вихідна стаття: A Critical Deep Dive into the WPF Rendering System

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.