Минимизировать отставание 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.
Документ, о котором я упоминал выше, представляет Choreographer как способ получения обратных вызовов для каждого vsync. Однако я не уверен, что этого достаточно, чтобы добиться минимального лага, которого я добиваюсь. Я протестировал выполнение requestRender() для обратного вызова vsync с очень минимальным onDrawFrame(), и он действительно демонстрирует поведение числа буферов 0/1. Однако, что если SurfaceFlinger не сможет выполнить всю свою работу в течение одного кадра (возможно, появится уведомление или что-то еще)? В этом случае я ожидаю, что мой рендерер с радостью будет производить 1 кадр на vsync, но потребительская часть этого BufferQueue пропустила кадр. Результат: теперь мы чередуем от 1 до 2 буферов в нашей очереди, и мы получили задержку между рендерингом и просмотром кадра.
Похоже, что документ предлагает посмотреть на разницу во времени между сообщенным временем vsync и временем выполнения обратного вызова. Я вижу, как это может помочь, если ваш обратный вызов доставлен поздно из-за вашего основного потока из-за прохода макета или чего-то еще. Однако я не думаю, что это позволило бы обнаружить, что SurfaceFlinger пропускает ритм и не использует кадр. Есть ли способ, которым приложение может решить, что SurfaceFlinger уронил кадр? Также кажется, что неспособность сказать, что длина очереди нарушает идею использования времени vsync для обновлений состояния игры, так как в очереди есть неизвестное количество кадров до того, как будет отображаться тот, который вы рендерили.
Сокращение максимальной длины очереди и использование обратного давления было бы одним из способов достижения этого, но я не думаю, что есть API для установки максимального количества буферов в GLSurfaceView BufferQueue?
1 ответ
Отличный вопрос
Немного фона для всех, кто читает это:
Цель здесь состоит в том, чтобы минимизировать задержку отображения, то есть время между тем, когда приложение отображает кадр, и когда панель дисплея подсвечивает пиксели. Если вы просто бросаете контент на экран, это не имеет значения, потому что пользователь не может понять разницу. Однако, если вы реагируете на сенсорный ввод, каждый кадр задержки заставляет ваше приложение чувствовать себя чуть менее отзывчивым.
Проблема аналогична A/V-синхронизации, где вам нужно аудио, связанное с кадром, чтобы выйти из динамика, когда видеокадр отображается на экране. В этом случае общая задержка не имеет значения, если она одинаково одинакова как для аудио, так и для видео выходов. Это сталкивается с очень похожими проблемами, потому что вы потеряете синхронизацию, если SurfaceFlinger остановится и ваше видео будет последовательно отображаться на один кадр позже.
SurfaceFlinger работает с повышенным приоритетом и выполняет относительно мало работы, поэтому вряд ли он пропустит ритм сам по себе... но это может произойти. Кроме того, он объединяет кадры из нескольких источников, некоторые из которых используют заборы, чтобы сигнализировать об асинхронном завершении. Если своевременный видеокадр составлен с выводом OpenGL, и рендеринг GLES не завершился, когда истекает крайний срок, вся композиция будет отложена до следующего VSYNC.
Желание минимизировать задержку было достаточно сильным, поэтому в выпуске Android KitKat (4.4) в SurfaceFlinger появилась функция DispSync, которая сбрасывает половину задержки с обычной задержкой в два кадра. (Это кратко упоминается в документации по графической архитектуре, но не широко используется.)
Такова ситуация. В прошлом это было меньше проблем для видео, потому что 30fps видео обновляет каждый второй кадр. Икоты работают сами по себе, потому что мы не пытаемся сохранить очередь полной. Мы начинаем смотреть видео 48 Гц и 60 Гц, так что это имеет большее значение.
Вопрос в том, как мы можем определить, отображаются ли фреймы, которые мы отправляем в SurfaceFlinger, как можно скорее, или тратим дополнительный фрейм в ожидании после буфера, который мы отправили ранее?
Первая часть ответа: вы не можете. На SurfaceFlinger нет запроса состояния или обратного вызова, который бы сообщал вам, каково его состояние. Теоретически вы можете запросить само BufferQueue, но это не обязательно скажет вам, что вам нужно знать.
Проблема с запросами и обратными вызовами состоит в том, что они не могут сказать вам, что такое состояние, только то, что было в состоянии. К тому времени, когда приложение получает информацию и действует на нее, ситуация может быть совершенно другой. Приложение будет работать с обычным приоритетом, поэтому оно может быть задержано.
Для аудио / видео синхронизации это немного сложнее, потому что приложение не может знать характеристики дисплея. Например, некоторые дисплеи имеют "умные панели" со встроенной памятью. (Если то, что на экране, не обновляется часто, вы можете сэкономить много энергии, не заставляя панель сканировать пиксели по шине памяти 60x в секунду.) Это может добавить дополнительный интервал задержки, который необходимо учитывать.
Решение, к которому движется Android для синхронизации аудио / видео, состоит в том, чтобы приложение сообщало SurfaceFlinger, когда оно хочет, чтобы кадр отображался. Если SurfaceFlinger пропускает крайний срок, он отбрасывает кадр. Он был добавлен экспериментально в 4.4, хотя на самом деле он не предназначен для использования до следующего выпуска (он должен работать достаточно хорошо в "L preview", хотя я не знаю, включает ли он все части, необходимые для его полного использования),
Приложение использует это для вызова eglPresentationTimeANDROID()
продление до eglSwapBuffers()
, Аргументом функции является желаемое время представления (в наносекундах) с использованием той же временной базы, что и у хореографа (в частности, Linux). CLOCK_MONOTONIC
). Таким образом, для каждого кадра вы берете временную метку, полученную от Choreographer, добавляете желаемое количество кадров, умноженное на приблизительную частоту обновления (которую вы можете получить, запросив объект Display - см. MiscUtils # getDisplayRefreshNsec ()), и передаете ее в EGL. При замене буферов желаемое время представления передается вместе с буфером.
Напомним, что SurfaceFlinger просыпается один раз за VSYNC, просматривает коллекцию ожидающих буферов и доставляет набор аппаратному обеспечению дисплея через Hardware Composer. Если вы запрашиваете отображение в момент времени T, и SurfaceFlinger считает, что кадр, переданный аппаратному средству отображения, будет отображаться в момент времени T-1 или ранее, кадр будет удерживаться (и предыдущий кадр снова показывается). Если кадр появится в момент времени T, он будет отправлен на дисплей. Если кадр появится в момент времени T+1 или позже (т. Е. Он пропустит свой крайний срок), а в очереди за ним есть еще один кадр, запланированный на более позднее время (например, кадр, предназначенный для времени T+1), то кадр, предназначенный для времени T, будет отброшен.
Решение не совсем подходит для вашей проблемы. Для синхронизации аудио / видео вам нужна постоянная задержка, а не минимальная задержка. Если вы посмотрите на действия Графика по " запланированному обмену ", вы можете найти код, который использует eglPresentationTimeANDROID()
способом, аналогичным тому, что сделал бы видеоплеер. (В своем текущем состоянии это немного больше, чем "генератор тона" для создания вывода systrace, но основные элементы есть.) Существует стратегия рендеринга на несколько кадров вперед, поэтому SurfaceFlinger никогда не запускается, но это совершенно неправильно для вашего приложение.
Механизм времени представления, однако, предоставляет способ отбрасывать кадры, а не давать им резервную копию. Если вам известно, что между временем, сообщаемым Choreographer, и временем, когда ваш кадр может отображаться, есть задержка в два кадра, вы можете использовать эту функцию, чтобы гарантировать, что кадры будут отброшены, а не поставлены в очередь, если они находятся слишком далеко в прошлое. Операция Grafika позволяет установить частоту кадров и запрошенную задержку, а затем просмотреть результаты в systrace.
Для приложения было бы полезно узнать, сколько кадров на самом деле имеет задержка SurfaceFlinger, но для этого нет запроса. (В любом случае, с этим несколько неловко иметь дело, поскольку "умные панели" могут изменять режимы, тем самым изменяя задержку дисплея; но если вы не работаете с A/V-синхронизацией, все, что вам действительно нужно, - это минимизировать задержку SurfaceFlinger.) Это Достаточно безопасно принять два кадра на 4.3+. Если это не два кадра, у вас может быть неоптимальная производительность, но чистый эффект будет не хуже, чем если бы вы вообще не установили время презентации.
Вы можете попытаться установить желаемое время представления, равное метке времени Хореографа; временная метка в недавнем прошлом означает "показывать как можно скорее". Это обеспечивает минимальную задержку, но может негативно сказаться на плавности. SurfaceFlinger имеет задержку в два кадра, потому что он дает всему в системе достаточно времени для выполнения работы. Если ваша рабочая нагрузка неравномерна, вы будете колебаться между задержкой в однокадровом и двухкадровом режимах, и выходные данные будут выглядеть неожиданно при переходах. (Это было проблемой для DispSync, который сокращает общее время до 1,5 кадров.)
Я не помню когда eglPresentationTimeANDROID()
Функция была добавлена, но в старых версиях она должна быть запрещена.
Итог: для 'L' и, в некоторой степени, 4.4, вы должны быть в состоянии получить желаемое поведение, используя расширение EGL с двумя кадрами задержки. На более ранних выпусках нет никакой помощи от системы. Если вы хотите убедиться, что на вашем пути нет буфера, вы можете сознательно отбрасывать кадр время от времени, чтобы позволить опустошению очереди буфера.
Обновление: один из способов избежать очереди кадров - это вызов eglSwapInterval(0)
, Если вы отправляете выходные данные непосредственно на дисплей, вызов отключит синхронизацию с VSYNC, не ограничивая частоту кадров приложения. При рендеринге через SurfaceFlinger это переводит BufferQueue в "асинхронный режим", что приводит к тому, что он отбрасывает кадры, если они отправляются быстрее, чем система может их отобразить.
Обратите внимание, что вы все еще трижды буферизуете: один буфер отображается, один удерживается SurfaceFlinger для отображения при следующем переключении, а другой втягивается приложением.