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

  • Отправляем ввод контроллера с машины А на машину Б по сети

  • Б рендерит кадр на GPU

  • Б кодирует кадр в битовый поток

  • Б отправляет результат по сети машине А

  • A декодирует битовый поток

  • A отображает изображение на экране

  • В мозге цели высвобождается дофамин

Каждый этап в этой цепочке повышает задержки, а нам нужно их как можно сильнее минимизировать. Обычно в качестве решения используется ускоренное GPU сжатие видео при помощи какого-нибудь кодека, обычно H.264, HEVC или, если хотите заморочиться, AV1. В идеале весь процесс должен выполняться примерно за 20 мс.

Чтобы эта система хорошо работала, нам нужно достаточно серьёзно приструнить кодеки. Современные видеокодеки любят задержки, потому они позволяют реализовывать такие трюки, как гибкое управление битрейтом и B-кадры. Именно это позволяет таким кодекам обеспечивать невероятные коэффициенты сжатия, но в нашей системе от всех этих трюков придётся избавиться. Нельзя, чтобы кодек повышал задержки, а когда мы имеем дело с фиксированным бюджетом битрейта (Ethernet-кабелем или WiFi-антенной), у нас остаются следующие варианты:

  • Жёстко ограниченный сверху битрейт

    • Нет буфера, благодаря которому можно было бы применять переменный битрейт

  • Бесконечные P-кадры GOP или intra-refresh

    • Каждый из этих вариантов по-своему обрабатывает утерю пакетов

При стриминге игры ожидается наличие широкого канала связи. В частности, при стриминге в локальной сети канал практически бесконечен. Гигабитный Ethernet — это древняя технология, а сотни мегабит через WiFi — тоже не проблема. На мой взгляд, это немного меняет приоритеты.

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

Избавляемся от прогнозирования движения, intra-only

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

  • Превосходную устойчивость к ошибкам

    • Даже при локальном стриминге по WiFi на мобильном устройстве это всё равно очень важно (по крайней мере, исходя из моего опыта)

  • Простота

  • Постоянство качества

    • При использовании постоянного битрейта (CBR) качество видео сильно зависит от того, насколько хорошо будет справляться прогнозирование движения

Кодирование с использованием только intra-кадров (intra-only) используется в цифровой кинематографии (motion JPEG2000) и а профессиональных областях применения, в которых эти аспекты важнее, чем объём трафика. Теперь мы работаем со скоростями от 100 Мбит/с вместо 10-20 Мбит/с, поэтому стриминг через Интернет больше невозможен, за исключением peer-to-peer с оптоволоконными каналами. Для справки: сырой 1080p60 с цветовой субдискретизацией (chroma subsampling) 420 требует порядка 1,5 Гбит/с, и с увеличением параметров ситуация становится только хуже.

Избавляемся от энтропийного кодирования

Энтропийное кодирование — кошмар для параллелизации: кодирование на одних только GPU с compute-шейдерами становится чрезвычайно трудоёмкой задачей. Давайте просто избавимся от него и посмотрим, чего мы сможем достичь. Нам нужна скорость!

Для такой ситуации тоже существуют кодеки, но очень специализированные. В сфере профессионального широковещания есть кодеки, предназначенные для проталкивания данных по готовой инфраструктуре с «нулевыми» задержками и минимальной нагрузкой на оборудование. Например, этому была посвящена моя дипломная работа. Более близким к потребительскому рынку примером может быть сжатие потока дисплеев VESA (не знаю, используется ли там энтропийное кодирование, но коэффициенты сжатия настолько малы, что я сомневаюсь в этом). В этой сфере не так много готового ПО, обычно всё реализуется в крошечных ASIC. Если технологию не поддерживает FFmpeg, то для простых смертных она, по сути, не существует.

Дискретные вейвлет-преобразования

Все современные кодеки — это дискретное косинусное преобразование (Discrete Cosine Transform, DCT) плюс миллион хаков поверх него, чтобы всё выглядело красиво. Но существует альтернатива, которой в 90-х попытались заменить DCT. Достаточно неплохое её объяснение можно посмотреть в этом видео: https://www.youtube.com/watch?v=UGXeRx0Tic4. Сегодня сжатие на основе дискретных вейвлет-преобразований (DWT) имеет свою нишу использования, но только в intra-сжатии видео. И это грустно, потому что технология весьма изящна: https://en.wikipedia.org/wiki/Discrete_wavelet_transform

