Привет! Я Артур Валиев. Мы с командой EvertyDesk размышляем над экспериментальным режимом для игрового стриминга и хотим обсудить не очередной ползунок качества, не новый пресет битрейта и не надпись Ultra Low Latency на главном экране.

Нас интересует более странный вопрос:

Обязан ли весь кадр принадлежать одному и тому же моменту времени?

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

Но для пользователя кадр не является неделимым объектом.

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

Так появилась гипотеза EVRT.perceptual_latency: задержку можно рассматривать не как одно число для всего экрана, а как поле, распределённое по изображению.

Это пока не готовая технология и не обещание победить физику. Скорее, это попытка честнее распорядиться тем временем, которое сеть всё равно у нас отнимает.

FPS измеряет поток кадров, но не ощущение контроля

Начнём с привычной ловушки.

Если система выдаёт 60 кадров в секунду, интервал между кадрами равен:

T_{\text{frame}}=\frac{1000}{FPS}.

Для 60 FPS получаем:

 T_{\text{frame}}=\frac{1000}{60}\approx16{,}67\ \text{мс}.

Для 30 FPS:

 T_{\text{frame}}=\frac{1000}{30}\approx33{,}33\ \text{мс}.

Из этого легко сделать слишком прямой вывод: удвоили частоту кадров — вдвое уменьшили задержку.

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

Полная задержка интерактивной системы складывается из множества этапов:

L_{\text{total}}= L_{\text{input}}+ L_{\text{capture}}+ L_{\text{render}}+ L_{\text{encode}}+ L_{\text{queue}}+ L_{\text{network}}+ L_{\text{decode}}+ L_{\text{compose}}+ L_{\text{scanout}}.

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

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

Сервер может честно рендерить 60 FPS, но очередь кодировщика способна добавить десятки миллисекунд. Клиент может декодировать 60 кадров, но показывать каждый второй. Дисплей может работать на 120 Гц, хотя достоверный серверный кадр приходит раз в 33 мс.

Поэтому возможны парадоксальные режимы:

  • 60 FPS с длинной очередью ощущаются тяжелее, чем 30 FPS с коротким конвейером;

  • локальный курсор поверх старого видеопотока реагирует быстрее, чем весь остальной мир;

  • дисплей обновляется часто, но новых сведений о состоянии игры почти не получает;

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

FPS описывает производительность потока, а не всю причинную цепочку между рукой и изображением.

У пикселя тоже есть возраст

Удалённое изображение всегда приходит из прошлого. Пока кадр кодируется и летит по сети, мир продолжает меняться.

Это нельзя отменить, но можно измерить.

Назовём возрастом точки изображения разницу между моментом показа и моментом состояния мира, которое эта точка отображает:

 Age(x,y,t)=t-t_{\text{truth}}(x,y).

«Возраст» данных в точке изображения с координатами ((x, y)) в момент времени (t) определяется как разница между текущим временем и моментом, когда отображаемое состояние этой точки соответствовало реальности. Чем больше это значение, тем сильнее информация на экране устарела.

В обычном видео стриминге мы мысленно считаем возраст почти равномерным:

Age(x,y,t)\approx L_{\text{uniform}}.

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

Центр, края, прицел, фон и интерфейс получают приблизительно один временной срез. Если кадр опоздал на 50 мс, то на 50 мс опоздало всё.

Условно это выглядит так:

Центр:    50 мс
Края:     50 мс
Прицел:   50 мс
HUD:      50 мс
Фон:      50 мс

Но уже сегодня эта равномерность часто нарушается.

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

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

В эксперименте можно сделать различие явным:

Локальный ввод:      0–5 мс
Область прицела:    10–18 мс
Центр внимания:     12–25 мс
Средняя зона:       25–40 мс
Периферия:          50–90 мс
Статичный фон:      иногда ещё больше

Тогда возраст становится не константой, а функцией координат, времени и контекста:

 Age=Age(x,y,t,\text{context}).

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

Самая простая геометрическая модель связывает возраст с расстоянием до центра внимания:

 r=\sqrt{(x-c_x)^2+(y-c_y)^2}.

Величина (r) — это расстояние от точки изображения с координатами ((x, y)) до центра ((c_x, c_y)). Она показывает, насколько далеко рассматриваемая точка находится от выбранного центра кадра.

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

Однако одной геометрии недостаточно.

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

Важность — это не место, а вероятность последствий

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

