Оптимизация шейдеров для iOS и Android

Материал из Вики по GameMaker
Перейти к: навигация, поиск

На сайте Apple для разработчиков есть целый сборник советов по OpenGL ES, всем рекомендуется к прочтению целиком, но я перевёл и адаптировал под гамак очень важную её часть - советы по оптимизации шейдеров.

Содержание

[править] Соответствуйте аппаратным ограничениям на шейдеры

OpenGL ES имеет жёсткие ограничения на количество переменных каждого типа, используемых в шейдерах. При этом стандарт не обязывает драйвер в случае отказа предоставлять программное исполнение кода, шейдер просто не скомпилируется. При разработке приложений нужно убедиться, что шейдер корректно компилируется.

[править] Используйте маркеры точности чисел

Маркеры точности были добавлены в GL ES чтобы удовлетворить потребность в компактных по размеру типов, чтобы лучше соответствовать вычислительным способностям мобильных устройств. Каждая переменная может указать свою собственную точность. Стандарт не требует, чтобы на реальном устройстве числа были именно указанной точности, но компилятор может принять это во внимание, чтобы сгенерировать более эффективный и быстрый код. Ограничения на разные типы указаны в спецификации, а именно:

  • lowp - низкая точность, число ограничено величинами ±2.0, 2-3 десятичных знака; для целых чисел ±255
  • mediump - средняя точность, ограничение ±16384.0, 2-3 десятичных знака; для целых чисел ±1024
  • highp - высокая точность, ограничение ±4.6 квадриллиона (18 нулей), 4-5 десятичных знаков; для целых чисел ±65535

Тут указаны минимальные требования спецификации, реально значения могут быть и выше, но это зависит от конкретного устройства и в целом рассчитывать на такое не стоит. Важно отметить, что ограничения на величину никаким образом не обеспечиваются (слишком большое число не будет "обрезано" до ограничений, а будет глючить), так что после понижения точности приложение надо перетестировать.

Если сомневаешься, какой тип выбрать - выбирай высокую точность. Для данных о цвете, лежащих в пределах от 0 до 1 (без HDR) подойдёт низкая точность. Для нормалей и векторов подойдёт средняя точность. Геометрию лучше вычислять в высокой точности.

precision highp float; // устанавливает точность по-умолчанию для всех float и его производных типов (векторы/матрицы).
uniform lowp sampler2D sampler; // Texture2D() возвращает lowp.
varying lowp vec4 color;
varying vec2 texCoord;   // использует точность highp по-умолчанию.

[править] Избегайте необоснованных операций с векторами

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

highp float f0, f1;
highp vec4 v0, v1;
v0 = (v1 * f0) * f1;

Если эту операцию производить на скалярном (не-векторном) процессоре, то сначала выполнится умножение v1 * f0, и его результат - вектор - будет умножен на f1, таким образом процессор будет вычислять не 4 + 1 операции, а целых 8 (две векторные операции над вектором из четырёх составных). Если же сначала перемножить f0 * f1, то тогда будет выполняться только 4 операции умножения вектора + 1 операция умножения двух чисел. Аналогичным образом, если вычисления над вектором проводятся не полностью (не над всеми составными), то нужно указывать маску вычислений, чтобы не выполнять лишние расчёты:

highp vec4 v0;
highp vec4 v1;
highp vec4 v2;
v2.xz = v0 * v1;

[править] Используйте юниформы и константы вместо того, чтобы вычислять числа в шейдере

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

Ветвления (if-then-else) не рекомендуются к использованию в шейдерах, так как они снижают способность графического процессора к параллельному вычислению что снижает производительность. Это менее ярко выражено на OpenGL ES 3.0-совместимых устройствах.

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

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

Поэтому, если ветвлений никак не избежать, то следует знать, что ветвление на константе работает быстрее всего, ветвление на юниформе работает на адекватной скорости, а ветвление на переменной, вычисленной в шейдере, работает медленно.

[править] Избегайте циклов

Циклы имеют плохую производительность в силу целого ряда причин и их всегда нужно избегать. Иногда можно использовать векторные операции вместо циклов (если система поддерживает векторные вычисления, то этот способ будет самым быстрым), в остальных случаях нужно раскрывать циклы (gamemaker попытается это сделать автоматически). Если циклов никак не избежать, то цикл должен иметь ограничение-константу, чтобы избежать динамического ветвления. Также, зависший шейдер может завесить всю систему и придётся перезагружать устройство.

// вариант с циклом
int i;
float f;
float v[4];
for(i = 0; i < 4; i++)
    v[i] += f;
 
// вариант с раскрытием
float f;
float v[4];
v[0] += f;
v[1] += f;
v[2] += f;
v[3] += f;
// вариант с вектором
float f;
vec4 v;
v += f;

[править] Избегайте вычисления адресации массивов в шейдере

Адресация массивов по вычисленным в шейдере переменным как правило медленнее, чем адресация по юниформам и константам. Доступ к юниформ-массивам как правило быстрее, чем ко временным массивам. Принимайте во внимание динамический семплинг.

Динамический семплинг, также известный как "зависимое чтение текстуры", происходит когда шейдер вычисляет собственные координаты для семплинга вместо того чтобы использовать те, что были предоставлены изначально. На OpenGL ES 3.0-совместимых устройствах такая операция не несёт дополнительных вычислительных затрат. На других устройствах это может создать задержку прочтения данных из текстуры, снижая производительность. Процессор может загрузить пиксель заранее (префетч) до того, как выполнить шейдер, нивелируя таким образом часть задержки чтения текстуры. Часто можно перенести вычисление координат текстур в вертекс-шейдер, таким образом фрагмент-шейдер изначально будет иметь изменённые координаты текстуры и не потребуется динамически их менять.

[править] Используйте мультитекстурирование вместо многопроходного рендерения

Рендерение в несколько проходов требует изменения параметров OpenGL, смены контекстов, текстур и прочего, и это отнимает время. Вместо этого лучше сразу рисовать из нескольких текстур прямо в шейдере. Любое мобильное устройство поддерживает как минимум две мультитекстуры (первая из них всегда используется GameMaker для отрисовки - спрайты, бекграунды, частицы, и т.д.), в то время как многие новые устройства поддерживают 8 или даже больше. Чтобы отправить в шейдер дополнительные текстуры, нужно сначала извлечь номер семплера из шейдера, а потом забиндить текстуру на этот семплер.

sampler = shader_get_sampler_index(shader, "s_MultiTexSampler")
tex = sprite_get_texture(sprite_index, 0);
shader_set(shader);
texture_set_stage(sampler, tex);
uniform sampler2D s_MultiTexSampler;
 
void main()
{
    gl_FragColor = v_vColour * texture2D( gm_BaseTexture, v_vTexcoord ) * texture2D ( s_MultiTexSampler, v_vTexcoord );
}

Стоит отметить, правда, что по умолчанию GameMaker заталкивает все текстуры в атлас, что создаёт некоторые затруднения с использованием мультитекстур так как координаты спрайтов могут не совпадать, а с другой стороны если оба спрайта на одной текстуре, то можно сэмплить с одной и той же текстуры. Также, в гамаке не предусмотрено специальной функции, которая вернула бы количество доступных мультитекстур, поэтому это число придётся извлекать из шейдера.

[править] В заключение

На десктопах эти мудрости менее применимы, в частности все числа всегда имеют максимальную точность, все процессоры векторные, а производительность ветвлений не такая плохая, но и там есть смысл прислушиваться к этим советам.

Персональные инструменты
Пространства имён

Варианты
Действия
Навигация
Инструменты