120 FPS и плавная игра - не одно и то же
Средняя частота кадров плохо описывает кратковременные остановки. Если большая часть кадров выводится за 7-8 мс, а один внезапно занимает 80 мс, счетчик FPS после этого снова покажет привычное трехзначное значение. Глаз запомнит другое: картинка на мгновение дернулась.
Поэтому при анализе таких проблем полезнее смотреть на frametime, время подготовки каждого отдельного кадра. При стабильных 60 FPS новый кадр должен появляться примерно каждые 16,7 мс, при 120 FPS - каждые 8,3 мс. Один кадр продолжительностью 50 или 100 мс уже создает заметный рывок, хотя за следующую секунду средний FPS может почти восстановиться.
Шейдерная компиляция интересна именно этим. Она может практически не изменить среднюю производительность сцены после завершения работы, но испортить первый момент появления нового материала, эффекта, объекта или варианта освещения.
Из-за этого возникает на первый взгляд странная ситуация. Владелец меняет видеокарту среднего класса на гораздо более быструю, FPS заметно растет, а знакомый рывок возле той же двери остается.
Что такое шейдер и зачем его вообще компилировать
Современный GPU не получает от игры команду вроде "нарисуй мокрый асфальт с отражением фонаря". Видеокарта выполняет специализированные программы, которые определяют, что нужно делать с вершинами, пикселями и другими данными в графическом конвейере.
Такие программы называются шейдерами. Vertex shader обрабатывает вершины геометрии, pixel или fragment shader участвует в вычислении итогового цвета пикселя, compute shader используется для более общих параллельных вычислений. В современном рендерере применяются и другие стадии, особенно если задействована трассировка лучей.
Материал стены при этом не сводится к одному универсальному шейдеру. На код могут влиять нормали, прозрачность, режим смешивания, тени, число и тип источников света, отражения, скелетная анимация, качество эффектов и десятки других условий.
В движке исходный вариант еще не является программой, которую конкретный GPU может непосредственно выполнить. Код проходит несколько стадий подготовки и в итоге должен превратиться в машинное представление, подходящее определенной архитектуре GPU и драйверу.
Именно часть этой работы игрок иногда оплачивает временем кадра.
Почему нельзя один раз скомпилировать шейдер и положить его в игру
У консоли разработчик знает аппаратную платформу заранее. У конкретного поколения PlayStation или Xbox набор конфигураций жестко ограничен, поэтому значительную часть графической подготовки можно выполнить до того, как игра попадет к пользователю.
ПК устроен иначе. Одна копия игры должна работать на GeForce, Radeon и Intel Arc разных поколений, с разными версиями драйверов. Даже две видеокарты одного производителя могут использовать разные архитектуры и требовать разного низкоуровневого кода.
Игра поэтому обычно распространяет шейдеры в форме, которая еще требует обработки на пользовательской системе. Для DirectX это может быть промежуточное представление DXIL, для Vulkan применяется SPIR-V. Дальнейшая работа уже зависит от графического API и драйвера.
Microsoft прямо называет компиляцию шейдеров одной из причин долгого первого запуска и внутриигровых заиканий в описании Advanced Shader Delivery. Компания объясняет проблему тем, что традиционно финальная компиляция зависит от сочетания самой игры, GPU и драйвера.
Отсюда следует неприятный для владельца ПК вывод: наличие готовой игры на диске еще не означает, что абсолютно весь исполняемый графический код для его видеокарты уже существует.

