Стриминг игрового процесса с одной машины на другую достаточно популярен сегодня. Для этого процесса требуются очень низкие задержки — здесь важна каждая миллисекунда. Нам нужно выполнять следующие задачи:
Отправляем ввод контроллера с машины А на машину Б по сети.
Б рендерит кадр на 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-сжатии видео. И это грустно, потому что технология весьма изящна.

Программист графики сразу же узнает эту структуру, потому что это просто старые добрые 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 из-за сниженной внутренней точности.
Заключение
Меня очень радуют результаты, а пользоваться моим созданным с нуля решением для стриминга очень увлекательно.
Комментарии (31)

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

old_bear
31.08.2025 13:38О, прямо
въетнамскиефлешбэки пошли как кадр из parkjoy увидел. Насмотрелся я на него в своё время при отладке AVC-Intra кодека для ПЛИС...
Само кодирование, кстати, вполне себе реализуемо аппаратно для AVC-Intra или HEVC-Intra с потоком 100-200 мегабит в секунду и с микроскопической задержкой в 16-32 строки пикселей за счёт блочной структуры кодирования. Но вот выбрать правильную стеиень квантования чтобы уложиться в CBR, не зная заранее содержимое всего кадра - это из серии гадания по хрустальному шару. Так то по предыдущему кадру можно статистику набрать, но при резкой смене плана это не помогает. У нас даже шутка была, что каждый сотрудник компании за время работы придумывает как минимум один новый алгоритм контроля битрейта.
graphican
31.08.2025 13:38Блоки 32х32. Сжимаю jpeg и zlib со средним сжатием. Помечаю, что занимает меньше. Объединяю в макроблоки - прямоугольные области с только jpeg и только zlib кодированием - пережимается макроблок целиком. Работает в реальном времени с приемлемым потоком. Степень сжатия регулируется только для jpeg. Если в полёте больше чем 3 кадра - сжимаем сильнее. Если 3 и меньше на протяжении секунды - немного уменьшаем сжатие до тех пор, пока не превысим 3 кадра в полёте. Это в aeroadmin реализация. Удаленное администрирование требует минимальных задержек.
Все операции многопоточно - минимум задержка

TimurZhoraev
31.08.2025 13:38Имеются ли артефакты на границах блоков? Сжимается только покадровое изменение картины? В библиотеках самого jpeg вроде как имеется уже возможность поиграть с размером блоков DCT (lossy), а затем ещё поднастроить результат квантования (lossless).

graphican
31.08.2025 13:38Сжатие только покадрово. Артефакты блоков не видно. Виден муар от сжатия текста jpeg. Но как-только появляется возможность, то сильно сжатые блоки обновляются с меньшим сжатием и артефакты исчезают.
В jpeg только качество выставляется и цветовое прореживание.
Нет необходимости прям точно в какой-то размер вписываться

TimurZhoraev
31.08.2025 13:38Кстати вроде как подобный метод с переменным сжатием используется в DJVU. Там получается так что делаются из изображения слои, каждый слой это свой пространственный фильтр низкой частоты грубо говоря (например, усреднение по соседним точкам). Как только производная от изменения яркости становится большой (или в спектре появляются ВЧ-составляющие) применяется уже более мелкое сжатие локальной области. То есть сжимаются уже не исходная картина, а разбитая на эти слои, потом они снова складываются-восстанавливаются.

TimurZhoraev
31.08.2025 13:38В ещё стародавнее время в мультиплее можно было передавать изображение игры практически мгновенно, создавая двоичный поток об этой сцене по коаксиалу 10 мбит, в качестве проигрывателя - движок самой игры, там уже и текстуры есть и всё что нужно, достаточно представлять координаты и дерево объектов.
Гипотетически, можно байткод, который и формирует это дело между GPU<->CPU перехватывать и уже отправлять непосредственно его, включая физику, а далее его восстанавливать на GPU-приёмнике, наверняка подобного рода технологию рано или поздно завезут, так как GPU в облаке как сервис и виртуальные видеокарты прямо говорят о том что это необходимо сделать, в этом случае можно рендерить хоть 8к, разумеется, для нативных фильмовых сцен это дело не подойдёт а для геймплея вполне.
me21
31.08.2025 13:38Примерно так же в Линуксе работал графический сервер иксов по сети, отправлялись команды типа "отрисовать окно", "отрисовать кнопку", "вывести текст". В результате программа работала на одном компьютере, а её интерфейс - на другом. Но сейчас от этого ушли...

