Подтвердить что ты не робот

Минимизировать задержку Android GLSurfaceView

Следуя другим вопросам, связанным с переполнением стека, я прочитал руководство для внутренних поверхностей Android Surfaces, SurfaceViews и т.д. здесь:

https://source.android.com/devices/graphics/architecture.html

Это руководство дало мне намного лучшее понимание того, как все разные части подходят друг другу на Android. В нем описывается, как eglSwapBuffers просто подталкивает визуализированный кадр в очередь, которая позже будет потребляться SurfaceFlinger, когда он подготавливает следующий кадр для отображения. Если очередь заполнена, она будет ждать, пока буфер не станет доступен для следующего кадра перед возвратом. В приведенном выше документе описывается это как "набивка очереди" и полагается на "противодавление" буферов подкачки, чтобы ограничить рендеринг на vsync дисплея. Это то, что происходит с использованием непрерывного режима рендеринга по умолчанию для GLSurfaceView.

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

В отличие от рендеринга по требованию, как правило, происходит гораздо реже, чем частота обновления отображения, поэтому обычно BufferQueues для этих представлений пуст, и поэтому любые обновления, помещенные в эти очереди, будут захвачены SurfaceFlinger на самом следующем vsync.

Итак, вот вопрос: как настроить непрерывный рендеринг, но с минимальным отставанием? Цель состоит в том, что очередь буферов пуста в начале каждого vsync, я представляю свой контент менее чем за 16 мс, выталкиваю его в очередь (количество буфера = 1), а затем его потребляет SurfaceFlinger на следующем vsync (количество буферов = 0), повторите. Количество буферов в очереди можно увидеть в systrace, поэтому цель состоит в том, чтобы этот чередование составлял от 0 до 1.

В документе, который я упоминал выше, представлен хореограф как способ получить обратные вызовы для каждого vsync. Однако я не убежден, что этого достаточно, чтобы добиться минимального отставания, которое я испытываю. Я тестировал выполнение requestRender() в обратном вызове vsync с минимальным значением onDrawFrame(), и он действительно демонстрирует поведение счетчика 0/1. Однако что, если SurfaceFlinger не сможет выполнить всю свою работу за один период (возможно, появляется уведомление или что-то еще)? В этом случае я ожидаю, что мой рендеринг с радостью будет генерировать 1 кадр за vsync, но потребительский конец этого BufferQueue сбросил фрейм. Результат: теперь мы чередуем между 1 и 2 буферами в нашей очереди, и мы получили кадр задержки между выполнением рендеринга и просмотром фрейма.

В документе, как представляется, предлагается посмотреть смещение по времени между сообщенным временем vsync и началом обратного вызова. Я вижу, как это может помочь, если ваш обратный вызов доставляется последним из-за вашего основного потока из-за передачи макета или чего-то еще. Однако я не думаю, что это позволило бы обнаружить SurfaceFlinger, пропуская бит и не используя раму. Есть ли способ, которым приложение может работать, что SurfaceFlinger сбросил фрейм? Также кажется неспособностью сказать, что длина очереди нарушает идею использования времени vsync для обновлений состояния игры, так как там будет отображаться неизвестное количество кадров в очереди до того, как будет отображаться тот, который вы показываете.

Уменьшение максимальной длины очереди и использование избыточного давления было бы одним из способов достижения этого, но я не думаю, что существует API для установки максимального количества буферов в GLSurfaceView BufferQueue?

4b9b3361

Ответ 1

Отличный вопрос.

Быстрый бит фона для всех, кто читает это:

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

Проблема похожа на A/V sync, где вам нужен звук, связанный с кадром, чтобы выйти из динамика, когда видеокадра отображается на экране. В этом случае общая латентность не имеет значения до тех пор, пока она будет одинаково одинаковой на аудио и видео выходах. Это сталкивается с очень похожими проблемами, потому что вы потеряете синхронизацию, если SurfaceFlinger закроется, и ваше видео будет последовательно отображаться на один кадр позже.

SurfaceFlinger работает с повышенным приоритетом и делает относительно небольшую работу, поэтому вряд ли может пропустить бит самостоятельно... но это может произойти. Кроме того, он компонует кадры из нескольких источников, некоторые из которых используют заграждения для сигнализации асинхронного завершения. Если видеофрагмент во время записи формируется с выходом OpenGL, а рендеринг GLES не завершен, когда достигнут крайний срок, вся композиция будет отложена до следующего VSYNC.

Желание свести к минимуму задержку было достаточно сильным, так что в выпуске Android KitKat (4.4) появилась функция "DispSync" в SurfaceFlinger, которая бреет половину латентного времени от обычной двухкадровой задержки. (Это кратко упоминается в документе графической архитектуры, но не широко используется.)

Так что ситуация.. В прошлом это было меньше проблем для видео, потому что 30fps-видео обновляет каждый другой фрейм. Икоты работают сами по себе, потому что мы не пытаемся сохранить очередь в полном объеме. Мы начинаем видеть видео с частотой 48 Гц и 60 Гц, поэтому это имеет значение больше.

