Минимизация задержки ввода в играх DirectX универсальной платформы Windows (UWP)

Минимизация задержки ввода в играх DirectX универсальной платформы Windows (UWP)

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

Задержка ввода

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

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

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

Эффективность энергопотребления

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

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

Выбор приоритетов

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

Ответы на эти вопросы помогут отнести ваше приложение к одному из следующих сценариев.

  1. Отрисовка по запросу. Игры из этой категории должны обновлять экран только в ответ на определенные типы ввода. Здесь очень высока эффективность энергопотребления, поскольку приложение не выполняет постоянную отрисовку одинаковых кадров, а задержка ввода мала, потому что приложение почти все время проводит в ожидании ввода. Примерами приложений такой категории служат настольные игры и программы для чтения новостей.
  2. Отрисовка по запросу с кратковременной анимацией. Этот сценарий аналогичен первому, но здесь некоторые типы ввода запускают анимацию, которая не зависит от последующего ввода пользователя. Эффективность энергопотребления высока, поскольку игра не выполняет постоянную отрисовку одинаковых кадров, а задержка ввода, пока игра не выполняет анимацию, мала. К этой категории относятся интерактивные детские игры и настольные игры с анимацией каждого хода.
  3. Отрисовка с частотой 60 кадров в секунду. В этом сценарии игра постоянно обновляет экран. Эффективность энергопотребления низка, поскольку игра отрисовывает кадры с максимальной скоростью, поддерживаемой экраном. Задержка ввода велика, потому что DirectX блокирует поток, пока идет представление содержимого. Это делается, чтобы поток не отправлял на экран больше кадров, чем тот может показать пользователю. Примером игр этой категории служат шутеры от первого лица, стратегии реального времени и игры с реалистичной физикой.
  4. Отрисовка с частотой 60 кадров в секунду с минимальной задержкой ввода. Здесь, как и в сценарии 3, приложение постоянно обновляет экран, и поэтому эффективность энергопотребления будет низкой. Отличие состоит в том, что игра отвечает на ввод в отдельном потоке, и поэтому обработка ввода не блокируется представлением графики на экране. К этой категории относятся многопользовательские сетевые игры, игры с боевыми действиями и игры, где нужно попадать в ритм и вовремя выполнять различные действия, поскольку они поддерживают обработку входных движений в очень ограниченные отрезки времени.

Реализация

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

  1. Обработка ввода
  2. Обновление состояния игры
  3. Отрисовка содержимого

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

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

Сценарий 1. Отрисовка по запросу

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

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

Сценарий 2. Отрисовка по запросу с периодической анимацией

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

В коде также заложен однопотоковый цикл, использующий ProcessOneAndAllPending для отправки событий ввода в очередь. Разница в том, что в процессе анимации цикл меняется и использует CoreProcessEventsOption::ProcessAllIfPresent, чтобы не ждать новых событий ввода. Если событий нет, ProcessEvents немедленно возвращается, за счет чего приложение может отображать следующий кадр анимации. По завершении анимации цикл вновь переключается на ProcessOneAndAllPending, чтобы ограничить обновление экрана.

Для перехода между ProcessOneAndAllPending и ProcessAllIfPresent приложение должно отслеживать собственное состояние, чтобы знать, воспроизводится ли анимация. В игре-пазле для этого добавляется новый метод, который можно вызывать во время цикла игры в классе GameState. Ветвь анимации внутри цикла обновляет состояние анимации, вызывая новый метод Update GameState.

Сценарий 3. Отрисовка с частотой 60 кадров в секунду

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

Как и в сценариях 1 и 2, приложение строится на однопотоковом цикле. Однако поскольку отрисовка происходит постоянно, нет необходимости отслеживать состояние игры (в отличие от первых двух сценариев). Поэтому для обработки событий можно по умолчанию использовать ProcessAllIfPresent. Если событий нет, ProcessEvents немедленно возвращается и переходит к отрисовке следующего кадра.

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

Но у данного способа есть и обратная сторона. Отрисовка с частотой 60 кадров в секунду использует больше электроэнергии, чем отрисовка по запросу. Лучше всего использовать ProcessAllIfPresent, пока игра меняет содержимое, отображаемое в каждом следующем кадре. Задержка ввода при этом увеличивается на 16,7 мс, поскольку теперь приложение блокирует цикл на время интервала синхронизации дисплея вместо ProcessEvents. При этом некоторые события ввода могут игнорироваться, поскольку очередь обрабатывается со скоростью, эквивалентной частоте кадров (60 Гц).

Сценарий 4. Отрисовка с частотой 60 кадров в секунду с минимальной задержкой ввода

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

В четвертой итерации игра строится на сценарии 3, разделяя обработку ввода и отрисовку графики из цикла на отдельные потоки. Благодаря разделению потоков обработка ввода не задерживается выводом графики, хотя код при этом усложняется. В сценарии 4 поток ввода вызывает ProcessEvents с CoreProcessEventsOption::ProcessUntilQuit, который ждет новые события и распределяет все доступные. Это длится до тех пор, пока окно не будет закрыто или пока игра не вызовет CoreWindow::Close.

Шаблон Приложение DirectX 11 и XAML (универсальное приложение Windows) в Microsoft Visual Studio 2015 разделяет цикл игры на несколько потоков по аналогичному принципу. С помощью объекта Windows::UI::Core::CoreIndependentInputSource он запускает поток обработки ввода, а также создает поток отрисовки независимо от потока пользовательского интерфейса XAML. Подробности об этих шаблонах см. в статье Создание проекта игры универсальной платформы Windows и DirectX на основе шаблона.

Дополнительные способы сокращения задержки ввода

Цепочки буферов с поддержкой ожидания

Игры DirectX реагируют на действия пользователя, обновляя изображение на экране. На дисплеях с частотой 60 Гц изображение обновляется каждые 16,7 мс (1 секунда/60 кадров). На рисунке 1 представлены примерный жизненный цикл и реакция на событие ввода относительно сигнала обновления 16,7 мс (VBlank) для приложений с отрисовкой 60 кадров в секунду.

в Windows 8.1 DXGI появился флаг DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT для цепочки буферов, что позволяет приложениям легко сократить эту задержку, не требуя от них реализации эвристики для сохранения текущей очереди. Цепочки буферов с таким флагом называются цепочками буферов с поддержкой ожидания. На рисунке 2 представлены примерный жизненный цикл и реакция на событие ввода для приложений, использующих цепочки буферов с поддержкой ожидания.

Из этих схем следует, что игры потенциально могут снижать задержку ввода на два полных кадра, если они способны отрисовывать и выводить каждый кадр за те 16,7 мс, за которые обновляется изображение на дисплее. Пример игры-пазла использует цепочки буферов с поддержкой ожидания и контролирует заполнение очереди Present с помощью вызова m_deviceResources->SetMaximumFrameLatency(1);

📎📎📎📎📎📎📎📎📎📎