Оптимизация задержки ввода для игр DirectX универсальная платформа Windows (UWP)
Задержка ввода может значительно повлиять на опыт игры, и оптимизация может сделать игру более полированной. Кроме того, правильная оптимизация событий ввода может улучшить время работы батареи. Узнайте, как выбрать правильные параметры обработки событий ввода CoreDispatcher, чтобы убедиться, что игра обрабатывает входные данные максимально плавно.
Задержка ввода
Задержка ввода — это время, необходимое системе для реагирования на входные данные пользователя. Ответ часто является изменением того, что отображается на экране, или то, что слышало через аудио обратной связи.
Каждое событие ввода, исходя из сенсорного указателя, указателя мыши или клавиатуры, создает сообщение для обработки обработчиком событий. Современные сенсорные дигитайзеры и игровые периферийные устройства сообщают о событиях ввода не менее 100 Гц на указатель, что означает, что приложения могут получать 100 событий или больше в секунду на указатель (или нажатие клавиш). Эта скорость обновлений усиливается, если одновременно происходит несколько указателей или используется устройство ввода с более высокой точностью (например, игровой мышью). Очередь сообщений о событии может быстро заполниться.
Важно понимать требования к задержке входных данных в игре, чтобы события обрабатывались таким образом, что лучше всего подходит для сценария. Нет ни одного решения для всех игр.
Эффективность питания
В контексте задержки ввода "эффективность питания" относится к тому, сколько игра использует GPU. Игра, использующая меньше ресурсов GPU, более эффективна и обеспечивает большую продолжительное время работы батареи. Это также относится к ЦП.
Если игра может нарисовать весь экран менее чем на 60 кадров в секунду (в настоящее время максимальная скорость отрисовки на большинстве дисплеев), не ухудшая взаимодействие с пользователем, это будет более эффективным путем рисования менее часто. Некоторые игры обновляют экран только в ответ на входные данные пользователя, поэтому эти игры не должны рисовать одно и то же содержимое несколько раз в 60 кадров в секунду.
Выбор оптимизации для
При разработке приложения DirectX необходимо выбрать некоторые варианты. Требуется ли приложению отрисовка 60 кадров в секунду для отображения гладкой анимации или требуется ли отрисовка только в ответ на входные данные? Должна ли она иметь наименьшую возможную задержку ввода или может ли она терпеть немного задержки? Будут ли мои пользователи ожидать, что мое приложение будет рассудительным о использовании батареи?
Ответы на эти вопросы, скорее всего, выровняют приложение с одним из следующих сценариев:
- Отрисовка по запросу. Игры в этой категории должны обновлять экран только в ответ на определенные типы входных данных. Эффективность питания отлична, так как приложение не отображает идентичные кадры многократно, и задержка ввода низка, так как приложение тратит большую часть времени ожидания входных данных. Настольные игры и новости читатели являются примерами приложений, которые могут попасть в эту категорию.
- Отрисовка по запросу с временными анимациями. Этот сценарий аналогичен первому сценарию, за исключением того, что некоторые типы входных данных запускают анимацию, которая не зависит от последующих входных данных от пользователя. Эффективность питания хороша, так как игра не отображает идентичные кадры многократно, и задержка ввода низка, пока игра не анимирует. Интерактивные детские игры и настольные игры, которые анимируют каждый шаг, являются примерами приложений, которые могут попасть в эту категорию.
- Отрисовка 60 кадров в секунду. В этом сценарии игра постоянно обновляет экран. Эффективность питания низка, так как она отображает максимальное количество кадров, которое может отображаться. Задержка ввода высока, так как DirectX блокирует поток во время представления содержимого. Это позволяет потоку отправлять больше кадров на дисплей, чем он может отображать пользователю. Стрелки первого человека, игры стратегии в режиме реального времени и игры на основе физики являются примерами приложений, которые могут попасть в эту категорию.
- Отрисовка 60 кадров в секунду и достижение минимальной возможной задержки ввода. Аналогично сценарию 3, приложение постоянно обновляет экран, поэтому эффективность питания будет плохой. Разница заключается в том, что игра реагирует на входные данные в отдельном потоке, поэтому обработка входных данных не блокируется путем представления графики на дисплее. Онлайн многопользовательские игры, боевые игры или ритм/время игры могут попасть в эту категорию, потому что они поддерживают перемещение входных данных в чрезвычайно жестких окнах событий.
Внедрение
Большинство игр DirectX управляются тем, что называется циклом игры. Базовый алгоритм состоит в том, чтобы выполнить следующие действия, пока пользователь не выйдет из игры или приложения:
- Входные данные процесса
- Обновление состояния игры
- Рисование содержимого игры
Когда содержимое игры DirectX отрисовывается и готово к отображению на экране, цикл игры ожидает, пока GPU будет готов к получению нового кадра, прежде чем снова просыпаться, чтобы обработать входные данные.
Мы покажем реализацию цикла игры для каждого из сценариев, упомянутых ранее, выполнив итерацию по простой головоломке головоломки. Точки принятия решений, преимущества и компромиссы, обсуждаемые с каждой реализацией, могут служить руководством для оптимизации приложений для низкой задержки ввода и эффективности питания.
Сценарий 1. Отрисовка по запросу
Первая итерация головоломки игры головоломки обновляет только экран, когда пользователь перемещает кусок головоломки. Пользователь может перетащить кусок головоломки на место или привязать его к месту, выбрав его, а затем коснувшись правильного назначения. Во втором случае головоломка будет переходить к месту назначения без анимации или эффектов.
Код содержит однопоточный игровой цикл в методе IFrameworkView::Run, использующего CoreProcessEventsOption::P rocessOneAndAllPending. При использовании этого параметра все доступные в настоящее время события в очереди отправляются. Если события не ожидаются, цикл игры ожидает, пока он не появится.
void App::Run()
{
while (!m_windowClosed)
{
// Wait for system events or input from the user.
// ProcessOneAndAllPending will block the thread until events appear and are processed.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
// If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
// scene and present it to the display.
if (m_updateWindow || m_state->StateChanged())
{
m_main->Render();
m_deviceResources->Present();
m_updateWindow = false;
m_state->Validate();
}
}
}
Сценарий 2. Отрисовка по запросу с временными анимациями
Во второй итерации игра изменяется таким образом, чтобы когда пользователь выбирает кусок головоломки, а затем касается правильного назначения для этого фрагмента, он анимирует по экрану, пока он не достигнет его назначения.
Как и раньше, код имеет однопоточный игровой цикл, использующий ProcessOneAndAllPending для отправки входных событий в очереди. Разница заключается в том, что во время анимации цикл изменяется на использование CoreProcessEventsOption::P rocessAllIfPresent , чтобы он не ждал новых входных событий. Если события не ожидаются, ProcessEvents возвращается немедленно и позволяет приложению представить следующий кадр в анимации. После завершения анимации цикл переключается на ProcessOneAndAllPending , чтобы ограничить обновления экрана.
void App::Run()
{
while (!m_windowClosed)
{
// 2. Switch to a continuous rendering loop during the animation.
if (m_state->Animating())
{
// Process any system events or input from the user that is currently queued.
// ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
// you are trying to present a smooth animation to the user.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
m_state->Update();
m_main->Render();
m_deviceResources->Present();
}
else
{
// Wait for system events or input from the user.
// ProcessOneAndAllPending will block the thread until events appear and are processed.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
// If any of the events processed resulted in a need to redraw the window contents, then we will re-render the
// scene and present it to the display.
if (m_updateWindow || m_state->StateChanged())
{
m_main->Render();
m_deviceResources->Present();
m_updateWindow = false;
m_state->Validate();
}
}
}
}
Чтобы обеспечить переход между ProcessOneAndAllPending и ProcessAllIfPresent, приложение должно отслеживать состояние, чтобы узнать, является ли это анимацией. В приложении головоломки для головоломки вы можете добавить новый метод, который можно вызвать во время цикла игры в классе GameState. Ветвь анимации цикла игры обновляет состояние анимации путем вызова нового метода GameState Update.
Сценарий 3. Отрисовка 60 кадров в секунду
В третьей итерации приложение отображает таймер, показывающий пользователю, сколько времени они работали над головоломкой. Так как оно отображает истекшее время до миллисекунда, оно должно отображать 60 кадров в секунду, чтобы обеспечить актуальность отображения.
Как и в сценариях 1 и 2, приложение имеет однопоточный игровой цикл. Разница с этим сценарием заключается в том, что, поскольку она всегда отрисовка, она больше не должна отслеживать изменения в состоянии игры, как было сделано в первых двух сценариях. В результате по умолчанию можно использовать ProcessAllIfPresent для обработки событий. Если события не ожидаются, ProcessEvents возвращается немедленно и переходит к отрисовке следующего кадра.
void App::Run()
{
while (!m_windowClosed)
{
if (m_windowVisible)
{
// 3. Continuously render frames and process system events and input as they appear in the queue.
// ProcessAllIfPresent will not block the thread to wait for events. This is the desired behavior when
// trying to present smooth animations to the user.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
m_state->Update();
m_main->Render();
m_deviceResources->Present();
}
else
{
// 3. If the window isn't visible, there is no need to continuously render.
// Process events as they appear until the window becomes visible again.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
}
}
}
Этот подход является самым простым способом написания игры, так как нет необходимости отслеживать дополнительное состояние, чтобы определить, когда нужно отрисовывать. Она обеспечивает максимально быструю отрисовку вместе с разумной скоростью отклика входных данных в интервале таймера.
Тем не менее, эта простота развития приходит с ценой. Отрисовка в 60 кадров в секунду использует больше мощности, чем отрисовка по требованию. Лучше всего использовать ProcessAllIfPresent , когда игра меняется, что отображается каждый кадр. Это также увеличивает задержку ввода на 16,7 мс, так как приложение теперь блокирует цикл игры на интервале синхронизации дисплея вместо ProcessEvents. Некоторые события ввода могут быть удалены, так как очередь обрабатывается только один раз на кадр (60 Гц).
Сценарий 4. Отрисовка 60 кадров в секунду и достижение минимальной возможной задержки ввода
Некоторые игры могут игнорировать или компенсировать увеличение задержки ввода, наблюдаемой в сценарии 3. Однако если низкая задержка ввода критически важна для опыта игры и чувства обратной связи игрока, игры, которые отображают 60 кадров в секунду, необходимо обрабатывать входные данные в отдельном потоке.
Четвертая итерация головоломки игры головоломки строится на сценарии 3 путем разделения входной обработки и отрисовки графики из цикла игры на отдельные потоки. Наличие отдельных потоков для каждого гарантирует, что входные данные никогда не задерживаются графическими выходными данными; однако код становится более сложным в результате. В сценарии 4 входной поток вызывает ProcessEvents с CoreProcessEventsOption::P rocessUntilQuit, который ожидает новых событий и отправляет все доступные события. Это поведение продолжается до закрытия окна или игры вызывает CoreWindow::Close.
void App::Run()
{
// 4. Start a thread dedicated to rendering and dedicate the UI thread to input processing.
m_main->StartRenderThread();
// ProcessUntilQuit will block the thread and process events as they appear until the App terminates.
CoreWindow::GetForCurrentThread()->Dispatcher->ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
}
void JigsawPuzzleMain::StartRenderThread()
{
// If the render thread is already running, then do not start another one.
if (IsRendering())
{
return;
}
// Create a task that will be run on a background thread.
auto workItemHandler = ref new WorkItemHandler([this](IAsyncAction^ action)
{
// Notify the swap chain that this app intends to render each frame faster
// than the display's vertical refresh rate (typically 60 Hz). Apps that cannot
// deliver frames this quickly should set this to 2.
m_deviceResources->SetMaximumFrameLatency(1);
// Calculate the updated frame and render once per vertical blanking interval.
while (action->Status == AsyncStatus::Started)
{
// Execute any work items that have been queued by the input thread.
ProcessPendingWork();
// Take a snapshot of the current game state. This allows the renderers to work with a
// set of values that won't be changed while the input thread continues to process events.
m_state->SnapState();
m_sceneRenderer->Render();
m_deviceResources->Present();
}
// Ensure that all pending work items have been processed before terminating the thread.
ProcessPendingWork();
});
// Run the task on a dedicated high priority background thread.
m_renderLoopWorker = ThreadPool::RunAsync(workItemHandler, WorkItemPriority::High, WorkItemOptions::TimeSliced);
}
Шаблон приложения 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 кадров в секунду:
Рисунок 1
В Windows 8.1 DXGI представила флаг DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT для цепочки буферов, что позволяет приложениям легко уменьшить эту задержку, не требуя от них реализации эвристики, чтобы сохранить пустую очередь Present. Цепочки буферов, созданные с помощью этого флага, называются подождаемыми цепочками буферов. На рисунке 2 показан приблизительный жизненный цикл и ответ на входное событие при использовании цепочки подкачки, доступные для ожидания:
Рисунок 2
На этих схемах мы видим, что игры могут снизить задержку ввода на два полных кадра, если они способны отрисовки и представления каждого кадра в бюджете 16,7 мс, определенного скоростью обновления дисплея. В примере головоломки для головоломки используются подождите цепочки буферов и управляют ограничением очереди "Настоящее" путем вызова: m_deviceResources->SetMaximumFrameLatency(1);