Глибоке занурення у систему рендерингу 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)? Адже воно підвищує продуктивність!

глибоке

Але WPF має темні сторони і в цьому випадку. Для кожного BitmapCache він виконує окремий виклик малювання. Не брешу, іноді вам дійсно треба виконувати виклик малювання для рендерингу одиничного об'єкта(Visual). Всяке буває. Але давайте уявимо сценарій, в якому у нас є з 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.

А що з Silverlight?

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

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

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