Здравствуйте, что-бы разобраться c уравнениями, нужно примерно понимать что это не новость а статья.

[ \boxed{ \begin{aligned} I_{\text{final}}(x,y,t) &= w_b(x,y,t), I_{\text{last}}!\left( W!\left(x,y,\Delta u(t)\right) \right) \ &\quad+ w_f(x,y,t), I_{\text{world}}!\left(x,y,t-a_f\right) \ &\quad+ w_m(x,y,t), I_{\text{world}}!\left(x,y,t-a_m\right) \ &\quad+ w_p(x,y,t), I_{\text{world}}!\left(x,y,t-a_p\right) \ &\quad+ I_{\text{critical}}(x,y,t) + I_{\text{HUD}}(x,y,t), \[6pt] P_i &= w_AA_i+ w_MM_i+ w_UU_i+ w_TT_i+ w_GG_i+ w_SS_i, \[4pt] AgeLimit_i &= a_{\min} + (a_{\max}-a_{\min}) (1-P_i)^\gamma C_i, \[4pt] Slack_i &= AgeLimit_i-Age_i, \[4pt] Score_i &= \frac{ P_i\Delta E_i }{ B_i\left( \varepsilon+\max(Slack_i,0) \right) }, \[4pt] {x_i^*} &= \underset{x_i\in{0,1}}{\arg\max} \sum_i x_i Score_i, \[4pt] \text{при условиях}\qquad \sum_i x_iB_i &\le B_{\text{network}}, \ \sum_i x_iD_i &\le D_{\text{device}}, \ T_{\text{compose}} &\le T_{\text{display}}, \[4pt] Age_i>AgeLimit_i \ \text{или}\ C_i<C_{\text{critical}} &\quad\Rightarrow\quad \text{принудительная коррекция}. \end{aligned} } ]

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

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

Дядя Гусь, объясняет 7000 читателем что такое математические обозначения, скажите Гусю спасибо.
Дядя Гусь, объясняет 7000 читателем что такое математические обозначения, скажите Гусю спасибо.

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

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

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

Поэтому можно собирать итоговое изображение из нескольких слоёв разной свежести.

Общая модель

Условно работу такой системы можно описать следующей формулой:

[ \boxed{ \begin{aligned} I_{\text{final}}(x,y,t) &= w_b(x,y,t), I_{\text{last}}!\left( W(x,y,\Delta u(t)) \right) \ &\quad+ w_f(x,y,t), I_{\text{world}}(x,y,t-a_f) \ &\quad+ w_m(x,y,t), I_{\text{world}}(x,y,t-a_m) \ &\quad+ w_p(x,y,t), I_{\text{world}}(x,y,t-a_p) \ &\quad+ I_{\text{critical}}(x,y,t) + I_{\text{HUD}}(x,y,t), \[6pt] P_i &= w_AA_i+ w_MM_i+ w_UU_i+ w_TT_i+ w_GG_i+ w_SS_i, \[4pt] AgeLimit_i &= a_{\min} + (a_{\max}-a_{\min}) (1-P_i)^\gamma C_i, \[4pt] Slack_i &= AgeLimit_i-Age_i, \[4pt] Score_i &= \frac{ P_i\Delta E_i }{ B_i\left( \varepsilon+\max(Slack_i,0) \right) }, \[4pt] \mathbf{x}^* &= \underset{x_i\in{0,1}}{\arg\max} \sum_i x_i Score_i, \[4pt] \text{при условиях}\qquad \sum_i x_iB_i &\le B_{\text{network}}, \ \sum_i x_iD_i &\le D_{\text{device}}, \ \sum_i x_iT_i &\le T_{\text{display}}, \[4pt] Age_i>AgeLimit_i \quad\text{или}\quad C_i<C_{\text{critical}} &\Rightarrow \text{принудительная коррекция}. \end{aligned} } ]

Но мы разжуем по букавкам.

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

Смотри, как устроен этот конструктор:

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

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

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

Гуси помогают прочитать первый абзац
Гуси помогают прочитать первый абзац
  1. сборку итогового изображения;

  2. оценку важности областей;

  3. определение допустимого возраста данных;

  4. выбор обновлений с учётом ограниченных ресурсов;

  5. принудительное исправление серьёзных ошибок.

Из чего собирается кадр

