dFdxFine
возвращает одинаковое значение для двух пикселей квадрата, то есть на квадрат получается 2 уникальных значения.
dFdxCoarse
возвращает одно значение для всего квадрата.
subgroupQuadBroadcast
возвращает точное значение из каждого потока внутри квадрата.
В фрагментном шейдере subgroupQuadBroadcast
соответствует реальному квадрату, а в остальных - нет и по координатам из gl_GlobalInvocationID
"квадрат" оказывается линией.
Эмуляция дериватив через subgroupQuadBroadcast
для использования в компьют шейдере.
#define dFdxFine( x ) (subgroupQuadBroadcast( (x), (gl_SubgroupInvocationID&2)|1 ) - subgroupQuadBroadcast( (x), gl_SubgroupInvocationID&2 ))
#define dFdyFine( x ) (subgroupQuadBroadcast( (x), (gl_SubgroupInvocationID&1)|2 ) - subgroupQuadBroadcast( (x), gl_SubgroupInvocationID&1 ))
#define dFdxCoarse( x ) (subgroupQuadBroadcast( (x), 1 ) - subgroupQuadBroadcast( (x), 0 ))
#define dFdyCoarse( x ) (subgroupQuadBroadcast( (x), 2 ) - subgroupQuadBroadcast( (x), 0 ))
Существует расширение GLSL_NV_compute_shader_derivatives
которое позволяет использовать деривативы в компьют шейдере, а также явно задать расположение пикселей в квадрате.
Для процедурного SDF в 2D деривативы не нужны, если прирост координат идет одинаково по осям и равномерно, например uv + (1,0)
, uv + (0,1)
.
Деривативы нужны:
- для 3D пространства
- для неравномерного 2D
- для SDF текстур.
Тонкие линии
Деривативы нужны чтобы узнать изменение пространства между соседними пикселями. Они не требуются только в 2D пространстве без искажений, тогда деривативы всегда будут возвращать одинаковую разницу.
На картинке зеленая линия uv
- координаты в пространстве, они изменяются равномерно, без перегибов. Красная линия sdf
- дистанция до линии или любой другой формы, заданной SDF функцией, дистанция измеряется в том же пространстве, что и uv
.
Часто sdf идет с перегибами, здесь пик оказался между пикселями и потерялся, поэтому минимальное значение sdf смещается на расстояние между пикселями md
.
В 3D на расстоянии или под большим углом шаг становится слишком большим и теряются детали в sdf, тогда сглаживание перестает работать и приходится делать затухание.
Пример.
Шрифты
SDF шрифты позволяют их увеличивать и уменьшать не теряя сглаживание. Также как с линиями, при уменьшении шаг градиента увеличивается, чтобы заполнить хотя бы 1 пиксель.
Константное смещение для градиента (smoothstep
) используется для изменения стиля шрифта (жирный, контурный).
Пример.
Tangent и Bitangent вектора должны быть направлены в ту же сторону что и текстурные координаты UV.
Зная как меняется worldPos
и uv
можно рассчитать нормаль как векторное произведение и касательные (TB) как 3D вектор для uv
.
float3x3 ComputeTBNinFS (float2 uv, float3 worldPos)
{
float3 wp_dx = dFdx( worldPos );
float3 wp_dy = dFdy( worldPos );
float2 uv_dx = dFdx( uv );
float2 uv_dy = dFdy( uv );
float3 t = normalize( wp_dx * uv_dy.t - wp_dy * uv_dx.t );
float3 b = normalize( -wp_dx * uv_dy.s + wp_dy * uv_dx.s );
float3 n = normalize( cross( wp_dy, wp_dx ));
return float3x3( t, b, n );
}
- В большинстве случаев нет разницы между dFdxFine и dFdxCoarse.
- Хорошо работает для плоских поверхностей и небольших изгибов.
- Хорошо работает для карты высот, но только когда высоты в формате float32, для float16 точность теряется и результат хуже.
- Можно использовать как альтернативный способ расчета нормалей для проверки корректности трансформаций у предрасчитаных нормалей.
-
Фильтрация R16F текстуры с включенным
mediump float
работает по-разному на NVidia и других ГП. На NV появляются артефакты фильтрации. -
На мобилках требуется
highp sampler2D
для 16-битных форматов иначе теряется точность даже без фильтрации (texelFetch). -
Фильтрация текстур происходит с 8-битной точностью. ref
- Актуально для всех форматов.
- Проявляется при расчете попиксельных нормалей для карты высот через деривативы.
-
Исправить проблему с 8-битной фильтрацией можно через
textureGather()
, но есть разница междуfract()
в шейдере и определением текселя. ref- Решается добавлением смещения
fract(... + 1/512)
для 8-битной субпиксельной точности. - На разном железе субпиксельное смещение может отличаться. ref
- MoltenVk всегда возвращает точность в 4 бита, как минимум по спецификации, хотя реальная точность может быть больше.
- Решается добавлением смещения
Эффект рассеивания света на линзе. Чем больше яркость, тем больше рассеивается. Результат прибавляется к цвету сцены.
Если рисовать через 2 треугольника, то будет задействовано больше потоков, чем при использовании одного треугольника растянутого на [2, 2].
На Adreno выключается TBDR для полноэкранного прохода.
Multiview - позволяет рисовать в массив 2D текстур с разными проекциями на view. В вершинном шейдере можно выбрать трансформацию по gl_ViewIndex
, задать view нельзя, вершины дублируются во все view.
Вершинный шейдер (а также TES, GS) вызывается один раз, а растеризуется в разные view с разной проекцией, для этого в NV сначала выполняется общая часть VS, а затем для каждого вию выполняется код, зависящий от gl_ViewIndex
.
На TBDR архитектуре multiview позволяет один раз выбрать тайл в который будет рисоваться треугольник. Также улучшаяется использование текстурного кэша, когда известно, что одна и та же геометрия рисуется в разные области, так тайлы в разные view могут идти последовательно.
Чем больше общей геометрии в каждом view, тем лучше производительность. Для рисования разной геометрии в разные view стоит использовать другой способ.
NV Turing поддерживает 4 view в железе, и 32 через API, .
Используется в VR для рисования в оба глаза за один проход. Используется для cascaded shadow map.
Multiview в ARM Mali.
Layered rendering - позволяет рисовать в массив 2D текстур. Задается через gl_Layer
в геометричеком шейдере.
Расширение VK_EXT_shader_viewport_index_layer
позволяет выбирать слой в вершинном шейдере, дублирование геометрии делается через инстансинг.
Viewport и scissor задаются сразу для всех слоев.
Используется для рисования кубических карт за один рендер пасс.
Viewport array - позволяет рисовать в 2D текстуру с разными проекциями на виюпорт. Задается через gl_ViewportIndex
в геометричеком шейдере.
Расширение VK_EXT_shader_viewport_index_layer
позволяет выбирать виюпорт в вершинном шейдере, дублирование геометрии делается через инстансинг.
Используется как layered rendering, только позволяет задавать размер области и не требует массив 2D текстур. Позволяет задавать отдельный scissor для каждого виюпорта.
До появления VRS использовалось для multi-res shading - экран делится на 9 частей, края рендерятся в меньшем разрешении.
- Растеризация, тест глубины и выполнение фрагментного шейдера идет блоками по 2х2 пикселя (quads).
- В тайловой архитектуре (TBR) область в 16х16 пикселя привязана к одному SM, в TBDR архитектуре размер тайла начинается с 16х16, в ARM Mali Valhall архитектуре fragment task заполняет область в 32х32 пикселей, в 5thGen увеличили до 64х64. Поэтому на всех архитектурах рендеринг в текстуру должен быть в область кратную 16, чтобы максимально нагрузить ГП.
Размер тайла может быть меньше, при большом G-буфере или использовании большого количества регистров.
- На старых мобилках максимальный размер 64 (8х8), поддерживается и 128, но с вдвое меньшим количеством регистров.
- На NV Turing нужно минимум 128 (32х4) потоков чтобы максимально загрузить SM.
Сильно зависит от однородности потока выполнения внутри варпа (uniform control flow). Если все потоки идут по одинаковому пути, то ветвление быстрее умножения, если по разным, то умножение будет быстрее на некоторых GPU (NVidia).
- Компилятор заменяет повторяющиеся деления на одно переворачивание (1/x) и умножения.
- Реализация
FastSign
черезStep
, который возвращает -1 или 1, намного быстрее чемSignOrZero
(sign
из GLSL), аcopysign
из MSL - быстрееStep
. FMA
на мобильных работает черезfp32 FMA
, а на NV и Intel используетfp16 FMA x2
что в 2 раза быстрее fp32 для half2, half4.[[unroll]]
сильно замедляет компиляцию пайплайна, в редких случаях дает 2х ускорение, но часто слабо влияет.- На NV mediump может работать медленнее чем highp, на мобильных аналогично fp16.
- Для uint
FindMSB
в 2 раза быстрееFindLSB
, для intFindLSB
может быть быстрее. - На NV/Intel FP32ADD выполняется в 2 раза быстрее чем FP32FMA, FP32MUL и соответствует максимальной производительности по спецификации.
- На мобилках FP32ADD, FP32MUL, FP32FMA выполняется за один цикл.
- В спецификациях считают FMA за 2 инструкции и указывают в 2 раза большую производительность в FLOPS.
SFU pipe (special function unit) - на нем выполняются более редкие операции типа переворачивания (1/x), sqrt, sin, cos, exp, log, fract, ceil, round, sign и тд.
Чаще всего на 4 потока варпа приходится 1-2 SFU, поэтому все перечисленные операции относительно медленные, но некоторые выполняются за одну инструкцию, а другие эмулируются и занимают еще больше времени.
Обычно заточен только под fp32 тип и не имеет оптимизаций под mediump и fp16.
В зависимости от производителя стоимость операций может сильно отличаться, иногда Round в 10 раз дольше Fract, а Length в 2 раза быстрее InvSqrt и Sqrt, на одних деление в 2 раза быстрее Sqrt, на других одинаково.
Normalize часто сделан через 1/Length, а Distance через Length(a - b), поэтому работают чуть медленнее чем Length. Где-то Pow сделан через умножение, поэтому время выполнения растет от степени, а где-то Pow работает за константное время.
В среднем 4 цикла: div, InvSqrt, Sqrt, Fract, SignOrZero, Length, SmoothStep.
В среднем 8 циклов: mod, Pow, Exp, Log, Round, Sin, Cos, SinH, CosH.
От 12 циклов: ASin, ACos, Tan, ATan.
Операции сравнения чаще всего выполняются за 1 цикл.
Это equal, lessThan, Min, Max, Step.
Приведение к диапазону (clamp) выполняются за 1-4 цикла.
Отдельный случай: clamp(x,0,1)
может быть одной инструкцией типа add_sat
.
У некоторых производителей clamp(x,-1,1)
работает как и clamp(x,0,1)
за один цикл.
Clamp без констант работает за 3-4 цикла.
Конвертация типов
Битовый каст типа uintBitsToFloat работает быстрее всего.
В среднем 4 цикла занимает конвертация между int и float.
Integer типы
Битовые операции и сложение работает за 1 цикл. На мобильных архитектурах может быть медленее и доходить до 2-4 циклов. На некоторых архитектурах может работать параллельно с float, на других - часть float блоков отключается и теряется производительность.
Около 4 циклов: mul, FindMSB(uint), BitCount.
От 8 циклов: FindLSB, FindMSB(int), uaddCarry, usubBorrow.
От 16 циклов: div, mod, umulExtended.
Подробные результаты микробенчмарков: GPU_Benchmarks
Чаще всего доступны счетчики производительности, часть из них в количестве (циклы, байты, транзакции и тд), часть в процентах. Для количественных значений нужно запускать микробенчмарки чтобы найти максимальное значение за кадр или за секунду, например GB/s для памяти. Значения из спецификаций не всегда совпадают с измеряемыми, и сами спецификации на смартфоны содержат ошибки. Например кроме скорости самой памяти есть еще пропускная способность шины (AXI bus) по которой соеднены ГП с памятью.
Главный показатель это частота ГП. Если на ГП нет нагрузки, то для экономии энергии частота понижается. Многие счетчики в процентах могут при этом показывать до 100% нагрузки, но пока частота низкая это не имеет особого значения. Когда нет доступа к счетчикам, проверить достиг ли ГП максимальной частоты можно двумя способами:
- Увеличить нагрузку в 2 раза, тогда время работы должно увеличиться в те же 2 раза. Если это не так, значит производительность упирается во что-то другое.
- Посмотреть историю времени выполнения каждого кадра. Если частота ГП плавает, то время будет нестабильным. Нужно повышать нагрузку пока время кадров не станет одинаковым.
Второй показатель это нагрузка на внешнюю память (RAM, external memory). Если нагрузка приближается к максимальной, значит кэши не используются. Чем больше используются кэши, тем больше памяти обрабатывает ГП.
Следом идет L2 кэш.
На Mali кэш L2 используется также для хранения тайла и общей памяти компьют шейдера, поэтоу чем больше G-буфер или размер общей памяти, тем больше шансов, что данные вытеснятся из кэша и скорость доступа к ним сильно замедлится.