Счетчик кадров врет, и мерить надо совсем другое
Счетчик кадров врет, и мерить надо совсем другое

Средний fps – вещь, которая к реальной плавности геймплея может не иметь никакого отношения. Вообще. Даже если вы собрали топовый ПК и имеете стабильные 180 кадров в счетчике картинка все равно может дергаться. А все потому, что глаз реагирует не на количество кадров, а на равномерность их появления. И вот эту равномерность средний показатель – тот самый AVG – как раз и прячет. Значит, надо смотреть на другие цифры. Но на какие?

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

Что включить, чтобы увидеть проблему

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

Скачиваем, устанавливаем и идем в настройки. Там находим вкладку мониторинга, а там уже находим строку Frametime. Напротив нее ставим галочку, а ниже включаем «Показывать в ОЭД» и меняем тип отображения на график.

Смотреть надо не на средний fps (он-то как раз может быть очень красивым), а на форму линии
Смотреть надо не на средний fps (он-то как раз может быть очень красивым), а на форму линии
  • Ровная полоса с редкими зубцами — все хорошо, можно играть дальше.

  • Частокол — микрофризы. Значит, кадры приходят неравномерно, но средний fps при этом может быть вполне себе высоким.

  • Одиночные пики в потолок — те самые фризы на доли секунды, и каждый такой пик – это замирание картинки, которое мы воспринимаем как слайдшоу.

График хорош тем, что проблему на нем видно сразу и безо всякой математики. А вот чтобы понять ее масштаб и сравнить результаты между собой, понадобятся уже точные замеры.

Тут вариантов два:

  • Первый – CapFrameX, и для большинства задач его хватает с головой. Это программа, которая пишет время каждого кадра в течение всего прогона, а потом сама считает перцентили, строит распределение и показывает, насколько ровно шли кадры. Всего-то и нужно, что запустить запись, отыграть пару минут и остановить, а программа посчитает все сама.

  • Второй вариант – PresentMon от Intel. Он снимает данные прямо из конвейера вывода Windows, то есть максимально близко к железу, и показывает время кадра раздельно по процессору и видеокарте. Штука полезная, потому что сразу видно, кто из них задержал кадр, а значит и куда копать. Правда, интерфейс там куда более спартанский, а результаты придется анализировать самому.

Однако есть нюанс, который легко упустить. Чтобы посчитать 0.1% low, программе нужна большая выборка. Очень большая. Гоняете полминуты — в расчёт попадёт всего пара кадров, и любой случайный фриз перевернёт цифру. Так что гоняйте минуты две, и обязательно на живом геймплее, а не на пустой локации. И лучше пару раз подряд: если результаты пляшут, доверять им ещё рано.

Какие цифры смотреть и что они значат

График графиком, но рано или поздно вы упретесь в цифры. А их в любой из этих программ будет три:

  • Средний fps. Показывает скорость, но не плавность. Полезен только для сравнения настроек между собой.

  • 1% low. Средний результат по одному проценту самых медленных кадров. Ловит регулярные просадки: подгрузку ассетов при перемещении, перегруженные сцены, упор в процессор.

  • 0.1% low. То же самое по одной десятой процента худших. А вот тут уже экстремальные выбросы: компиляция шейдеров, накладные расходы драйвера, влезшее фоновое приложение.

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

Разница между двумя последними, кстати, колоссальная, и это объясняет очень многое. Если игра встала колом на 300 миллисекунд один раз за сессию, в 1% low это не отразится почти никак — один кадр из 10 000 веса в статистике не наберет. Зато в 0.1% low он вылезет во всей красе.

Средний fps у вас может быть отличный, 1% low тоже, а игра все равно будет дергаться
Средний fps у вас может быть отличный, 1% low тоже, а игра все равно будет дергаться

Универсальных порогов тут, кстати, нет и быть не может. Два миллисекунды разброса при 240 кадрах и при 45 — это совершенно разные истории, да и от самой игры многое зависит. Так что смотрите не на абсолютные цифры, а на свои же замеры до и после.

Хотя совсем явные случаи видно и без всяких методик. Средние 120 при 1% low в районе 40 — это разрыв втрое, и тут уже понятно, что с равномерностью беда.

Почему один медленный поток роняет весь кадр

Чтобы понимать, куда смотреть дальше, надо знать, как кадр вообще собирается. А собирают его трое:

  • Игровой поток – он считает логику: тики акторов, физику, анимацию, ввод, поведение ИИ.

  • Поток рендеринга – он превращает сцену в команды для видеокарты: отсечение невидимого, настройка материалов, формирование вызовов отрисовки.

  • Видеокарта – она эти команды исполняет.

В норме все три компонента работают внахлест. GPU рисует кадр, в это время поток рендеринга готовит следующий, а игровой поток просчитывает, что будет после него. Если все происходит именно так, никаких проблем нет. Но ведь бывает и по-другому.

Предположим, игровой поток отработал за 5 мс, рендер завершился за 4, а видеокарта сработала за 15. Сколько в таком случае уйдет на один кадр? Верно, 15. Конвейер-то идет со скоростью самого медленного участка.

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