Первая часть формулы описывает итоговое изображение:

[ 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}}. ]

Фактически клиент может одновременно использовать несколько источников изображения. Но совсем не понятно, какие клиенты? Какие источники? Кто мы? Гусь тут один!

Дядя гусь поможет
Дядя гусь поможет

Скорректированный прошлый кадр

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

[ I_{\text{warp}}(x,y,t)= I_{\text{last}}!\left(W(x,y,\Delta u(t))\right). ]
Дядя гусь пытается объяснить разные символы в UTF8
Дядя гусь пытается объяснить разные символы в UTF8

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

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

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

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

Свежая центральная область

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

  • участок вокруг прицела;

  • противника;

  • интерактивный объект;

  • место касания на сенсорном экране;

  • направление движения камеры.

Эта область должна обновляться чаще остальных и иметь минимальный возраст.

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

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

Свежая центральная область

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

  • участок вокруг прицела;

  • противника;

  • интерактивный объект;

  • место касания на сенсорном экране;

  • направление движения камеры.

Эта область должна обновляться чаще остальных и иметь минимальный возраст.

Средняя область

Дядя Гусь про среднюю область (Не Тверскую)
Дядя Гусь про среднюю область (Не Тверскую)

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

Периферия

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

Критические элементы

Критические данные накладываются поверх остальных слоёв. К ним могут относиться:

  • быстро появившаяся цель;

  • вспышка выстрела;

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

  • резкая коррекция положения объекта;

  • подтверждение попадания.

Такие элементы нельзя надолго оставлять в очереди.

Интерфейс

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

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

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

Интерфейс Дяди Гуся (10лвл)
Интерфейс Дяди Гуся (10лвл)

Как определяется важность области

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

[ P_i= w_AA_i+ w_MM_i+ w_UU_i+ w_TT_i+ w_GG_i+ w_SS_i. ]

Приоритет находится в диапазоне от нуля до единицы.

Он может учитывать следующие факторы:

  • положение рядом с прицелом;

  • количество движения;

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

  • присутствие цели;

  • визуальную заметность;

  • принадлежность к интерфейсу.

Например, противник около прицела может получить приоритет 0,95. Неподвижная стена на краю кадра — 0,2.

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

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

Допустимый возраст данных

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

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

Ну вовсе не понятно, а статья длинная и возвращаться в начало не хочется, поэтому - мы лучше позовем дядю Гуся и взглянем на изображение да?)

Дядя Гусь рисует всякие букавки.
Дядя Гусь рисует всякие букавки.

Запас времени

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

[ Slack_i=AgeLimit_i-Age_i. ]

Если участку разрешено иметь возраст 40 мс, а текущий возраст равен 25 мс, запас составляет 15 мс.

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

Отрицательный запас означает, что данные устарели сильнее допустимого:

[ Slack_i<0. ]

В таком случае участок должен получить высокий приоритет или перейти в режим принудительной коррекции. Но мы ведь лучше позовем Гуся ведь так? Га га га га

Дядя гусь, приди пожалуйста, нам тут нужно проклевать формулки:

Малыш Гусь рассказывает про запас времени
Малыш Гусь рассказывает про запас времени

Оценка полезности обновления

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

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

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

Что-то совсем на непонятном, что такое "скоре", а как "Ви" и треугольник? Дядя гусь помоги пожалуйста, мы никак не поймем эти рисунки!

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

Оценка увеличивается, когда:

  • область имеет высокий приоритет;

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

  • размер обновления небольшой;

  • срок обновления уже приближается.

Малое положительное число (\varepsilon) защищает формулу от деления на ноль.

Пример

Рассмотрим три участка.

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

  • приоритет — 0,95;

  • ожидаемое улучшение — 0,9;

  • размер обновления — 12 КБ;

  • запас времени почти исчерпан.

Второй участок содержит движущийся автомобиль:

  • приоритет — 0,7;

  • ожидаемое улучшение — 0,6;

  • размер обновления — 18 КБ;

  • запас времени — около 3 мс.

Третий участок содержит неподвижную стену:

  • приоритет — 0,2;

  • ожидаемое улучшение — 0,15;

  • размер обновления — 30 КБ;

  • запас времени — 20 мс.

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

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

Выбор обновлений

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