Сгенерированная картинка - как хороший пример
Сгенерированная картинка - как хороший пример
 P_i= w_AA_i+ w_MM_i+ w_UU_i+ w_TT_i+ w_GG_i+ w_SS_i, \qquad P_i\in[0,1].

Итоговая оценка (P_i) для объекта (i) рассчитывается как взвешенная сумма шести показателей: (A_i), (M_i), (U_i), (T_i), (G_i) и (S_i). Коэффициенты (w_A), (w_M), (w_U), (w_T), (w_G) и (w_S) определяют вклад каждого показателя в общий результат: чем больше вес, тем сильнее соответствующий фактор влияет на оценку. Полученное значение нормируется в диапазоне от 0 до 1.

Здесь:

  • A_i — вероятность внимания;

  • M_i — интенсивность движения;

  • U_i — важность элемента интерфейса;

  • T_i — наличие цели или критического объекта;

  • G_i — вероятность скорого перевода взгляда;

  • S_i — неожиданность изменения сцены;

  • w — веса соответствующих факторов.

Чем выше P_i, тем меньше должен быть допустимый возраст.

Одну из возможных зависимостей можно записать так:

AgeLimit_i= a_{\min}+ (a_{\max}-a_{\min})(1-P_i)^{\gamma}.

Допустимый возраст данных для объекта (i) определяется его приоритетом (P_i). Чем выше приоритет, тем ближе предел к минимальному значению (a_{\min}); чем ниже приоритет, тем ближе он к (a_{\max}). Параметр (\gamma) задаёт нелинейность этой зависимости и определяет, насколько резко меняется допустимый возраст при изменении приоритета.

Пусть минимально допустимый возраст равен 12 мс, максимальный — 80 мс, а:

\gamma=2.

Для критической цели с приоритетом:

P=0{,}95

получаем:

AgeLimit= 12+68(1-0{,}95)^2 \approx12{,}17\ \text{мс}.

Для почти неподвижной стены с:

P=0{,}2: -->AgeLimit= 12+68(1-0{,}2)^2 \approx55{,}52\ \text{мс}.

Формула выражает простую мысль:

Чем важнее объект, тем свежее должны быть данные о нём. Для критической цели допустимая задержка составляет около 12 мс. Для почти неподвижной стены — около 56 мс. То есть важные объекты нужно обновлять чаще, а фоновые можно реже. Цель должна быть почти настоящей, а стена может немного подождать.

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

Но сама постановка меняет цель оптимизации: мы перестаём одинаково дорого обновлять всё изображение и начинаем распределять свежесть по последствиям ошибки.

Старое изображение допустимо только при уверенности

Есть важное ограничение.

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

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

Введём уверенность реконструкции:

C_i\in[0,1].

При -

C_i=1

локальная репроекция считается надёжной. При -

C_i=0

использовать старый фрагмент опасно.

Тогда дополнительный допустимый возраст можно ограничить выражением:

ExtraAge_i= (a_{\max}-a_{\min})(1-P_i)^\gamma C_i,

Дополнительная задержка для объекта зависит от его приоритета и коэффициента (C_i). Чем ниже приоритет объекта, тем большую задержку можно ему добавить. Коэффициент (C_i) регулирует, насколько сильно это правило применяется к конкретному объекту.

а общий предел записать как:

AgeLimit_i=a_{\min}+ExtraAge_i.

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

Здесь принципиально важно направление зависимости.

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

При раскрытии ранее скрытой области, появлении нового объекта, резком изменении контраста или быстром независимом движении требуется свежая коррекция:

C_i<C_{\text{critical}} \quad\Rightarrow\quad \text{force correction}.

Если коэффициент (C_i) падает ниже критического значения, система принудительно запускает коррекцию.

Это место, где красивая идея сталкивается с причинностью.

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

Кадр как визуальный долг

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

Мне нейросети помогли визуализировать идею, это ссылка исходник html, а тут ссылка с крутой визуализацией где можно покрутить ручки, опубликовано на github.io, своих ссылок не вставлял.

visual percetual latency
visual percetual latency

Каждая область что-то должна пользователю:

  • фон должен точную текстуру;

  • противник — правильную позицию;

  • вспышка — своевременное появление;

  • прицел — прямую связь с рукой;

  • интерфейс — понятное подтверждение действия.

Но срочность этих долгов различна.

Традиционный видеопоток старается погасить все долги одновременно: ждёт новый цельный кадр и заменяет им старый.

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

