Доброго дня хабр. Это моя первая статья на хабре, не судите строго.

В данной статье я хотел-бы рассмотреть тему генерации кода шейдеров для разных платформ, а также их оптимизацию.


С каждым годом мобильные устройства становятся все более мощнее, и более доступнее. Сейчас ни кого не удивишь 3d игрой на мобильном, рынок растет, а вместе с ним и растет желание платформ видеть эксклюзивы только в своем маркете. Еще 3-5 лет назад для разработки игр предоставлялось 2 апи, OpenGLES и DirectX, то сегодня их кол-во растет. Apple активно продвигает Metal, а на этой неделе уже вышли development версии android прошивок с поддержкой Vulkan. С ростом числа поддерживаемых в проекте систем рендеринга растет и кол-во платформо-зависимого кода.

Задаваясь вопросом «Как минимизировать кол-во платформо-зависимого кода?» я пришел к выводу что необходимо начать с шейдеров, несмотря на то что синтаксис у современных языков разный (HLSL, GLSL, Metal) выполняют они одни и те же задачи. Первое что приходит в голову — писать на одном синтаксисе и генерировать из него остальные. Идея не нова, данный подход активно используется в Unity, Unreal Engine. В Unity для шейдеров используется NVidia CG подобный синтаксис, CG Toolkit позволяет генерировать код для разных платформ. Но к сожалению NVidia уже достаточно прекратила его поддержку. И как не трудно заметить в профилях нет современных api к примеру Metal, но при этом Unity поддерживает его генерацию. После не продолжительных поисков я нашел фреймворк оптимизирующий glsl es шейдеры с поддержкой генерации Metal, по заверениям автора данной библиотеки она используется в Unity.

Итого получилось что можно писать шейдеры на GLSL и генерировать из них Metal. Но остается еще один синтаксис — HLSL, найти генератор из GLSL в HLSL у меня не получилось, к тому же в HLSL существует понятие точности отсутствующее в GLSL 1.0. Но зато нашелся обратный генератор все у того же автора. За основу в итоге был взят синтаксис HLSL. Осталось определится о формате входных данных, к примеру все у того же CG Toolkit есть возможность загружать fx файлы, содержащие необходимые функции и метаданные с необходимой информацией (настройки pipeline, имена функций для шейдеров). Для парсинга fx файлов было решено использовать проект hlslparser, в данный проект была добавленна возможность понимать метаданные. В итоге собрав все это во едино, была написана не большая библиотека, позволяющая генерировать GLSL, HLSL, Metal шейдеры из входных fx файлов с синтаксисом HLSL.

Ниже следует пример входного fx файла, и что из него сгенерировалось

fx.fx
float4x4 u_MVPMatrix;

struct VS_DEFAULT_OUTPUT 
{
   float4 position: POSITION;
   float2 texture_coord: TEXCOORD0;
   float4 color: COLOR0;
};

VS_DEFAULT_OUTPUT vs_default_texture(float4 u_position: POSITION, float2 u_texture_coord: TEXCOORD0, float4 u_color: COLOR0) 
{
	VS_DEFAULT_OUTPUT Out;
	Out.position = mul(u_MVPMatrix, u_position);
	Out.texture_coord = u_texture_coord;
	Out.color = u_color;
	return Out;
}

float4 ps_default_texture(VS_DEFAULT_OUTPUT Out, uniform sampler2D u_texture) : COLOR
{
	float4 clr = tex2D(u_texture, Out.texture_coord) * Out.color;
	return clr;
}

technique default_texture
{
	pass P0
	{
		vertexShader = vs_default_texture();
		pixelShader = ps_default_texture();
		depthtest = false;
		depthwrite = false;
	}
}


Сгенерированный вертексный шейдер для GLES выглядит следующим образом
uniform highp mat4 u_MVPMatrix;
attribute highp vec4 u_position;
attribute highp vec2 u_texture_coord;
attribute highp vec4 u_color;
varying highp vec2 xlv_TEXCOORD0;
varying highp vec4 xlv_COLOR0;
void main ()
{
  gl_Position = (u_MVPMatrix * u_position);
  xlv_TEXCOORD0 = u_texture_coord;
  xlv_COLOR0 = u_color;
}

И соответственно пиксельный шейдер
uniform sampler2D xlu_u_texture;
varying highp vec2 xlv_TEXCOORD0;
varying highp vec4 xlv_COLOR0;
void main ()
{
  lowp vec4 tmpvar_1;
  tmpvar_1 = texture2D (xlu_u_texture, xlv_TEXCOORD0);
  highp vec4 tmpvar_2;
  tmpvar_2 = (tmpvar_1 * xlv_COLOR0);
  gl_FragColor = tmpvar_2;
}