[ \mathbf{x}^*= \underset{x_i\in{0,1}}{\arg\max} \sum_i x_iScore_i. ]

Переменная (x_i) обозначает решение:

  • 1 — отправить обновление;

  • 0 — отложить его.

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

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

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

Ограничение сети

[ \sum_i x_iB_i\le B_{\text{network}}. ]

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

Ограничение декодера

[ \sum_i x_iD_i\le D_{\text{device}}. ]

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

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

Ограничение композиции

[ \sum_i x_iT_i\le T_{\text{display}}. ]

Все выбранные слои нужно успеть собрать до вывода следующего изображения на экран.

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

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

Но стоп, Дядя гусь а зачем вообще ограничивать декодер и композицию, разве и так не понятно что мы что-то жмем?

@user, мало выбрать полезные тайлы нужно еще Га га га
@user, мало выбрать полезные тайлы нужно еще Га га га

Принудительная коррекция

Некоторые обновления нельзя откладывать независимо от их обычной оценки:

[ Age_i>AgeLimit_i \quad\text{или}\quad C_i<C_{\text{critical}}. ]

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

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

В таком случае система может:

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

  • временно снизить качество соседних областей;

  • отправить небольшой аварийный патч;

  • заменить участок упрощённым изображением;

  • отключить локальную деформацию для проблемной зоны.

Принудительные обновления должны резервировать ресурсы раньше обычной очереди.

Упрощённый планировщик на Rust

Ниже приведён пример, который:

  1. вычисляет приоритет каждого тайла;

  2. определяет допустимый возраст;

  3. вычисляет запас времени и оценку;

  4. сначала выбирает обязательные исправления;

  5. затем добавляет остальные тайлы по убыванию оценки;

  6. учитывает ограничения сети, декодера и композиции.

use std::cmp::Ordering;

#[derive(Clone, Copy)]
struct Factors {
    attention: f64,
    motion: f64,
    input: f64,
    target: f64,
    saliency: f64,
    interface: f64,
}

#[derive(Clone, Copy)]
struct Weights {
    attention: f64,
    motion: f64,
    input: f64,
    target: f64,
    saliency: f64,
    interface: f64,
}

#[derive(Clone)]
struct Tile {
    name: &'static str,
    factors: Factors,

    // Уверенность в текущем локальном изображении.
    confidence: f64,

    // Текущий возраст данных в миллисекундах.
    age_ms: f64,

    // Ожидаемое уменьшение визуальной ошибки.
    delta_error: f64,

    // Стоимость передачи и обработки.
    bytes_kb: f64,
    decode_cost: f64,
    compose_ms: f64,
}

#[derive(Clone, Copy)]
struct Config {
    min_age_ms: f64,
    max_age_ms: f64,
    gamma: f64,
    epsilon: f64,
    critical_confidence: f64,
}

#[derive(Debug)]
struct EvaluatedTile {
    index: usize,
    priority: f64,
    age_limit_ms: f64,
    slack_ms: f64,
    score: f64,
    forced: bool,
}

fn calculate_priority(tile: &Tile, weights: Weights) -> f64 {
    let f = tile.factors;

    let priority =
        weights.attention * f.attention
        + weights.motion * f.motion
        + weights.input * f.input
        + weights.target * f.target
        + weights.saliency * f.saliency
        + weights.interface * f.interface;

    priority.clamp(0.0, 1.0)
}

fn calculate_age_limit(
    priority: f64,
    confidence: f64,
    config: Config,
) -> f64 {
    let extra_age =
        (config.max_age_ms - config.min_age_ms)
        * (1.0 - priority).powf(config.gamma)
        * confidence.clamp(0.0, 1.0);

    config.min_age_ms + extra_age
}

fn evaluate_tile(
    index: usize,
    tile: &Tile,
    weights: Weights,
    config: Config,
) -> EvaluatedTile {
    let priority = calculate_priority(tile, weights);

    let age_limit_ms =
        calculate_age_limit(priority, tile.confidence, config);

    let slack_ms = age_limit_ms - tile.age_ms;

    let denominator =
        tile.bytes_kb
        * (config.epsilon + slack_ms.max(0.0));

    let score = if denominator > 0.0 {
        priority * tile.delta_error / denominator
    } else {
        f64::INFINITY
    };

    let forced =
        tile.age_ms > age_limit_ms
        || tile.confidence < config.critical_confidence;

    EvaluatedTile {
        index,
        priority,
        age_limit_ms,
        slack_ms,
        score,
        forced,
    }
}