Кадр перестаёт быть фотографией единственного момента и становится договором между:

  • последней известной истиной;

  • свежим пользовательским вводом;

  • локальным предсказанием;

  • сетью;

  • серверной коррекцией.

Такая формулировка звучит философски, но приводит к вполне инженерным решениям.

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

Вопрос уже не только в том, насколько стар весь экран, а в том:

В каком порядке система возвращает пользователю причинно важные части настоящего?

Пример первый: свежий курсор над старым миром

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

Если клиент просто ждёт следующую достоверную картинку, экран ещё десятки миллисекунд показывает старое направление, и управление ощущается вязким.

Вместо ожидания можно взять последний кадр и применить преобразование, основанное на самом свежем вводе:

 I_{\text{warp}}(x,y,t)= I_{\text{last}}\bigl(W(x,y,\Delta u)\bigr),

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

где:

  • последний кадр — последний полный кадр;

  • изменение ввода — изменение пользовательского ввода;

  • деформация — функция деформации координат.

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

Warp не создаёт новую истину. Он лишь заставляет старое изображение быстрее отвечать на новое движение.

Поэтому полезно разделить два прицела.

Первый — локальный, непосредственно связанный с положением указателя:

Crosshair_{\text{input}}(t)=Pointer(t).

Второй соответствует более свежей, но всё же задержанной серверной области:

Crosshair_{\text{focus}}(t)=Pointer(t-a_f).

Расстояние между ними показывает накопленный визуальный долг управления:

 D_{\text{aim}}\approx |v|a_f.

Если курсор движется со скоростью 0,8 пикселя в миллисекунду, а возраст focus-зоны равен 15 мс, расстояние составит:

 D_{\text{aim}}\approx0{,}8\cdot15=12\ \text{pix}.

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

Расстояние между ними зависит от скорости движения курсора и задержки focus-зоны. Например, при скорости 0,8 пикселя в миллисекунду и задержке 15 мс расхождение составит около 12 пикселей.

Локальный прицел говорит:

Клиент получил движение руки.

Серверный прицел говорит:

Вот положение, согласованное с более свежей игровой истиной.

Первый не должен притворяться подтверждённым попаданием, но может снять ощущение, будто между рукой и экраном находится слой вязкой жидкости.

Здесь проходит важная этическая граница интерфейса.

Клиент вправе мгновенно сообщить, что нажатие принято, но не вправе объявить противника уничтоженным, пока сервер не подтвердил результат.

Предсказывать действие можно. Подменять исход действия нельзя.

Пример второй: осколок, который разоблачает иллюзию

Статичная сцена слишком легко убеждает нас, что репроекция решила проблему.

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

Пусть его истинная позиция описывается обычной баллистикой:

\vec{x}_{\text{true}}(t)= \vec{x}_0+ \vec{v}_0t+ \frac{1}{2}\vec{g}t^2.

Отображаемая позиция зависит от возраста области, в которой находится объект:

 \vec{x}{\text{display}}(t)= \vec{x}{\text{true}} \left( t-Age\bigl(\vec{x}_{\text{true}}(t)\bigr) \right).

Сначала вычисляется истинная позиция, и только затем по ней определяется временная зона. Иначе объект может начать влиять на собственную задержку и «застрять» у границы слоёв.

Приблизительная длина визуального отставания равна:

D_{\text{particle}}\approx |\vec{v}(t)|\cdot Age.

При скорости 0,35 пикселя в миллисекунду область возрастом 15 мс даст отставание:

 D_{\text{focus}}\approx0{,}35\cdot15=5{,}25\ \text{pix}.

Область возрастом 80 мс даст уже:

 D_{\text{edge}}\approx0{,}35\cdot80=28\ \text{pix}.

В центре расхождение почти незаметно. На периферии становится очевидно, что зритель видит прошлое.

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

Старая картинка может быстро откликнуться на поворот, но это ещё не значит, что независимые объекты заняли правильные позиции.

Когда два времени конфликтуют

Разный возраст частей экрана создаёт риск причинно несовместимых кадров.

Представим противника, который за 50 мс пересёк границу между периферией и центральной зоной. Старый слой показывает прежнюю позицию, свежий — новую.

В области смешивания могут возникнуть:

  • два силуэта;

  • полупрозрачный след;

  • разорванная модель;

  • несовпадающая тень;

  • оружие без персонажа;

  • персонаж без части анимации.

Это уже не просто визуальный шум. Это конфликт двух версий реальности.

