${habrauser}, Привет!

При разработке игрового фреймворка Oriol Engine (которая, к слову, до сих пор ведётся) мы столкнулись с проблемой написания шейдеров для Cross-API рендеринга. В RHI-слой данного фреймворка было запланировано добавить поддержку таких графических API, как DX11/DX12, OpenGL и Vulkan.

И вот тут возникает вопрос: как же писать шейдеры на одном языке и обеспечить их поддержку на других графических API?

Для начала стоит определиться: на каком языке изначально мы планируем писать шейдеры? Из‑за приоритетов разработки игр в дальнейшем под продукцию Microsoft, в нашем случае логичнее переводить HLSL (версии 4 и 5) в GLSL, а не наоборот, ибо нет полного контроля над HLSL‑исходником.

Итак, уже начинает вырисовываться какая-то общая концепция: сначала конвертируем HLSL-исходник в GLSL, компилируем эти два исходника и помещаем в один бинарный файл под тегами [HLSL] и [GLSL] для дальнейшего удачного прочтения бинарника.

Да, в конечном итоге оно так и работает.

Схема работы компонента ShaderPack
Схема работы компонента ShaderPack

Почему же мы даже не стали смотреть в сторону SPIR-V? Потому что нам нужно было что-то компактное и самостоятельное, что можно добавить одним компонентом в фреймворк и дальше не париться.

Составление самого бинарного файла .shader особого интереса из себя не представляет, потому что это файл с довольно простой структурой: под тегами [HLSL] и [GLSL] хранится соответствующий байт-код того или иного исходника.

Структура .shader файла
Структура .shader файла

Но вот сама конвертация HLSL To GLSL (HTG) требует должного внимания. Сперва разберём этот компилятор на этапы: Frontend, Middle-end, Backend, и подробно рассмотрим каждый.

Этапы конвертации HLSL To GLSL (HTG)
Этапы конвертации HLSL To GLSL (HTG)

Frontend

Фронтовая часть HTG обрабатывает исходный HLSL-код в такой последовательности (мало чем отличается от frontend любого другого компилятора).

Препроцессинг

По официальному справочнику от Microsoft, препроцессор HLSL должен распознавать 12 директив: #define, #elif, #else, #endif и другие.

Не буду подробно про них здесь рассказывать. HTG в данном плане работает в точности по справочнику, поэтому welcome to the Microsoft Learn.

Парсинг

Здесь тоже всё достаточно стандартно. Сначала проводим лексический и синтаксический анализы. На этих этапах:

  • выявляются лексические и синтаксические ошибки;

  • проверяется соответствие HLSL‑спецификации;

  • извлекаются семантики (например, POSITIONTEXCOORD).

Middle-end

Данный этап является переходным в конвертации HLSL в GLSL. Здесь происходит трансляция в промежуточное низкоуровневое IR-представление на основе AST-дерева, близкое к GLSL. Сперва HLSL-семантики переводятся в GLSL-директивы (например, layout(location = 0)). Затем текстуры и сэмплеры разделяются, структуры и типы данных адаптируются под GLSL-синтаксис.

Backend

В конечном этапе IR транслируется в GLSL-код: формируются объявления переменных и функций, вставляются необходимые препроцессорные директивы, обрабатываются особенности целевой платформы (например, версии GLSL). К выходному коду применяются форматирование (отступы, переносы) и манглинг имён там, где это требуется.

Про последнее давайте поподробнее:

Для разных категорий идентификаторов применяются разные схемы генерации префикса или суффикса:

  • Глобальные переменные: g_ + хеш или уникальный индекс (например, g_var123).

  • Локальные переменные: l_ + номер блока + индекс (например, l_blk2_var4).

  • Параметры функций: p_ + имя функции + индекс (например, p_main_arg0).

  • Поля структур: s_ + имя структуры + имя поля (например, s_VertexInput_pos).

HLSL‑семантики преобразуются в GLSL‑атрибуты, а имя переменной может быть манглено для отражения семантики:

// HLSL
float4 pos : POSITION;
// GLSL
layout(location = 0) in vec4 s_VertexInput_pos;

Если два идентификатора после манглинга совпадают, к ним добавляется уникальный суффикс (например, _1_2).

Конец

Да, на этом, в общем-то, всё. На данный проект мы потратили почти половину года своей жизни. Это была довольно кропотливая работа, и я не исключаю того факта, что что-то мог упустить в данной статье, поэтому смело критикуйте в комментариях.

Если у вас возник интерес самостоятельно покопаться в коде, то оставлю ссылку на сам компонент вот тут.

Спасибо за прочтение :-)