${habrauser}, Привет!
При разработке игрового фреймворка Oriol Engine (которая, к слову, до сих пор ведётся) мы столкнулись с проблемой написания шейдеров для Cross-API рендеринга. В RHI-слой данного фреймворка было запланировано добавить поддержку таких графических API, как DX11/DX12, OpenGL и Vulkan.
И вот тут возникает вопрос: как же писать шейдеры на одном языке и обеспечить их поддержку на других графических API?
Для начала стоит определиться: на каком языке изначально мы планируем писать шейдеры? Из‑за приоритетов разработки игр в дальнейшем под продукцию Microsoft, в нашем случае логичнее переводить HLSL (версии 4 и 5) в GLSL, а не наоборот, ибо нет полного контроля над HLSL‑исходником.
Итак, уже начинает вырисовываться какая-то общая концепция: сначала конвертируем HLSL-исходник в GLSL, компилируем эти два исходника и помещаем в один бинарный файл под тегами [HLSL] и [GLSL] для дальнейшего удачного прочтения бинарника.
Да, в конечном итоге оно так и работает.

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

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

Frontend
Фронтовая часть HTG обрабатывает исходный HLSL-код в такой последовательности (мало чем отличается от frontend любого другого компилятора).
Препроцессинг
По официальному справочнику от Microsoft, препроцессор HLSL должен распознавать 12 директив: #define, #elif, #else, #endif и другие.
Не буду подробно про них здесь рассказывать. HTG в данном плане работает в точности по справочнику, поэтому welcome to the Microsoft Learn.
Парсинг
Здесь тоже всё достаточно стандартно. Сначала проводим лексический и синтаксический анализы. На этих этапах:
выявляются лексические и синтаксические ошибки;
проверяется соответствие HLSL‑спецификации;
извлекаются семантики (например,
POSITION,TEXCOORD).
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).
Конец
Да, на этом, в общем-то, всё. На данный проект мы потратили почти половину года своей жизни. Это была довольно кропотливая работа, и я не исключаю того факта, что что-то мог упустить в данной статье, поэтому смело критикуйте в комментариях.
Если у вас возник интерес самостоятельно покопаться в коде, то оставлю ссылку на сам компонент вот тут.
Спасибо за прочтение :-)