В играх на Unreal Engine эту разбивку, кстати, можно посмотреть, не отходя от кассы. Открываем консоль, вводим stat unit, и в углу появляются четыре строки:

  • Frame — полное время кадра, от начала симуляции до вывода на экран.

  • Game — сколько занял игровой поток.

  • Draw — сколько занял поток рендеринга.

  • GPU — сколько работала видеокарта.

Дальше ищем, какая из трех последних величин ближе всего к Frame. Нашли? Это и есть боттлнек. 

  • Если это Game 18, Draw 8, GPU 12 — все упирается в игровой поток, и видеокарту менять бессмысленно.

  • Game 8, Draw 6, GPU 20 — все упирается в графику.

Ну а если фризы редкие и в статичных цифрах их не поймать, вводите stat unitgraph. Он покажет то же самое, но в виде графиков, где спайк видно сразу.

Ботлнек - штука неприятная
Ботлнек - штука неприятная

Проверка за две минуты без всяких утилит

Хотя, если честно, до графиков дело может и не дойти. Два самых частых типа фризов разделяются вообще без инструментов, и уходит на это пара минут.

Пробегите один и тот же участок карты в игре дважды и посмотрите, что будет:

  • Дернулось только в первый раз — это компиляция шейдеров. Скомпилированный объект лег в кэш и повторно уже не собирается.

  • Дергается стабильно на одном и том же месте — это подгрузка данных, она же traversal stutter. Границу стриминга вы пересекаете каждый раз, значит и грузиться будет каждый раз.

Для шейдеров, кстати, есть и вторая проверка, понадежнее. Дело в том, что скомпилированные объекты драйвер складывает в собственный кэш на диске, и живет этот кэш между запусками игры. Так вот, если его почистить, все скомпилируется заново.

Найти его можно в настройках драйвера: у NVIDIA это пункт про кэш шейдеров в панели управления, у AMD аналогичная настройка в фирменном приложении. А дальше дело за малым – остается только сбросить кэш и пройти по тому же маршруту еще разок. Фризы вернулись именно там, где были при первом прохождении? Значит, это компиляция, и вопросов больше нет.

Откуда берется компиляция шейдеров

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

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

А ведь всей этой возни могло бы и не быть вовсе
А ведь всей этой возни могло бы и не быть вовсе

Дело в том, что в Direct3D 11 все выглядело иначе. Компиляция там никуда не девалась, просто драйвер прятал ее от вас: команды он получал по отдельности, а окончательно собирал все уже перед самой отрисовкой. А вот в Direct3D 12 и Vulkan решили отдать управление движку. Теперь состояние конвейера надо упаковать в отдельный неизменяемый объект, тот самый Pipeline State Object, и собрать его заранее.

Придумывали PSO как раз для того, чтобы убрать компиляцию из игрового процесса. Идея-то здравая: собери все на загрузке, и в бою компилировать уже ничего не нужно.

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

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

Почему нельзя просто скомпилировать все заранее

Тут вся штука в количестве, и лучше всего это видно по цифрам Epic для Fortnite. За один матч движок компилирует около 30 тысяч объектов состояния, а использует из них примерно 10 тысяч. А всего возможных комбинаций – миллионы.

Собрать такое количество объектов на загрузке нельзя. На это банально не хватит ни времени, ни памяти. А раз так, нужно предсказывать, что именно понадобится, и вот этим как раз занимается precaching (русского аналога у этого термина нет, но суть из названия, думаю, понятна и так), появившийся в Unreal Engine 5.2.

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

Стало ли лучше? Ну, в общем, да. Только система с тех пор прилично выросла и закрывает уже большинство сценариев. Хотя Epic и сами признают: пробелы в покрытии остались, а хитчи никуда не делись. Скажем, глобальные шейдеры постобработки — тени, размытие в движении, отражения — до сих пор способны уронить кадр. А декали и вовсе не покрывались вообще никак до версии 5.6.

Если дергается везде и понемногу

Бывает и так, что четких фризов нет вообще, а картинка все равно постоянно подрагивает. Заодно потрескивает звук, а мышь иногда как будто спотыкается. Причем везде, а не в конкретных местах.

Тут дело обычно в задержках на уровне ядра, и проверяется это утилитой LatencyMon.

Работает она так: запускаете, играете минут пять, а потом смотрите на две строки в отчете. Называются они Highest ISR routine execution time и Highest DPC routine execution time, и рядом с каждой будет имя модуля.

Что это за строчки такие? 

  • Первая — сколько времени провисел обработчик прерывания. Штука эта работает на высоком уровне IRQL и блокирует всё остальное, поэтому Microsoft рекомендует укладываться в 25 микросекунд.

  • Вторая — сколько заняла отложенная обработка, которую драйвер спихнул в очередь DPC. Там норматив помягче, около 100 микросекунд.

А теперь смотрите, что бывает на практике. Задержки от миллисекунды уже дают слышимые щелчки и видимые рывки, хотя тут многое зависит от частоты возникновения. В совсем запущенных случаях встречались 15 726 микросекунд у драйвера NVIDIA и 101 миллисекунда у dxgkrnl.sys — это, если считать, превышение норматива в тысячу раз.