Программист графики сразу же узнает эту структуру, потому что это просто старые добрые mip-текстуры с добавлением специй. По сути, мы выполняем даунсэмплинг изображений и вычисляем «погрешность» между изображениями высокого и низкого разрешения. После обработки N пикселей мы получаем N / 2 низкочастотных и N / 2 высокочастотных пикселей. Спроектированные для этой работы фильтры крайне специфичны (я не знаю, как их пишут, да мне это и не интересно), но по своей сути это простое ядро свёртки. Количество уровней может варьироваться; я выбрал пять уровней декомпозиции.

После того, как изображение фильтруется на различные полосы, значения дискретизируются. Дискретизация вейвлетов — довольно хитрый процесс, потому что нужно учитывать, что в процессе реконструкции фильтры имеют разные коэффициенты усиления. В случае фильтра CDF 9/7 высокие частоты ослабляются на 6 дБ и существуют другие эффекты при апсэмплинге полос низкого разрешения (вставка нулей). Я не стал заморачиваться с графиками и просто вставил их из своей дипломной. CDF 9/7 имеет спектр, очень схожий с использованным мной 5/3.

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

Классические артефакты вейвлетов

Всем знают о печально известных блочных артефактах JPEG. Типичный недостаток вейвлетов — дискретизация высокочастотной информации в 0, даже если этого не должно быть. Это приводит к размытию, а в особо тяжёлых случаях — и к кольцеобразным артефактам. Но учитывая то, насколько размытыми оказываются сегодня игры из-за TAA, возможно, это вообще будет незаметно?

 Современные проблемы требуют современных решений.

Очень быстрая упаковка коэффициентов в блоки

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

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

Блок 32×32 разбивается на блоки 8×8, которые затем разбиваются на блоки 4×2. Этот дизайн оптимизирован под иерархию потоков GPU:

  • 1 поток: коэффициенты 4×2

  • Кластер потоков (подгруппа): блок 8×8

  • Рабочая группа: блок 32×32 (128 потоков)

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

Как и в случае с большинством вейвлет-кодеков, я выбрал кодирование битовых плоскостей, но вместо применения очень сложной (и ужасно медленной) схемы энтропийного кодирования битовые плоскости просто передаются в сыром виде. Я уже делал такое для своего проекта дипломной, и это оказалось на удивление эффективным. Количество битов на коэффициент передаётся на уровне блока 4×2. Я провёл эксперименты с этими размерами блоков, и 4×2 оказался самым подходящим. Благодаря использованию операций с подгруппами и префиксных сумм в рабочей группе декодирование и кодирование данных выполняется очень быстро. Для ненулевых коэффициентов биты знаков плотно упаковываются в конец блока 32×32. Реализовать всё это было умеренно сложно, но особых трудностей не возникло.

Подробности можно посмотреть в моём наброске битового потока.

Точный и быстрый контроль битрейта

При таком стиле сжатия крайне важен контроль битрейта. У нас есть фиксированный (хотя и огромный) бюджет, в который нужно умещаться. Большинству видеокодеков с трудом удаётся выполнять это требование, потому что количество битов, извлекаемое из энтропийного кодирования, узнать до завершения сжатия не так просто. Обычно при работе с обычными ограничениями VBR у кодеков есть достаточно большой запас. Если на кадр пришлось 30% лишних данных, мы можем без проблем амортизировать это в течение нескольких кадров, но в данном случае этого запаса не существует, потому что мы предполагаем нулевые задержки буферизации.

Без энтропийного кодирования задачу можно сделать тривиальной. Для каждого блока 32×32 я проверяю, что произойдёт, если я выброшу 1, 2, 3, … битов. Замеряю психовизуально взвешенную погрешность (MSE) и битовые затраты на неё, а затем сохраняю в буфер на будущее.

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

В последнем проходе каждый блок 32×32 проверяет, сколько битов нужно выбросить, и упаковывает окончательный битовый поток в буфер. Результат будет гарантированно находиться в пределах заданного нами битрейта, обычно на 10-20 байтов ниже целевого значения.