Шейдер и PSO - не совсем одно и то же
Фраза "компиляция шейдеров" удобна, но современная проблема шире.
В Direct3D 12 движок работает с Pipeline State Object, PSO. Такой объект объединяет набор состояний графического конвейера, необходимых для определенного варианта рендеринга. Туда входят используемые шейдеры и параметры, связанные с rasterization, depth/stencil, blending и другими частями pipeline.
Преимущество подхода в том, что драйвер получает заранее описанное состояние вместо множества мелких изменений прямо перед отрисовкой. Недостаток проявляется, если нужная комбинация встретилась впервые и PSO приходится готовить слишком поздно.
Epic в документации Unreal Engine отдельно связывает shader compilation, PSO и framerate hitches. В описании PSO Precaching движок собирает возможные PSO и асинхронно готовит их заранее, чтобы нужное состояние не пришлось создавать в самый неудобный момент.
Именно поэтому в техническом разговоре выражение PSO compilation stutter часто точнее простого "шейдеры компилируются".
Что происходит в кадре, который внезапно занимает 100 мс
Представим, что игрок впервые входит в помещение. Там появляется материал стекла с определенным режимом прозрачности, динамический источник света, частицы пыли и отражение.
Движок начинает формировать команды для GPU и обнаруживает, что соответствующий вариант pipeline еще не готов. Если подготовка не была выполнена заранее и нет подходящего результата в кэше, системе приходится создать его сейчас.
Часть работы выполняется на CPU и внутри драйвера. В этот момент рендер не может продолжить нужную операцию так же быстро, как в обычном кадре. Вместо условных 8 мс кадр занимает 40, 80 или 150 мс.
Следующий кадр снова укладывается в норму.
Внешне это ощущается не как постоянный низкий FPS, а как одиночный толчок. Если подобных новых состояний много, игра начинает подергиваться при перемещении по ранее не посещенным областям.
Почему второй проход по тому же месту часто становится плавным
Результат компиляции можно сохранить.
После первого создания pipeline драйвер или сама игра помещает подготовленные данные в кэш. При следующей встрече с тем же состоянием дорогостоящую работу выполнять заново уже не требуется.
Это дает характерный способ распознавания проблемы. Игрок проходит один участок и замечает рывки. Перезагружает сохранение, идет тем же путем второй раз, и большинство рывков исчезает.
Такое поведение хорошо согласуется с прогревом shader или pipeline cache.
Khronos показывает тот же принцип в документации Vulkan. В официальном примере Pipeline Management создание pipeline без готового кэша занимает больше времени и способно вызвать внезапное увеличение времени кадра. Сохраненный VkPipelineCache позволяет повторно использовать подготовленные данные между запусками приложения.
Это не означает, что любой исчезающий после второго прохода фриз обязательно вызван шейдером. Кэшируются также данные файловой системы, игровые ресурсы и другие компоненты. Но повторяемость "первый раз плохо, второй хорошо" является сильной зацепкой.
Почему после обновления драйвера фризы могут вернуться
Кэш не всегда можно использовать после изменения программной среды.
Скомпилированный результат зависит от драйвера и аппаратной платформы. После установки другой версии драйвера старые бинарные данные могут стать несовместимыми или быть намеренно признаны недействительными.
Тогда игра снова встречает часть состояний впервые.
Именно поэтому хорошо работающая неделями игра иногда начинает подергиваться сразу после обновления драйвера. Пользователь видит прямую временную связь и предполагает, что новый драйвер "снизил производительность", хотя средняя скорость GPU осталась прежней. В ряде случаев система просто заново наполняет кэш.
Похожая ситуация возникает после крупного обновления самой игры. Разработчики изменили материалы, шейдеры или рендерер, старый кэш перестал полностью соответствовать новой версии.
В Unreal Engine bundled PSO caches тоже привязаны к конкретным данным игры. Epic указывает, что значительные изменения контента и шейдеров могут сделать накопленные записи несовместимыми.
Поэтому сравнивать плавность до и сразу после большого обновления нужно осторожно.
Откуда берется экран "Compiling Shaders" перед запуском игры
Некоторые разработчики предпочитают решить проблему грубо, но предсказуемо: заставить пользователя подождать.
Игра запускает подготовку шейдеров до начала игрового процесса. Процессор получает тяжелую вычислительную работу, прогресс идет от нуля до ста процентов, после чего большая часть необходимых вариантов уже находится в кэше.
С точки зрения пользователя старт стал дольше. С точки зрения frametime это хороший обмен. Лучше несколько минут один раз посмотреть на индикатор, чем получать десятки пауз в бою.
Особенно это заметно в играх на современных API, где количество возможных состояний велико.
Но заранее подготовить абсолютно все бывает сложно. Количество вариантов растет из-за комбинаций материалов, эффектов и настроек, а часть состояний зависит от того, что реально появится во время игры.
Поэтому даже после отдельного экрана подготовки редкие shader hitches полностью не исключены.
Почему игру нельзя заставить скомпилировать вообще все возможные варианты
Проблема быстро упирается в комбинаторику.
Допустим, материал поддерживает несколько вариантов освещения, теней, геометрии, качества отражений и эффектов. Каждый переключатель способен создавать дополнительную permutation, то есть отдельный вариант шейдера.
Пять независимых признаков с двумя состояниями уже дают 32 комбинации. Десять - 1024. В реальном движке условий намного больше, хотя разработчики активно сокращают количество бессмысленных вариантов.
Если начать без разбора компилировать каждую теоретически возможную комбинацию, время запуска и размер кэша могут стать неприемлемыми.
Приходится выбирать: подготовить распространенные состояния заранее, остальные компилировать асинхронно либо столкнуться с редкой работой непосредственно во время игры.
На этом месте проблема из "медленной видеокарты" превращается в задачу проектирования движка.
Почему Unreal Engine часто упоминают рядом с shader stutter
Unreal Engine широко используется в современных PC-играх, поэтому его проблемы хорошо заметны статистически. Но сам движок не создает фундаментальную проблему компиляции: с необходимостью подготовки shader и pipeline работают и собственные движки.
Причина репутации Unreal связана с большим числом проектов, динамическими материалами, разнообразием контента и тем, как предыдущие поколения движка собирали PSO.
Epic эту проблему не отрицает. В актуальных версиях UE5 используется PSO precaching: движок пытается определить необходимые графические состояния и запустить их подготовку до момента непосредственной отрисовки.
При этом даже документация Epic признает, что отдельные PSO могут возникать непосредственно во время работы. Разработчикам нужно следить, какие pipeline не удалось подготовить заранее, и закрывать оставшиеся случаи.
То есть Unreal Engine дает инструменты, но сам факт использования UE5 не гарантирует отсутствие shader stutter.