fn try_reserve(
    tile: &Tile,
    network_left: &mut f64,
    decode_left: &mut f64,
    compose_left: &mut f64,
) -> bool {
    let fits =
        tile.bytes_kb <= *network_left
        && tile.decode_cost <= *decode_left
        && tile.compose_ms <= *compose_left;

    if !fits {
        return false;
    }

    *network_left -= tile.bytes_kb;
    *decode_left -= tile.decode_cost;
    *compose_left -= tile.compose_ms;

    true
}

fn main() {
    let weights = Weights {
        attention: 0.35,
        motion: 0.20,
        input: 0.15,
        target: 0.20,
        saliency: 0.10,
        interface: 0.00,
    };

    let config = Config {
        min_age_ms: 12.0,
        max_age_ms: 80.0,
        gamma: 2.0,
        epsilon: 0.1,
        critical_confidence: 0.5,
    };

    let tiles = vec![
        Tile {
            name: "Цель у прицела",
            factors: Factors {
                attention: 1.0,
                motion: 0.9,
                input: 0.8,
                target: 1.0,
                saliency: 0.7,
                interface: 0.0,
            },
            confidence: 0.95,
            age_ms: 17.0,
            delta_error: 0.90,
            bytes_kb: 12.0,
            decode_cost: 1.2,
            compose_ms: 0.4,
        },
        Tile {
            name: "Движущийся объект",
            factors: Factors {
                attention: 0.7,
                motion: 0.9,
                input: 0.6,
                target: 0.7,
                saliency: 0.6,
                interface: 0.0,
            },
            confidence: 0.90,
            age_ms: 14.0,
            delta_error: 0.60,
            bytes_kb: 18.0,
            decode_cost: 1.0,
            compose_ms: 0.5,
        },
        Tile {
            name: "Стена на краю",
            factors: Factors {
                attention: 0.1,
                motion: 0.0,
                input: 0.0,
                target: 0.0,
                saliency: 0.2,
                interface: 0.0,
            },
            confidence: 1.0,
            age_ms: 40.0,
            delta_error: 0.15,
            bytes_kb: 30.0,
            decode_cost: 0.8,
            compose_ms: 0.6,
        },
        Tile {
            name: "Непредсказуемая вспышка",
            factors: Factors {
                attention: 0.8,
                motion: 1.0,
                input: 0.7,
                target: 0.9,
                saliency: 1.0,
                interface: 0.0,
            },
            confidence: 0.40,
            age_ms: 10.0,
            delta_error: 0.80,
            bytes_kb: 16.0,
            decode_cost: 1.5,
            compose_ms: 0.5,
        },
    ];

    let mut evaluated: Vec<EvaluatedTile> = tiles
        .iter()
        .enumerate()
        .map(|(index, tile)| {
            evaluate_tile(index, tile, weights, config)
        })
        .collect();

    evaluated.sort_by(|a, b| {
        b.score
            .partial_cmp(&a.score)
            .unwrap_or(Ordering::Equal)
    });

    println!("Оценка тайлов:");

    for item in &evaluated {
        let tile = &tiles[item.index];

        println!(
            "{:25} P={:.3}, age={:.2}, limit={:.2}, \
             slack={:.2}, score={:.6}, forced={}",
            tile.name,
            item.priority,
            tile.age_ms,
            item.age_limit_ms,
            item.slack_ms,
            item.score,
            item.forced
        );
    }

    // Ресурсы на один цикл планирования.
    let mut network_left = 50.0;
    let mut decode_left = 4.5;
    let mut compose_left = 1.8;

    let mut selected = vec![false; tiles.len()];

    // Сначала резервируем ресурсы для обязательных коррекций.
    for item in evaluated.iter().filter(|item| item.forced) {
        let tile = &tiles[item.index];

        if try_reserve(
            tile,
            &mut network_left,
            &mut decode_left,
            &mut compose_left,
        ) {
            selected[item.index] = true;
        } else {
            eprintln!(
                "Не хватает ресурсов для критического тайла: {}",
                tile.name
            );
        }
    }

    // Затем добавляем обычные тайлы по убыванию оценки.
    for item in evaluated.iter().filter(|item| !item.forced) {
        let tile = &tiles[item.index];

        if try_reserve(
            tile,
            &mut network_left,
            &mut decode_left,
            &mut compose_left,
        ) {
            selected[item.index] = true;
        }
    }

    println!("\nВыбранные обновления:");

    for (index, tile) in tiles.iter().enumerate() {
        if selected[index] {
            println!("- {}", tile.name);
        }
    }

    println!(
        "\nОстаток ресурсов: сеть {:.1} КБ, \
         декодер {:.1}, композиция {:.1} мс",
        network_left,
        decode_left,
        compose_left
    );
}