TimurZhoraev
31.08.2025 13:38Ну почему же, если в качестве байткода вызывать методы того же Qt. Более того в новой почти полнофункциональной операционной системе называемой нынче "браузер" рендерить часть веб-контента AJAX-ом, то фактически эти самые JSON-XML-подобные пакеты и есть некая имитация иксов, концепт которых был ещё в середине 90-х. Можно образно сказать что фронтенд - это имитация рабочего стола и прочих элементов GUI, созданных на бекенде. А там хоть Ncurses или Turbo Vision по сети, но это уже более ранняя история.

vivan
31.08.2025 13:38А в чем смысл? Для рендеринга потребуется такой же мощный GPU, как и в локальном варианте. Нужны текстуры, значит нужна вся игра локально (за минусом бинарника). Сэкономить CPU?

sappience
31.08.2025 13:38Ну что вы, это же ворота в дивный новый мир. В котором нет спираченных игр, крякнутых, хакнутых, с читкодами. В котором никто не отреверсинженирит твою игру и не позаимствует решения. В котором пользователи не смогут заплатив за игру однажды играть потом, собаки такие, всю жизнь. И наигравшись перепродать игру не смогут. Пользователь вообще доступа к коду лишен. Он может только платить и получать видеопоток. Идеальный потребитель!

TimurZhoraev
31.08.2025 13:38Именно так! Пользователь освобождается от бремени переноса к себе бинарников геймплея и физики, которые лежат на сервере а на его стороне лишь тонкий клиент с GPU для отрисовки. Аналогично все САПР-ы. Мало того, там автоматом виртуальная флешка с ключами привязанные к ГлобалУслугам (эту тему кстати не только у нас курируют, взять те же корневые сертификаты) с биометрическим ID. Ну и соответственно трафик может быть для векторной графики из разряда US Robotics 56k

ZirakZigil
31.08.2025 13:38И какие от этого плюсы для пользователя? Текстуры, звуки, катсцены, модели, тексты и прочее это и есть 99% объёма игры. Сэкономить на паре десятков мегабайт бинарей в игре на 100 гигабайт?

TimurZhoraev
31.08.2025 13:38Для антарктической экспедиции, когда охота поиграть с друзьями на другом континенте а видос через исчезающий Старлинк или другой спутник на высоких широтах уже не прогнать через
.

ZirakZigil
31.08.2025 13:38И поэтому вместо таскания по сети только пакетов, нужных для мультиплеера, мы будем таскать те же пакеты для мультиплеера и к ним ещё пакеты, описывающие кадры? Звучит заманчиво (нет).

vivan
31.08.2025 13:38Такое давно решается серверной логикой для ключевых механик. F2P часто такие, в том числе и синглплеерные.

TimurZhoraev
31.08.2025 13:38Всё верно, речь идёт не о сервер-рендеринге а именно об этом действии на машине пользователя, которая выступает в качестве визуализатора, содержит терабайты текстур, демок, интро итд а по факту принимает поток графа сцены, который уже можно протолкнуть через узкополосные или падающие каналы. При этом, действительно, механика и прочее поведенческое "ноу-хау" (чтобы скрыть от читер-крякеров) из разряда ИИ, отрабатывается на игровом сервере. Вообщем это что то среднее между тонким клиентом и сервером.

TimurZhoraev
31.08.2025 13:38А если геймплей охота глянуть через спутник, ASDL, диалап, пусть даже и спустя сотню миллисекунд, может там MMORPG 8k, такая где нужно увидеть на экране пиксель чтобы разгадать где спрятался юнит, сжатие без потерь не пойдёт, канал не позволит, с потерями - будет один большой блюр на весь экран с косинусными артефактами. То есть это потенциально индустриальная задача, когда разработчики игры, CAE/CAD/CAM могут движением мыши сгенерировать легковесный вьюер без игрового движка (99.9 это текстуры и полигоны), чтобы можно было смотреть на удалёнке что происходит, делать туторы и классрумы под сотню человек не напрягая связь. Причём это может быть универсальное решение в виде некой стандартной либы под CPU-GPU, что-то как раз из разряда x11-ов и Wayland-а, только более адаптировано под эту задачу, включающую обработку таймаутов, лагов и непрорисовок (пока что эти интерфейсы подразумевают идеальную связь)
n0isy
Приветствую! Хорошая техническая статья. А можно для неспециалиста пояснить: мы жмём исходный поток во сколько раз? Если судить о 1.4гб/200Мб, то это x7 ?
Какие сферы применения вы предполагаете? Облачный гейминг? (Большеват все таки бюджет по полосе).
old_bear
Плашка в начале статьи намекает что это перевод...
Torvald3d
Вряд ли облачный с таким битрейтом, скорее просто удаленный дисплей по локалке, типа ПК в одной комнате, а девайс с экраном в другой. Например, стриминг с ПК на стимдек, телевизор или виар очки