Почему одна игра на Unreal Engine дергается, а другая работает ровно
Движок - фундамент, а не готовая игра.
Студия решает, какие материалы использовать, как собирать PSO, когда запускать precaching, сколько комбинаций существует, что разрешено создавать динамически и как устроены загрузочные экраны.
Можно сделать игру на Unreal и заранее пройти большое количество необходимых pipeline. Можно оставить редкие состояния на runtime и получить фриз в момент первого появления эффекта.
Поэтому фраза "это Unreal, значит будет тормозить" технически слабая. Правильнее оценивать конкретную реализацию.
Разработчик также может исправить shader stutter патчем, не меняя графику и системные требования. Достаточно изменить процедуру precaching или включить в набор ранее пропущенные PSO.
Почему более мощный процессор иногда помогает, но не решает проблему
Компиляция создает нагрузку на CPU. Быстрый процессор способен закончить часть работы быстрее, особенно если система умеет распараллеливать задачи.
Но есть предел.
Если рендер-поток должен дождаться результата определенной компиляции, задержка все равно попадет в frametime. Вместо 100 мс она может стать 50 мс, но это все еще заметный рывок.
Дополнительно часть работы находится в драйвере и зависит от реализации конкретного pipeline.
Поэтому Ryzen 9 или Core Ultra верхнего класса способен уменьшить тяжесть ситуации, но покупка CPU не исправляет архитектурно позднюю подготовку PSO.
Это одна из причин, почему shader stutter встречается на системах, которые по обычным игровым тестам намного превышают рекомендуемые требования.
Почему более быстрая видеокарта помогает еще меньше
Финальный шейдер исполняется на GPU, но фриз возникает во время подготовки к его выполнению.
Можно поставить видеокарту, которая после завершения компиляции рисует сцену со 160 FPS вместо 80 FPS. При этом первый неожиданный PSO по-прежнему требует подготовки.
Получается особенно раздражающий график frametime: между фризами система работает очень быстро, поэтому короткая остановка ощущается еще резче.
Пользователь видит низкую загрузку GPU во время рывка и иногда делает вывод, что видеокарта неисправна. На деле GPU может просто ждать, пока CPU, движок и драйвер подготовят следующие команды.
Высокая производительность GPU не устраняет ожидание работы, которая должна завершиться до начала отрисовки.