Ниже следуют сгенерированные шейдеры для Metal
#include <metal_stdlib>
#pragma clang diagnostic ignored "-Wparentheses-equality"
using namespace metal;
struct xlatMtlShaderInput {
  float4 u_position [[attribute(0)]];
  float2 u_texture_coord [[attribute(1)]];
  float4 u_color [[attribute(2)]];
};
struct xlatMtlShaderOutput {
  float4 gl_Position [[position]];
  float2 xlv_TEXCOORD0;
  float4 xlv_COLOR0;
};
struct xlatMtlShaderUniform {
  float4x4 u_MVPMatrix;
};
vertex xlatMtlShaderOutput xlatMtlMain (xlatMtlShaderInput _mtl_i [[stage_in]], constant xlatMtlShaderUniform& _mtl_u [[buffer(0)]])
{
  xlatMtlShaderOutput _mtl_o;
  _mtl_o.gl_Position = (_mtl_u.u_MVPMatrix * _mtl_i.u_position);
  _mtl_o.xlv_TEXCOORD0 = _mtl_i.u_texture_coord;
  _mtl_o.xlv_COLOR0 = _mtl_i.u_color;
  return _mtl_o;
}

#include <metal_stdlib>
#pragma clang diagnostic ignored "-Wparentheses-equality"
using namespace metal;
struct xlatMtlShaderInput {
  float2 xlv_TEXCOORD0;
  float4 xlv_COLOR0;
};
struct xlatMtlShaderOutput {
  half4 gl_FragColor;
};
struct xlatMtlShaderUniform {
};
fragment xlatMtlShaderOutput xlatMtlMain (xlatMtlShaderInput _mtl_i [[stage_in]], constant xlatMtlShaderUniform& _mtl_u [[buffer(0)]]
  ,   texture2d<float> xlu_u_texture [[texture(0)]], sampler _mtlsmp_xlu_u_texture [[sampler(0)]])
{
  xlatMtlShaderOutput _mtl_o;
  half4 tmpvar_1;
  tmpvar_1 = half4(xlu_u_texture.sample(_mtlsmp_xlu_u_texture, (float2)(_mtl_i.xlv_TEXCOORD0)));
  _mtl_o.gl_FragColor = ((half4)((float4)tmpvar_1 * _mtl_i.xlv_COLOR0));
  return _mtl_o;
}


Из недостатков данного подхода стоит отметить следующие
— Metal шейдеры имеют одинаковые имена функций, что не позволит их нести в одной библиотеке, к чему это может привести кроме как созданию для каждого типа шейдера отдельной библиотеки, я затрудняюсь ответить
— В сгенерированных шейдерах отсутствует работа с последними фичами платформ (geometry shader, instancing)

Из плюсов можно отметить
— Оптимизированные GLES шейдеры, из-за того что компиляция на данной платформе легла на плечи вендоров производителей драйверов, некоторые из них не проводят оптимизацию, из-за чего может страдать производительность