Дядя гусь, это Растик?

Да да, так оно и есть
Да да, так оно и есть

Что произойдёт в примере

Цель около прицела уже старше допустимого предела. Поэтому она будет выбрана принудительно.

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

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

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

Примерный результат:

Выбранные обновления:
- Цель у прицела
- Движущийся объект
- Непредсказуемая вспышка

Что произойдёт в примере

Цель около прицела уже старше допустимого предела. Поэтому она будет выбрана принудительно.

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

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

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

Примерный результат:

Выбранные обновления:
- Цель у прицела
- Движущийся объект
- Непредсказуемая вспышка

Почему одного Score недостаточно

Оценка помогает сортировать обновления, но не решает все проблемы.

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

Для более точного выбора можно использовать:

  • динамическое программирование;

  • ограниченный поиск с отсечениями;

  • несколько очередей приоритетов;

  • резервирование части бюджета для критических данных;

  • предсказание будущего движения;

  • обучение планировщика на пользовательской телеметрии.

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

Что измерять на практике

Среднее значение задержки не показывает полную картину. Полезнее отдельно измерять возраст данных для разных типов областей:

  • прицела;

  • целей;

  • движущихся объектов;

  • интерфейса;

  • центра кадра;

  • периферии.

Также стоит отслеживать перцентили:

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

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

Кроме задержки следует измерять:

  • число принудительных коррекций;

  • площадь устаревших областей;

  • величину визуального расхождения;

  • частоту ошибок локальной деформации;

  • стоимость обновлений в байтах;

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

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

  • количество пропущенных сроков.

Ограничения модели

Предложенная формула сильно упрощает реальную систему.

Дядя Гусь, мне кажется ну совсем не упрощает эта формула не?

Дядя гусь, обещает.
Дядя гусь, обещает.

Она не учитывает напрямую:

  • зависимость соседних тайлов друг от друга;

  • структуру видеокодека;

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

  • ошибки глубины;

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

  • сетевые потери;

  • джиттер;

  • конкуренцию между несколькими потоками;

  • особенности конкретного дисплея.

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

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

Итог

Главная идея модели состоит в том, что кадр не обязан иметь единую задержку. (кроме Гуся разумеется)

Наиболее важные данные можно обновлять чаще:

  • прицел;

  • цели;

  • ближайшие препятствия;

  • критические эффекты;

  • элементы управления.

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

  • неподвижный фон;

  • края изображения;

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

  • области вне направления движения.

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

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

Данная статья это спор с нейросетью что я не пробью свою историю в +2 рейтинга и расскажу понятным языком. Но я победил и вытянул +1, хотя когда уже заканчивал эту статью, поэтому все-таки публикую, надеюсь тут я рассказал все подробно об идее и разжевал формулы. Спасибо за внимание.

ChatGPT
ChatGPT

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


  1. ZUBlK
    19.07.2026 18:00

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


    1. JerryI
      19.07.2026 18:00

      А как деньги зарабатывать, если продавать софт как цифровую копию, а не как сервис... del


      1. vaalimusic Автор
        19.07.2026 18:00

        Я не зарабатываю деньги, я плохой специалист.


    1. vaalimusic Автор
      19.07.2026 18:00

      Ну так весь смысл о теории вроде как.


  1. ajijiadduh
    19.07.2026 18:00

    @vaalimusic вам первые 2 формулы самому-то норм видны?


    1. vaalimusic Автор
      19.07.2026 18:00

      Хорошо


  1. vaalimusic Автор
    19.07.2026 18:00

    Статья такая еще помойка.


  1. qr-kot
    19.07.2026 18:00

    Лучше бы автором текста была нейросеть.

    Картинки хорошо получились.