Вопрос заключается в том, как мы обнаруживаем, что кадры, которые мы отправляем в SurfaceFlinger, отображаются как можно скорее или тратят лишний кадр, ожидающий буфер, который мы отправили ранее?

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

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

Для A/V-синхронизации это немного сложнее, потому что приложение не может знать характеристики дисплея. Например, некоторые дисплеи имеют "интеллектуальные панели", в которые встроена память. (Если то, что на экране не обновляется часто, вы можете сэкономить много энергии, не имея панели сканирования пикселей по шине памяти 60x в секунду.) Они могут добавить дополнительный кадр задержки, который должен быть учтен.

Решение Android движется в направлении A/V sync, чтобы приложение указывало SurfaceFlinger, когда он хочет, чтобы кадр отображался. Если SurfaceFlinger пропустит крайний срок, он опустит рамку. Это было добавлено экспериментально в 4.4, хотя оно на самом деле не предназначено для использования до следующего выпуска (оно должно работать достаточно хорошо в "предварительном просмотре L", хотя я не знаю, включает ли это все части, необходимые для его полного использования).

Способ использования приложения заключается в вызове расширения eglPresentationTimeANDROID() до eglSwapBuffers(). Аргументом функции является желаемое время представления в наносекундах, используя ту же самую временную базу, что и хореограф (в частности, Linux CLOCK_MONOTONIC). Поэтому для каждого кадра вы берете временную метку, которую вы получили от хореографа, добавляете необходимое количество кадров, умноженное на приблизительную частоту обновления (которую вы можете получить, запросив объект Display - см. MiscUtils # getDisplayRefreshNsec()) и передать его EGL. При смене буферов требуемое время представления передается вместе с буфером.

Вспомните, что SurfaceFlinger просыпается один раз в VSYNC, просматривает коллекцию ожидающих буферов и доставляет набор аппаратных средств отображения через Hardware Composer. Если вы запросите отображение в момент времени T, и SurfaceFlinger считает, что кадр, переданный на аппаратное обеспечение дисплея, будет отображаться в момент времени T-1 или ранее, кадр будет удерживаться (и предыдущий кадр повторно показан). Если рамка появится в момент времени T, она будет отправлена ​​на дисплей. Если кадр появится в момент времени T + 1 или более поздний (т.е. Он пропустит свой крайний срок), и еще один кадр за ним в очереди, которая запланирована на более позднее время (например, кадр, предназначенный для времени T + 1), затем рамка, предназначенная для времени T, будет отброшена.

Решение не идеально подходит для вашей проблемы. Для синхронизации A/V вам требуется постоянная латентность, а не минимальная латентность. Если вы посмотрите на графику "запланированный своп, вы можете найти код, который использует eglPresentationTimeANDROID() способом, аналогичным тому, что сделает видеоплеер, (В текущем состоянии это немного больше, чем "генератор тона" для создания выхода systrace, но здесь есть основные части.) Стратегия состоит в том, чтобы сделать несколько кадров впереди, поэтому SurfaceFlinger никогда не иссякает, но это точно неправильно для вашего приложение.

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

Было бы полезно, чтобы приложение узнало, сколько на самом деле имеет количество задержек SurfaceFlinger, но для этого нет запроса. (В любом случае это неловко, так как "интеллектуальные панели" могут изменять режимы, тем самым изменяя латентность отображения, но если вы не работаете с A/V sync, все, что вам действительно нужно, сводит к минимуму задержку SurfaceFlinger.) достаточно безопасно принимать два кадра на 4.3+. Если это не два кадра, у вас может быть субоптимальная производительность, но чистый эффект будет не хуже, чем вы могли бы получить, если бы вы вообще не устанавливали время презентации.

Вы можете попробовать установить желаемое время показа, равное временной отметке хореографа; временная метка в недавнем прошлом означает "показать как можно скорее". Это обеспечивает минимальную задержку, но может иметь неприятные последствия при плавности. SurfaceFlinger имеет двухкадровую задержку, потому что он дает все в системе достаточно времени для выполнения работы. Если ваша рабочая нагрузка неравномерна, вы будете колебаться между однокадровой и двухкадровой задержкой, и выход будет выглядеть зависает на переходах. (Это было проблемой для DispSync, что сокращает общее время до 1,5 кадров.)

Я не помню, когда была добавлена ​​функция eglPresentationTimeANDROID(), но в старых версиях она должна быть не-op.

Нижняя строка: для "L" и в некоторой степени 4.4 вы должны иметь возможность получить нужное поведение, используя расширение EGL с двумя кадрами задержки. В более ранних версиях нет помощи от системы. Если вы хотите убедиться, что на вашем пути нет буфера, вы можете преднамеренно отбросить кадр так часто, чтобы освободить буферную очередь.

Обновить: один из способов избежать очередей кадров - вызвать eglSwapInterval(0). Если вы отправляете вывод непосредственно на дисплей, вызов будет отключать синхронизацию с VSYNC, что не позволяет ограничить частоту кадров приложения. При рендеринге через SurfaceFlinger это приводит к тому, что BufferQueue переходит в "режим async", что заставляет его отбрасывать кадры, если они отправляются быстрее, чем система может их отображать.

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