Если объект находился в точке -

\vec{x}(t-a_p)

на периферийном слое и в точке -

\vec{x}(t-a_f)

на свежем слое, расхождение равно:

 \Delta\vec{x}= \vec{x}(t-a_f)-\vec{x}(t-a_p).

При постоянной скорости:

 |\Delta\vec{x}| \approx |\vec{v}|(a_p-a_f).

Для скорости 0,5 пикселя в миллисекунду и разницы возрастов 60 мс получаем:

30\ \text{pix}.

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

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

  • маска объекта;

  • векторы движения;

  • области старого и нового положения;

  • сведения о раскрытых участках фона;

  • патч, который удалит старый силуэт;

  • повышенный приоритет всей траектории объекта.

В идеале эти данные предоставляет игровой движок.

Анализ готового видеопотока тоже возможен, но он значительно менее надёжен: кодек видит пиксели, а не причинную структуру сцены.

Где заканчивается обычный 2D warp

Глобальная двумерная деформация хорошо работает при:

  • повороте камеры;

  • небольшом изменении масштаба;

  • далёкой сцене;

  • слабом параллаксе;

  • коротком временном интервале.

Она работает плохо при:

  • движении камеры вперёд;

  • близкой геометрии;

  • появлении пространства за объектом;

  • быстрых независимых объектах;

  • резкой смене сцены;

  • частицах и сложной прозрачности.

Для поступательного движения потребуется глубина.

Экранную точку можно восстановить в трёхмерном пространстве:

\vec{X}=ZK^{-1}\vec{p},

где Z - глубина, K - матрица камеры, а -

\vec{p}

экранная координата.

После изменения камеры:

2D warp схема
2D warp схема
 \vec{X}'=R\vec{X}+\vec{t}.

Новая экранная координата определяется как:

\vec{p}'\sim K\vec{X}'.

Это уже depth-aware reprojection.

Но даже она не восстановит пиксели, которых не было в прошлом кадре. Поэтому раскрытые участки сцены придётся либо заполнять приближённо, либо немедленно обновлять свежими correction patches.

Не три мира, а три доступа к одному миру

Важно не спутать пространственно-временную фовеацию с запуском нескольких физических симуляций.

Мир должен оставаться единым:

 State_{\text{world}}(t).

Разные зоны получают разные временные срезы одной и той же истории:

 State_{\text{focus}}= State_{\text{world}}(t-a_f),  State_{\text{middle}}= State_{\text{world}}(t-a_m),  State_{\text{edge}}= State_{\text{world}}(t-a_p).

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

Это не три взрыва и не три версии противника.

Это одна реальность, доступ к которой открыт с разной задержкой.

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

Как собирается финальный кадр

Финальное изображение можно представить как композицию нескольких слоёв:

 I_{\text{final}}= w_bI_{\text{warp}}+ w_fI_{\text{focus}}+ w_mI_{\text{middle}}+ w_pI_{\text{peripheral}}+ I_{\text{critical}}+ I_{\text{HUD}}.

Финальное изображение собирается из нескольких слоёв: скорректированного прошлого кадра, центральной зоны, средней области, периферии, критически важных объектов и интерфейса. Коэффициенты (w_b), (w_f), (w_m) и (w_p) определяют вклад каждой области в итоговый кадр.

Здесь:

  1. деформированный кадр — последний готовый кадр, подстроенный под новый ввод;

  2. центральная область — самая свежая часть изображения;

  3. средний слой — область с небольшой задержкой;

  4. фон — более старая часть изображения по краям;

  5. критические элементы — важные цели, вспышки и исправления;

  6. интерфейс — прицел, индикаторы и другие элементы, которые рисуются локально.

  7. Пространственные веса должны плавно меняться и в сумме давать единицу для основных видео слоёв:

 w_b+w_f+w_m+w_p=1.

Центральный вес, например, можно задать гауссовой функцией:

 A(x,y)= \exp\left( -\frac{(x-c_x)^2}{2\sigma_x^2} -\frac{(y-c_y)^2}{2\sigma_y^2} \right).

Тогда:

 w_f(x,y)=A(x,y),

а вес старого базового слоя уменьшается по мере приближения к центру.

На практике понадобится больше масок, нормализация и защита от мерцания, но смысл остаётся прежним:

Временная граница не должна выглядеть как жёсткая линза, наложенная поверх игры.

 три доступа к одному миру
три доступа к одному миру

Предсказание будущего внимания

