Тройная буферизация Android - ожидаемое поведение?

Я изучаю производительность своего приложения, так как заметил, что оно пропускает несколько кадров при прокрутке. Я запустил systrace (на Nexus 4 с версией 4.3) и заметил интересный раздел в выводе.

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

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

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

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

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


Последующие вопросы

  1. В правой части этого снимка экрана приложение фактически обрабатывает буферы быстрее, чем дисплей потребляет их. Предположим, что во время выполнения #1 (обозначено на скриншоте) буфер A отображается, а буфер B визуализируется. #1 завершается задолго до вертикальной синхронизации и ставит буфер B в очередь. На данный момент, разве приложение не должно иметь возможность немедленно начать рендеринг буфера C? Вместо этого PerformTraversals #2 не запускается до следующей вертикальной синхронизации, что приводит к потере драгоценного времени между ними.

  2. Точно так же я немного смущен необходимостью waitForever здесь слева. Допустим, буфер A отображается, буфер B находится в очереди, а буфер C обрабатывается. Когда буфер C завершает рендеринг, почему он сразу не добавляется в очередь? Вместо этого он выполняет waitForever до тех пор, пока буфер B не будет удален из очереди, и в этот момент он добавляет буфер C, поэтому очередь, кажется, всегда остается в размере 1, независимо от того, насколько быстро приложение обрабатывает буферы.


person peterb_sm    schedule 30.04.2014    source источник


Ответы (1)


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

Метки не отображаются на ваших изображениях, но я предполагаю, что фиолетовая строка над зеленой строкой vsync — это статус BufferQueue. Вы можете видеть, что он обычно имеет 0 или 1 полный буфер в любое время. В самом левом углу «увеличенного слева» изображения вы можете видеть, что у него есть два буфера, но после этого у него есть только один, и на 3/4 экрана вы видите очень короткую фиолетовую полосу. указывает на то, что он едва отрендерил кадр вовремя.

См. этот пост и этот пост для фона.

Обновление для добавленных вопросов...

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

Счетчик BufferQueue, отображаемый в systrace, представляет собой количество буферов в очереди, т. е. количество буферов, в которых есть содержимое. Когда SurfaceFlinger захватывает буфер для отображения, он немедленно освобождает буфер, изменяя его состояние на «свободный». Это особенно интересно, когда буфер отображается на оверлее, потому что отображение рендерится непосредственно из буфера (в отличие от компоновки в пустой буфер и его отображения).

Позвольте мне повторить это еще раз: буфер, из которого дисплей активно считывает данные для отображения на экране, помечен как «свободный» в BufferQueue. Буфер имеет связанный забор, который изначально «активен». Пока он активен, никому не разрешено изменять содержимое буфера. Когда дисплей больше не нуждается в буфере, он сигнализирует о заборе.

Итак, причина, по которой код слева от вашей трассировки находится в waitForever(), заключается в том, что он ожидает сигнала забора. При срабатывании VSYNC дисплей переключается на другой буфер, сигнализирует об ограждении, и ваше приложение может немедленно начать использовать буфер. Это устраняет задержку, которая возникла бы, если бы вам пришлось ждать, пока SurfaceFlinger проснется, увидеть, что буфер больше не используется, отправить IPC через BufferQueue для освобождения буфера и т. д.

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

Суть в том, что вы никогда не увидите, чтобы BufferQueue превышала два значения для тройной буферизации.

Не все устройства работают так, как описано выше. Nexus 7 (2012 г.) не использует механизм «явной синхронизации», а устройства до ICS вообще не имеют BufferQueues.

Возвращаясь к вашему пронумерованному снимку экрана, да, между «1» и «2» есть много времени, когда ваше приложение может запускать PerformTraversals(). Трудно сказать наверняка, не зная, что делает ваше приложение, но я предполагаю, что у вас есть Управляемый хореографом цикл анимации, который пробуждается при каждом VSYNC и работает. Он не работает чаще, чем это.

Если вы используете системную трассировку Android Breakout, вы можете увидеть, как это выглядит при быстром рендеринге. как вы можете («заполнение очереди») и полагаться на обратное давление BufferQueue для регулирования скорости игры.

Особенно интересно сравнить N4 с 4.3 и N4 с 4.4. На 4.3 трассировка аналогична вашей: очередь в основном колеблется на 1, с регулярными падениями до 0 и случайными всплесками до 2. На 4.4 очередь почти всегда равна 2 со случайным падением до 1. В обоих случаях это спать в eglSwapBuffers(); в 4.3 трассировка обычно показывает waitForever() ниже этого, а в 4.4 показывает dequeueBuffer(). (Я не знаю причины этого навскидку.)

Обновление 2. Причиной различий между версиями 4.3 и 4.4 является изменение драйвера Nexus 4. Драйвер 4.3 использовал старый вызов dequeueBuffer, который превращается в dequeueBuffer_DEPRECATED() (Surface.cpp, строка 112). Старый интерфейс не воспринимает забор как "выходной" параметр, поэтому вызов должен вызывать сам waitForever(). Более новый интерфейс просто возвращает забор драйверу GL, который ждет, когда это необходимо (что может быть не сразу).

Обновление 3. Более подробное объяснение теперь доступно здесь. а>.

person fadden    schedule 30.04.2014
comment
Спасибо за ответ! Связанные сообщения действительно полезны, как и знание того, что systrace показывает буферную очередь. У меня все еще есть пара вопросов (добавлено выше). - person peterb_sm; 01.05.2014
comment
Спасибо за всю информацию! Вы правы, фрагмент systrace был во время броска ListView, и, читая источник, похоже, что AbsListView использует postOnAnimation() для обновления себя во время бросков. Мне все еще нужно выяснить, как лучше всего решить мою проблему с необходимостью наверстать упущенное после отставания, но знание причины действительно полезно. Спасибо еще раз! - person peterb_sm; 04.05.2014