Только вот с именем в отчете есть подвох. Скажем, dxgkrnl.sys — это графическое ядро Windows, и виноват он бывает редко. Куда чаще он просто оказывается тем местом, где вылезает задержка стороннего драйвера. Та же история и с Wdf01000.sys, через который работает половина драйверов в системе.

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

Почему на 240 Гц дергается заметнее, чем на 60

Иногда на высокочастотном мониторе картинка начинает “скакать” чаще, чем на низкочастотном
Иногда на высокочастотном мониторе картинка начинает “скакать” чаще, чем на низкочастотном

Мониторы, кстати, – это отдельная история. Берет, скажем, человек новую панель с высоким разрешением и частотой, чтобы было покрасивее и побыстрее, а получается все наоборот: картинка становится даже хуже, чем была.

Примечательно, однако, что никакого парадокса тут нет. Просто чем выше частота, тем короче кадр, и тем заметнее любое отклонение от ритма. На частоте 60 Гц кадр живет 16,6 миллисекунды, и опоздание на 5 миллисекунд теряется. А вот при 240 Гц кадр длится 4,2 миллисекунды, и если опоздание выходит больше самого кадра, не заметить его нельзя. То есть монитор стал лучше и просто перестал прощать системе ее медлительность.

Казалось бы, тут должен спасать VRR, он же G-Sync, он же FreeSync. Штука и правда полезная, вот только ожидания от нее обычно завышены.

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

Однако неровную выдачу кадров она не чинит. И, если движок отдает кадры вразнобой, монитор этому ритму вторит.

Чтобы проблем не было, лучше держать частоту кадров чуть ниже верхней границы диапазона VRR. На 144 Гц это где-то 138-141 кадр, на 240 — в районе 235. Точное число не так уж и важно, главное не упираться в потолок.

Зачем? Дело в том, что программные ограничители работают не идеально. Выставленные 144 на практике будут скакать в районе 144,2 или даже 145, и каждый такой заскок выталкивает систему за верхнюю границу VRR. А там уже включается обычная вертикальная синхронизация со всеми ее задержками, потом выключается обратно, и вот эти переключения вы и ощущаете как рывки.

С нижней границей та же история. Когда кадров становится меньше рабочего диапазона, включается компенсация низкой частоты: монитор начинает показывать каждый кадр по два или три раза. Скажем, 47 кадров на 144-герцовой панели превратятся в 141 герц. Работает механизм неплохо, но в момент включения и выключения картинка заметно дергается, а на OLED еще и яркость плавает.

Ну и напоследок про режим экрана, потому что тут многие живут устаревшими знаниями. Раньше все было просто: оконный режим без рамки гнал кадры через композитор Windows и добавлял задержку, а полноэкранный его обходил. В современной Windows разница куда меньше, потому что DX12 и оптимизированные игры на DX10 и DX11 работают уже по новой модели вывода. Но если конкретная игра дергается именно в окне, сравнить с полноэкранным все равно стоит.

Что делать с результатом

Ну а теперь собираем все вместе. Виновника вы скорее всего уже нашли, осталось понять, что с ним делать.

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

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

Подгрузка данных. Тут вариантов немного, потому что проблема в основном на стороне игры. Что реально помогает, так это перенос на накопитель побыстрее: если игра лежит на жестком диске, переезд на SSD убирает половину таких фризов. Хотя и тут надо помнить, что у быстрых твердотельников есть свои нюансы с нагревом. Еще стоит снизить настройки дальности прорисовки и качества текстур, потому что чем меньше данных надо подтянуть при пересечении границы, тем короче пауза. А вот все остальное уже вопрос к разработчику.

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

Скачущий фреймтайм без явных фризов. Поставьте ограничение кадров процентов на десять ниже того, что система выдает. Звучит контринтуитивно, но ровные 90 ощущаются лучше, чем скачущие от 100 до 140. Если включен VRR, ограничение обязательно, и ставить его надо на 3-5% ниже частоты монитора.

Упор в один из потоков. Тут универсального рецепта нет, зато есть понимание, куда не надо тратить деньги. Уперлись в игровой поток — новая видеокарта не поможет вообще никак, вопрос к процессору и к самой игре. Уперлись в GPU — снижайте настройки, которые грузят именно графику: разрешение, тени, трассировку. А, если говорить о процессорной части, то на нее еще и память влияет.

Что из всего этого следует

Средний fps показывает скорость, а не плавность. Смотреть надо на фреймтайм и на 0.1% low, потому что редкие выбросы прячутся именно там.

Время кадра определяется самым медленным из трех потоков, а не их суммой. Поэтому и апгрейд помогает через раз: если упор в игровой поток, новая видеокарта не даст ничего.

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

Компиляция — это плата за то, что Direct3D 12 переложил ответственность за состояние конвейера на разработчика. Механизм для решения проблемы существует с 2015 года, а пользоваться им научились совсем недавно.

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

А у вас как? Ловили фризы, смотрели фреймтайм? Расскажите, что в итоге оказалось виновато — особенно интересны случаи, где дело оказалось не в видеокарте.