Без eye tracker система не знает точную точку фиксации взгляда. Но в игре можно оценивать вероятную область внимания по поведению пользователя.

Например, будущий центр можно приблизить выражением:

 \vec{c}{\text{future}}= \vec{c}{\text{current}}+ \tau\vec{v}_{\text{input}},

где:

  1. текущий центр — положение области сейчас;

  2. скорость движения — направление и скорость камеры или курсора;

  3. время прогноза — насколько далеко вперёд система предсказывает положение центра.

При прогнозе на 30 мс зона с минимальной задержкой немного смещается в сторону движения.

Более полная карта внимания может учитывать несколько сигналов:

 AttentionMap= 0{,}35G_{\text{crosshair}}+ 0{,}20G_{\text{touch}}+ 0{,}15M_{\text{saliency}}+ 0{,}20T_{\text{targets}}+ 0{,}10U_{\text{interface}}.

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

Это не попытка прочитать мысли игрока.

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

Сетевой планировщик как редактор настоящего

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

Для каждого участка изображения известны:

  1. размер обновления;

  2. насколько оно улучшит картинку;

  3. как давно участок обновлялся;

  4. максимально допустимая задержка.

Оставшийся запас времени равен:

 Slack_i=Deadline_i-Age_i.

Чем меньше запас, тем срочнее обновление.

Простую оценку полезности передачи можно записать так:

 Score_i= \frac{\Delta E_i} {B_i\left(\varepsilon+\max(Slack_i,0)\right)}.

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

Высокий приоритет получает область, которая:

  • сильно уменьшает заметную ошибку;

  • требует мало битов;

  • почти исчерпала допустимый возраст.

В идеализированной форме планировщик решает задачу:

 \max \sum_i x_i\Delta E_i

при ограничении:

 \sum_i x_iB_i\le B_{\text{available}}, \qquad x_i\in{0,1}.

Планировщик выбирает, какие участки изображения обновить в первую очередь. Для каждого участка есть два значения: насколько обновление улучшит картинку и сколько данных потребуется передать. Переменная (x_i) показывает решение планировщика: 1 — обновить участок, 0 — пропустить. Задача — выбрать такой набор участков, который даст максимальное улучшение изображения и при этом не превысит доступный объём данных.

то напоминает задачу о рюкзаке.

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

Например, область может перейти в высокий приоритет при условии:

P_i>P_{\text{high}},

но вернуться обратно только при:

 P_i<P_{\text{low}}, \qquad P_{\text{low}}<P_{\text{high}}.

В философском смысле планировщик становится редактором настоящего.

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

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

HUD не обязан быть частью видеопотока

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

Сервер передаёт состояние:

ammo = 27
health = 81
weapon = rifle
shot_confirmed = false

А клиент самостоятельно строит изображение интерфейса.

Тогда задержка HUD приблизительно равна:

 L_{\text{HUD}}\approx L_{\text{message}}+ L_{\text{local render}}+ L_{\text{scanout}}.

Задержка интерфейса складывается из времени доставки сообщения, локальной отрисовки и вывода изображения на экран.

Она может быть значительно меньше возраста видеокадра.

Но здесь снова появляется граница между прогнозом и ложью.

Клиент может мгновенно показать:

Нажатие принято.

Но не должен показывать:

Противник убит.

пока сервер этого не подтвердил.

Локальный интерфейс может уменьшать ощущение задержки, но не должен подменять авторитетное состояние игры.

Почему ED Lite может означать не «хуже», а «избирательнее»

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

Но его можно понимать иначе.

ED Lite — это система, которая знает, что ресурс нельзя тратить одинаково на всё.

Вместо одного тяжёлого равномерного потока можно представить несколько логических компонентов:

Base Layer:
960×540
30 FPS
Дешёвый и относительно старый фон

Focus Truth:
Центральная или смысловая область
60–120 обновлений в секунду
Минимальный deadline

Critical Patches:
Цели
Вспышки
Новые объекты
Раскрытые участки сцены

HUD:
Локальная отрисовка
Подтверждённое состояние

Warp:
Реакция на свежий ввод
На каждом такте дисплея

Общий битрейт складывается из базового слоя, свежей зоны, критических патчей и метаданных:

 B_{\text{total}}= B_{\text{base}}+ B_{\text{focus}}+ B_{\text{critical}}+ B_{\text{metadata}}.

Но минимизировать нужно не сам битрейт, а взвешенную воспринимаемую ошибку:

 \min \sum_i P_iE_i

