У третій частині серії статей про графік Qt розглянемо, як обробляються шейдери Qt Quick у версії Qt 5.14 при перемиканні графа сцени на рендеринг через QRhi і Qt Rendering Hardware Interface (апаратний інтерфейс рендерингу Qt). Охопимо обробку шейдерів, перш ніж заглиблюватися в сам RHI, тому що додатки Qt Quick, що використовують елементи ShaderEffect або нестандартні матеріали (custom materials), повинні самі надавати код фрагмента та/або вершинного шейдера, і тому вони повинні знати (а в Qt 6 перейти на) новий підхід до обробки шейдерів.
Говорячи про Qt 6 (хоча все описане тут відноситься тільки до Qt 5.14 і може змінитися в пізніших випусках) те, що тут є, швидше за все, послужить основою для обробки графіки і обчислення шейдерів в Qt 6, як тільки нерівних країв, що залишилися, будуть усунуті.
Чому щось нове?
Проблема 1
Перегляд дерева вихідних текстів qt (тобто git repo, що містить QtQml, QtQuick та пов'язаних з ним модулів) та пошук у каталозі шейдерів з вершинними та фрагментними шейдерами для вбудованих матеріалів сценографа Qt Quick показує, що Qt Quick вже поставляється з двома версіями кожного вершинного або фрагментного шейдера GLSL:
Чому? Це пов'язано з підтримкою контекстів OpenGL профілю ядра (для версії 3.2 та вище). Оскільки реалізації OpenGL не зобов'язані підтримувати компіляцію шейдерів GLSL 100/110/120 у такому контексті, Qt немає іншого вибору, крім як постачати його з двома варіантами: один підходить для OpenGL ES 2.0, OpenGL 2.1 і профілів сумісності, а інший (версії 150 на практиці), який використовується лише тоді, коли контекст виявляється профільним. Як описано в частині 1 серії статей, це важливо для того, щоб дозволити розробникам додатків вирішувати, який контекст OpenGL вимагати, коли їх додаток комбінує свій власний рендеринг OpenGL і інтерфейс користувача на основі Qt Quick, незалежно від того, чи є контекст сумісністю або контекстом основного профілю Qt Quick як і раніше зможе відображати.
Це нормально, коли число варіантів дорівнює 2. Що якщо тепер можуть знадобитися Vulkan-сумісні GLSL (Vulkan-compatible GLSL), HLSL та MSL на додаток? На жаль, підхід не дуже масштабний.
Проблема 2
Деякі нові графічні API більше не мають вбудованої підтримки компіляції шейдерів, на відміну від OpenGL (до побачення glCompileShader). І, навіть, якщо вони роблять принаймні як окрему бібліотеку, вони можуть не пропонувати можливості відображення під час виконання, що означає, що немає способу динамічно визначити, які входи вершин та інші ресурси шейдерів є вершинними, фрагментними або обчислювальними шейдерами, і яке розташування цих ресурсів (наприклад, які імена та усунення елементів в єдиному блоці).
Проблема 3
Внутрішня деталь: система пакетування сценографа Qt Quick трохи покладається на переписування вершинного шейдера для матеріалів, які використовуються з так званою об'єднаною партією (merged batch) (це те, що виходить, коли кілька геометричних вузлів зрештою генерують один єдиний виклик відтворення). Перезапис шейдера на льоту перед передачею його в glCompileShader підходить, коли використовується лише одна мова шейдингу, але не масштабується, коли нам потрібно реалізувати одну і ту ж логіку для декількох різних мов.
Що тоді?
Дивлячись на сторінку Khronos SPIR page, ви бачите красиву та інформативну картину SPIR-V Open Source Ecosystem (про екосистему відкритого джерела SPIR-V). Чому б не спробувати спиратися на це?
Ключові компоненти, які цікаві:
• glslang, компілятор з (OpenGL або Vulkan) GLSL у SPIR-V, проміжне уявлення.
• SPIRV-Cross, бібліотека для роздумів про SPIR-V та розбирання його на мови високого рівня, такі як GLSL, HLSL та MSL.
Отже, якщо «стандартизувати» одну мову, таку як GLSL у стилі Vulkan, скомпілювати її у SPIR-V, вийде щось підходяще для Vulkan. Якщо потім запустити двійковий файл SPIR-V через SPIRV-Cross, отримаємо необхідну інформацію про відображення та зможемо згенерувати вихідний код для різних версій GLSL, HLSL та Metal Shading Language (GLSL, як і раніше, важливий, тому що, хоча для OpenGL існують розширення, що використовують SPIR-V, на це розраховувати просто не вийде, оскільки таке розширення, ймовірно, не буде на 90% цільових платформ і пристроїв Qt).
Нарешті, зберіть все це разом (включаючи метадані відображення) в пакет, що легко (де) серіалізується, і там є потрібне рішення.
Тому конвеєр, який використовується при запуску програми Qt Quick із встановленим QSG_RHI=1 :
Vulkan-flavor GLSL [ -> generate batching-friendly variant for vertex shaders] -> glslang : SPIR-V bytecode -> SPIRV-Cross : reflection metadata + GLSL/HLSL/MSL source -> pack it all together and serialize to a .qsb file
Розширення .qsb походить від імені інструмента командного рядка, що виконує описані вище кроки - qsb , скорочення від Qt Shader Baker (не плутати з QBS).
Під час виконання файли .qsb десеріалізуються в екземплярах QShader. Це досить простий контейнер, наступний стандартним шаблонам Qt, таким як неявне сумісне використання, і містить кілька варіантів вихідного коду і байт-коду для одного шейдера, разом з QShaderDescription, який, що не дивно, містить дані відображення. Як і решта RHI, ці класи на даний момент є приватним API.
Графічний шар безпосередньо використовує екземпляри QShader. Об'єкти стану графічного конвеєра задають QShader кожної активної стадії шейдера. Потім бекенди QRhi вибирають відповідний варіант шейдера з пакету QShader.
Починаючи з версії Qt 5.14, на практиці це означає пошук:
• SPIR-V 1.0 при націленні на Вулкан,
• Вихідний HLSL або DXBC для Shader Model 5.0, якщо націлено на D3D11,
• Metal 1.2 сумісний MSL-джерело або попередньо скомпільований metalib при націлюванні на Metal,
• джерело GLSL для однієї з версій 320, 310, 300, 100, при націлюванні на контекст OpenGL ES (у цьому порядку пріоритетів, починаючи з найвищої версії, що підтримується контекстом),
• джерело GLSL для однієї з версій 460, 450, ..., 330, 150, при націлюванні на контекст профілю ядра OpenGL (у тому порядку пріоритету, починаючи з найвищої версії, що підтримується контекстом),
• Джерело GLSL для однієї з версій 120, 110 при націлюванні на неосновний контекст OpenGL (у цьому порядку пріоритетів).
Спочатку записи HLSL і MSL у наведеному вище списку можуть здатися трохи дивними. Це пов'язано з тим, що можна компілювати HLSL і MSL з вихідного коду під час виконання (за замовчуванням підхід), також були проведені деякі експерименти, що дозволяють включати попередньо скомпіловані проміжні формати в пакети .qsb . На практиці це означає виклик fxc (поки немає підтримки dxc - це також у планах, але буде дійсно актуально тільки після того, як QT Company почне дивитися на D3D12) або інструментів командного рядка Metal перед етапом "-> pack it all together" («упакувати всі разом») в конвеєрі показано вище. Проблема тут полягає в тому, що такі інструменти пов'язані з їхньою платформою (Windows і MacOS відповідно), тому qsb може викликати їх лише при роботі на цій платформі. Так, наприклад, при ручній генерації файлу .qsb у Linux це не варіант. У довгостроковій перспективі це, швидше за все, стане меншою проблемою, тому що в часовому інтервалі Qt 6 розробникам все одно знадобиться дослідити кращу інтеграцію системи складання, тому ручні інструменти, такі як qsb, будуть менш поширеними.
Звідки ця штука qsb?
Із модуля Qt Shader Tools. Це забезпечує як API, QShaderBaker, так і інструмент командного рядка qsb для виконання етапів компіляції, перекладу та пакування, описаних вище.
На даний момент модуль qt-labs не постачається з Qt 5.14.
Чому? Ну, в основному через сторонні залежності, такі як glslang і SPIRV-Cross. Є кілька речей, які потрібно дослідити і з'ясувати, коли мова заходить про можливість компілювання та запуску на всіх цільових платформах Qt Company, також деяких речей, пов'язаних з ліцензіями тощо. Якщо все це звучить знайомо, то це тому, що деякі з ці проблеми були згадані в першій частині цієї серії статей, коли йшлося про рішення для перекладу API. Отже, на даний момент створення пакета .qsb включає перевірку і складання цього модуля, а потім запуск інструменту qsb вручну.
У той час, як необхідно рішення, яке входить у комплект постачання Qt, покладатися на автономну обробку шейдерів не так уже й погано. У будь-якому випадку це одна з цілей для Qt 6. Ідея полягає в тому, щоб мати щось, що інтегрується з системою складання Qt (Qt's build system) , щоб описані вище кроки обробки шейдера виконувались під час складання програми ( чи бібліотеки). Це залишається, як майбутнє завдання, головним чином через майбутній переход qmake -> cmake. Щойно ситуація стабілізується, можна розпочати будувати рішення поверх нової системи.
То як же це робить Qt Quick в Qt 5.14?
Дивлячись на qtdeclarative/src/quick/scenegraph/shaders_ng, відповідь очевидна, запустивши qsb вручну (зверніть увагу на compile.bat) і включивши файли .qsb в бібліотеку Qt Quick через систему ресурсів Qt. Це має стати трохи складнішим пізніше.
Файли .vert та .frag містять сумісний з Vulkan код GLSL і не постачаються у збірці Qt Quick. У файлі scenegraph.qrc перелічені лише файли .qsb.
Процес добре описаний на слайді нижче:
Кожен матеріал має тільки одну пару вершинних і фрагментних шейдерів, які завжди записуються як Vulkan-сумісний GLSL, дотримуючись кількох простих угод (як, наприклад, використання тільки одного уніфікованого буфера, поміщеного в прив'язку 0).
Кожен із цих файлів потім запускається через апарат для «випікання» шейдерів, у результаті виходить пакет QShader. У цьому прикладі результатом є 6 версій того самого шейдера, а також дані відображення (які qsb може друкувати, як текст JSON, однак, самі файли .qsb є стислими двійковими файлами і не читаються людиною). Проблеми 1 та 2 , про які йшлося вище, таким чином, вирішені.
Зверніть увагу на теги [Standard] у списку шейдерів. Якби це був вершинний шейдер, а також був вказаний аргумент -b, кількість вихідних шейдерів було б 12, а не 6. 6 додаткових було б позначено як [Batchable], вказуючи, що вони були дружніми для пакетування. Дещо змінені варіанти, призначені для рендерера графа сцен Qt Quick. Це вирішує проблему 3 за рахунок дещо вищих вимог до зберігання (але через скорочений час виконання це, ймовірно, того варте).
Це охоплює основні концепції нового шейдерного конвеєра. Ми повинні подивитись ShaderEffect і QSGMaterial в окремій статті. Основна ідея (починаючи з Qt 5.14) полягає в тому, щоб передавати імена файлів .qsb замість вихідних рядків шейдерів, але, зокрема, матеріали повинні знати ще кілька речей (в основному через роботу з уніфікованими буферами замість одиничних уніформ і відсутність концепції поточного контексту для кожного потоку, де кожен може довільно змінювати стан). Але про все це іншим разом.