Возможность подобного ограничения битрейта — сильное место многих вейвлет-кодеков. Большинство из них итеративно обходит от самой значимой до самой малозначимой битовой плоскости и может прекратить кодирование при достижении нужного предела битрейта. Это здорово, но работает всё это ужасно медленно…

Надо двигаться гораздо быстрее 

Так быстра ли моя система? Мне кажется, да. Ниже представлено кодирование и декодирование 1080p 4:2:0 игры Expedition 33, которая, как оказалось, представляет собой сложную задачу для сжатия изображений. Кучу зелёной растительности и TAA-шума довольно трудно закодировать.

0,13 мс на RX 9070 XT с RADV. Декодирование тоже выполняется очень быстро, меньше, чем за 100 микросекунд. Не думаю, что какой-то другой кодек хотя бы близок к этому. Проход DWT был достаточно сильно оптимизирован. Это один из тех случаев, в которых я выяснил, что математика упакованных FP16 на самом деле сильно помогает даже на монстре наподобие RDNA4. Проход дискретизатора выполняет основную работу во всех проходах; я приложил усилия и к его оптимизации. Впрочем, выполнение DWT в FP16 отрицательно влияет на максимально достижимые нами метрики качества.

При кодировании более «нормальных» игр этап дискретизации + анализа был нагружен гораздо меньше. 80 микросекунд на кодирование — это довольно неплохо.

Вот кодирование 4K в 4:2:0 печально известной сцены ParkJoy.

0,25 мс — это показывает, что 1080p с трудом удаётся полностью занять GPU. Интересный пример: передача изображения 4K RGBA8 по шине PCI-e намного медленнее, чем подобное его сжатие на GPU с последующим копированием сжатой полезной нагрузки.

Думаю, это на порядок величин быстрее, чем даже специализированные аппаратные кодеки на GPU. Это улучшение производительности напрямую преобразуется в снижение задержек (меньше времени на кодирование и декодирование), так что, возможно, я нашёл что-то интересное.

Энергопотребление на Steam Deck при декодировании тоже едва можно замерить. Не знаю точно, меньше ли оно, чем у аппаратного видеодекодера, но не удивлюсь, если это так.

Сравнение качества

Учитывая то, насколько нишевый и эзотерический этот кодек, сложно найти ему конкурента для сравнения. В сфере стриминга игр единственная реальная альтернатива — это сравнение с кодировщиками производителей GPU с кодеками H.264/HEVC/AV1 в FFmpeg. Очевидно, что стоит протестировать NVENC. VAAPI — тоже вариант, но, как минимум, реализация VAAPI в FFmpeg не обеспечивает целевых показателей CBR, а это жульничество и не подходит в данном конкретном сценарии использования. Возможно, я настроил его как-то не так, но не буду пытаться его отлаживать.

При 200 Мбит/с и 60 fps мне сложно рассмотреть какие-либо артефакты сжатия без параллельного сравнения + увеличения. Для столь простого кодека это достаточно неплохое достижение. Для объективных метрик я использовал https://github.com/psy-ex/metrics.

Даже начинать сравнивать подобный тривиальный кодек с такими кодеками немного глупо, но мы можем немного выровнять баланс сил, наложив на кодеки те же самые жёсткие ограничения, что и у PyroWave:

  • Intra-only

  • CBR с жёстко ограниченным сверху битрейтом

    • Кодировщику не даётся никакого запаса, что должно сильно усложнить контроль битрейта

  • Самые быстрые режимы (не уверен, что это особо важно для intra-only)

Пример командной строки:

ffmpeg -y -i input.y4m -b:v 100000k -c:v hevc_nvenc -preset p1 -tune ull -g 1 -rc cbr -bufsize 1667k out.mkv

Никто в здравом уме не будет выполнять стриминг таким образом, но давайте всё равно попробуем. 

Видеоклипы из игр — это пятисекундные клипы, которые я захватил сам в сырое видео NV12. Не думаю, что будет особо полезно загружать их. Мои написанные на коленке скрипты для генерации графиков можно найти здесь: https://github.com/Themaister/pyrowave/blob/master/metrics/triage.sh.