при ограничениях сети, декодера и времени композиции:

 B_{\text{total}}\le B_{\text{network}},  D_{\text{decode}}\le D_{\text{device}},  T_{\text{compose}}\le T_{\text{display}}.

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

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

Это предостерегает от чрезмерно сложной архитектуры.

Десятки независимых тайловых потоков могут оказаться тяжелее двух хорошо организованных слоёв. Красота модели не освобождает от стоимости декодирования, синхронизации и композиции на мобильном GPU.

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

Человек тоже не видит мир как синхронный массив

Здесь техническая гипотеза возвращается к философии восприятия.

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

Но восприятие не похоже на чтение идеально синхронного framebuffer.

Мы:

  • предсказываем движение;

  • достраиваем скрытые части объектов;

  • подавляем неважные детали;

  • переносим внимание;

  • замечаем изменение раньше, чем успеваем разобрать его содержание.

Это не означает, что человеку можно безнаказанно показывать ложь.

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

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

Более точная формулировка такова:

Мы хотим распределить неизбежное опоздание в соответствии с устройством действия и внимания.

Движение должно прийти раньше полной детализации. Подтверждение ввода — раньше подтверждения результата. Важный объект — раньше фона. Локальная гипотеза — раньше окончательной серверной коррекции.

Но каждое временное обещание должно в конце концов быть погашено общей реальностью.

Как проверить гипотезу, а не влюбиться в демонстрацию

Красивый стенд ничего не доказывает.

Более того, эффектная демонстрация особенно опасна: автор заранее знает, куда смотреть, понимает механику и невольно прощает артефакты.

Нужен сравнительный эксперимент как минимум с четырьмя режимами.

Режим A — обычная равномерная задержка

 Age(x,y)=L_{\text{uniform}}.

Все точки изображения имеют одинаковую задержку.

Режим B — только motion warp

Старый кадр локально реагирует на свежий пользовательский ввод.

Режим C — только свежая центральная область

Центр обновляется быстрее периферии, но глобальная репроекция не используется.

Режим D — полная схема

Warp, Focus Truth, Peripheral Truth и критические события работают совместно.

Оценивать следует не только субъективное «стало приятнее».

Нужно измерять:

  • время наведения;

  • среднюю ошибку прицела;

  • число попаданий;

  • обнаружение периферийных целей;

  • количество замеченных артефактов;

  • субъективную отзывчивость;

  • дискомфорт и усталость;

  • средний возраст пикселей;

  • 95-й и 99-й перцентили задержки;

  • битрейт;

  • время декодирования;

  • время композиции;

  • пропущенные display deadlines.

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

Поэтому важно исследовать распределение:

 P(L\le l)

и его процентный ранг:

 L_{50},\qquad L_{95},\qquad L_{99}.

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

L50 — обычная задержка для половины случаев;

L95 — задержка, ниже которой проходят 95% случаев;

L99 — задержка, ниже которой проходят 99% случаев.

Пользователь способен простить стабильные 30 мс и резко заметить периодические скачки до 150 мс.

Стабильность нередко ценнее рекордного минимального числа, которое система показывает только в идеальных условиях.

Отдельно нужно проверять честность предсказания.

Не возникло ли ложного второго силуэта? Не показывает ли локальный HUD событие, которое сервер затем отменяет? Не ухудшилось ли обнаружение угроз на периферии? Не приходится ли пользователю сознательно искать свежую зону?

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

Вместо заключения

Мы пока не знаем, станет ли Perceptual Latency Field частью рабочего режима EvertyDesk Lite.

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

Я старался после каждой формулы вставлять комментарий, но разметка LaTeX — сложная штука, приходилось начинать с новой строки. Но в целом, думаю, примерно рассказал понятно. Напомню: пошевелите тут курсором, очень забавно нейросеть сделала. https://vaalimusic.github.io/EVRT.percetual_latency/

Все эти исходы будут полезными результатами эксперимента.

Но сама постановка вопроса кажется важной.

Для ощущения контроля, возможно, не обязательно мгновенно обновлять каждый пиксель. Возможно, важнее правильно выбрать порядок возвращения истины:

  1. сначала движение;

  2. затем прицел;

  3. затем цель;

  4. затем критическое событие;

  5. и только после этого — полное согласие фона с настоящим.

Кадр думаю не обязан жить в одном времени. Но все его времена должны в конце концов встретиться в одной реальности ;)