Комментарии (13)


  1. Overlordff
    17.04.2016 15:37

    Здравствуйте. Я пока только изучаю программирование 3D графики с использованием Direct3D API. В вашем HLSL шейдере я вижу использование техник и .fx файлов. Не могли бы Вы рассказать, как на сегодняшний день на практике используются шейдеры в DirectX? Дело в том, что, если я не ошибаюсь, .fx эффекты в основном используются со специальным Effects фреймворком (из d3dx11), который объявлен deprecated в последнем DirectX 11. Как сегодня в индустрии используются шейдеры? Создаются в отдельных файлах с константными буферами или каким-то образом можно использовать техники?


    1. duck
      17.04.2016 15:51

      Насколько я понимаю fx файлы действительно объявлены deprecate с 11 версии. Использование файлов эффектов исключительно для удобоваримого описания настроек, и возможности содержать все типы шейдеров в одном файле. После генерации мы также получаем hlsl шейдер, с входной функцией main

      float4x4 u_MVPMatrix;
      struct VS_DEFAULT_OUTPUT {
          float4 position : POSITION;
          float2 texture_coord : TEXCOORD0;
          float4 color : COLOR0;
      };
      VS_DEFAULT_OUTPUT vs_default_texture(float4 u_position : POSITION, float2 u_texture_coord : TEXCOORD0, float4 u_color : COLOR0) {
          VS_DEFAULT_OUTPUT Out;
          ((Out).position = mul(u_MVPMatrix, u_position));
          ((Out).texture_coord = u_texture_coord);
          ((Out).color = u_color);
          return Out;
      };
      float4 ps_default_texture(VS_DEFAULT_OUTPUT Out, uniform sampler2D u_texture) : COLOR {
          float4 clr = (tex2D(u_texture, (Out).texture_coord) * (Out).color);
          return clr;
      };
      float4 main(VS_DEFAULT_OUTPUT Out, uniform sampler2D u_texture) : COLOR {
          return ps_default_texture(Out, u_texture);
      };
      


      сами по себе техники как я понимаю это описание к использованию шейдера, включение того или иного теста, указание входной функции. Все эти настройки можно хранить по раздельности с шейдером. А можно воспользоваться сторонними библиотеками к примеру NVFX. Как сейчас принято разрабатывать на последнем DX я честно говоря затрудняюсь ответить


    1. Nanako
      17.04.2016 16:44

      Есть достаточно жесткий пайплайн: Effect->Technique->Pass, но если вы не будете использовать geometry shaders в принципе ничего не изменилось. hlsl файл компилируется в эффект, который содержит в себе техники и пассы. Считается что техника это завершенный рендер, но все можно сочетать в любых пропорциях. Если раньше приходилось отдельно применять VS/GS/PS шейдеры, то теперь можно выбрать технику и нужный пасс из эффекта и все настройки к контексту применятся пакетом. Для сложного рендера стало проще, для новичков сложнее вобщем. Мой совет, начинайте с D3D9, он проще и с него потом можно легко перейти на 11 когда возникнет реальная необходимость.


    1. Nanako
      17.04.2016 16:52

      И да, AFAIK, не Effects стали deprecated, а поменялась реализация компилятора, что-то убрали, что-то перекинули в другие места. Сам пайплайн не меняли.


      1. Overlordff
        17.04.2016 17:08

        А что используют чаще? Эффекты или отдельные шейдеры? А ведь еще есть precompiled shaders и динамическая линковка. Вот вы, например, что используете?


        1. Nanako
          17.04.2016 17:27

          Перешел на эффекты 4 месяца назад, стало удобнее. Но у меня жутко сложный рендеринг, т.к. я ускоряю WPF на GPU, в том числе и просто расчеты данных всякие. Эффекты компилируются из исходников хранящихся в embedded resource при загрузке сборок, нативные библиотеки для компиляции подгружаются автоматически в зависимости от платформы рантайма .NET. Как-то так.


        1. Nanako
          17.04.2016 17:29

          В эффекте и переменные и constant buffer, описание типов, пара десятков шейдеров, несколько техник с 1-3 пассами.


          1. Overlordff
            17.04.2016 17:32

            А как интегрируете эффекты с приложением? В d3dcompiler_##.dll ничего про эффекты нет (если не ошибаюсь). Используете d3dx11effect фреймворк? Или что-то другое?


            1. Nanako
              17.04.2016 17:38

              В SharpDX .NET обертки из коробки есть.


  1. vidyacat
    17.04.2016 16:00

    Мне кажется ваша проблема надумана. Дело в том, что opengl прекрасно поддерживается ios, вулкан- этот перепиленный огл, и использует он синтаксис glsl, ну а платформа винды, под которую заточен hlsl прекрасно работает с опенжеле.

    Так вот, зачем вам генератор шейдеров, если уже есть glsl, под все возможные платформы? Суть аппловского метала, вулкана и hlsl в том, что они должны давать большую производительность из-за специализации под платформу. Если вы будете генерировать код под все платформы, то вы выстрелите себе в ногу, аннулировав приемущества, предоставленные этими апи.


    1. duck
      17.04.2016 16:17

      Дело в том, что opengl прекрасно поддерживается ios
      по мимо технической стороны вопроса часто на выбор технологии влияют другие факторы. К примеру приложения использующие Metal проще продвигать. Списков топ 10 лучших приложений использующих GLES я честно говоря не видел, а вот топ 10 лучших приложений с использованием метал встречаются. Да и думаю получить фичеринг приложению использующему любимое эплом детище будет проще.

      под которую заточен hlsl прекрасно работает с опенжеле
      есть ли поддержка opengl на мобильных платформах винды (после 8.0 версии)? Angle активно поддерживающийся google и ms работает явно хуже нативной поддержки DX

      Да и цель статьи просто описать как можно уменьшить платформо-зависимый код.


    1. creker
      17.04.2016 19:27

      Опенгл прекрасно не работает даже на взрослой винде.

      А суть метала, вулкана и директ12 совсем не в специализации, а в предоставлении низкоуровневого доступа к железу без потери абстракции от железа насколько это возможно. Иначе во всем этом было бы мало смысла. Их суть это избавление от API оверхеда, а не избавление от всех абстракций.


      1. iga2iga
        18.04.2016 13:20

        Работа OpenGL на винде зависит от вендора видеокарты только. opengl32.dll сам по себе ничего не стоит. Для nVidia, например, это nvogl32/64.dll. У Intel/AMD своя dll'ка с полной реализацией OpenGL. На мобильных платформах все так же. Есть поддержка вендора для конкретной платформы — есть opengl.