Тесты NVENC я проводил на RTX 4070 с драйверами 575.

ParkJoy

Я добавил эту сцену в тесты в качестве отправной точки, потому что она навсегда прописалась в голове каждого видеоинженера (наверно)… Клип записан в 50 fps, но поскольку мой тестовый скрипт написан для 1080p60, я изменил .y4m в шестнадцатеричном редакторе.

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

… VMAF, ты что, пьяный?

Возвращаемся к реальности.

Более типичные метрики выглядят похожими на то, что я ожидал. XPSNR должен быть взвешенным PSNR, учитывающим психовизуальные эффекты, но я понятия не имею, хорошая ли это объективная метрика.

Expedition 33

Эту игру довольно сложно кодировать. На самом деле, эта игра стала моим последним стимулом к созданию кодека, потому что даже при 50 Мбит/с с прогнозированием движения некоторые сцены вызывали у кодировщика серьёзные проблемы. По какой-то причине при достижении максимума битрейта некоторые области не перерисовывались правильно.

Не знаю, почему, но VMAF очень любит PyroWave.

Stellar Blade

Её оказалось на удивление легко кодировать. Возможно, дело в размытых фонах.

Использование FP16 ограничивает максимальные показатели PSNR.

Street Fighter 6

Думаю, эта сцена — хороший аргумент в пользу 4:4:4…

FF VII Rebirth

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

В таких сценариях использования VMAF похож на шуточную метрику…

Ещё один пример спрямления PSNR из-за сниженной внутренней точности.

Заключение

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

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


  1. n0isy
    31.08.2025 13:38

    Приветствую! Хорошая техническая статья. А можно для неспециалиста пояснить: мы жмём исходный поток во сколько раз? Если судить о 1.4гб/200Мб, то это x7 ?
    Какие сферы применения вы предполагаете? Облачный гейминг? (Большеват все таки бюджет по полосе).


    1. old_bear
      31.08.2025 13:38

      Плашка в начале статьи намекает что это перевод...


  1. n0isy
    31.08.2025 13:38

    И интересно, что "классическое видео" ParkJoy вообще нигде не выложено в открытый доступ. В отличие от Лены jpg. Где его можно глянуть?


    1. Aelliari
      31.08.2025 13:38

      Тут, но где его брал автор - я не знаю. По кадру из статьи вроде это оно


      1. n0isy
        31.08.2025 13:38

        Llm агент только после ресерча в интернете говорит о ftp и/или регистрации на шведском ресурсе. По self knowledge оно не знает что такое ParkJoy.



  1. old_bear
    31.08.2025 13:38

    О, прямо въетнамские флешбэки пошли как кадр из parkjoy увидел. Насмотрелся я на него в своё время при отладке AVC-Intra кодека для ПЛИС...
    Само кодирование, кстати, вполне себе реализуемо аппаратно для AVC-Intra или HEVC-Intra с потоком 100-200 мегабит в секунду и с микроскопической задержкой в 16-32 строки пикселей за счёт блочной структуры кодирования. Но вот выбрать правильную стеиень квантования чтобы уложиться в CBR, не зная заранее содержимое всего кадра - это из серии гадания по хрустальному шару. Так то по предыдущему кадру можно статистику набрать, но при резкой смене плана это не помогает. У нас даже шутка была, что каждый сотрудник компании за время работы придумывает как минимум один новый алгоритм контроля битрейта.


  1. TimurZhoraev
    31.08.2025 13:38

    В ещё стародавнее время в мультиплее можно было передавать изображение игры практически мгновенно, создавая двоичный поток об этой сцене по коаксиалу 10 мбит, в качестве проигрывателя - движок самой игры, там уже и текстуры есть и всё что нужно, достаточно представлять координаты и дерево объектов.
    Гипотетически, можно байткод, который и формирует это дело между GPU<->CPU перехватывать и уже отправлять непосредственно его, включая физику, а далее его восстанавливать на GPU-приёмнике, наверняка подобного рода технологию рано или поздно завезут, так как GPU в облаке как сервис и виртуальные видеокарты прямо говорят о том что это необходимо сделать, в этом случае можно рендерить хоть 8к, разумеется, для нативных фильмовых сцен это дело не подойдёт а для геймплея вполне.