studio programacion herramientas fundamentos con avanzado aplicaciones android surfaceview lag

programacion - manual de android en pdf



Minimizar Android GLSurfaceView lag (1)

Siguiendo algunas otras preguntas sobre el desbordamiento de pila, he leído la guía de los aspectos internos de Android Surfaces, SurfaceViews, etc. desde aquí:

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

Esa guía me ha dado una mejor comprensión de cómo todas las diferentes piezas encajan en Android. Cubre cómo eglSwapBuffers simplemente empuja el marco renderizado a una cola que, posteriormente, SurfaceFlinger consumirá cuando prepare el siguiente marco para su visualización. Si la cola está llena, esperará hasta que haya un búfer disponible para el siguiente fotograma antes de volver. El documento anterior describe esto como "rellenar la cola" y confiar en la "contrapresión" de los búferes de intercambio para limitar el renderizado al vsync de la pantalla. Esto es lo que sucede con el modo de procesamiento continuo predeterminado de GLSurfaceView.

Si su representación es simple y se completa en mucho menos que el período de fotogramas, el efecto negativo de esto es un retraso adicional causado por el BufferQueue, ya que la espera en SwapBuffers no ocurre hasta que la cola está llena, y por lo tanto el fotograma la representación de re siempre está destinada a estar en la parte posterior de la cola, por lo que no se mostrará de inmediato en el próximo vsync, ya que es probable que haya buffers antes de que esté en la cola.

En contraste, la representación a petición suele ser mucho menos frecuente que la tasa de actualización de la pantalla, por lo que las BufferQueues para esas vistas están vacías y, por lo tanto, SurfaceFlinger capturará todas las actualizaciones puestas en esas colas en el próximo vsync.

Así que aquí está la pregunta: ¿Cómo puedo configurar un renderizador continuo, pero con un retraso mínimo? El objetivo es que la cola del búfer esté vacía al comienzo de cada vsync, represento mi contenido en menos de 16 ms, lo empujo a la cola (conteo del búfer = 1) y SurfaceFlinger lo consume en el siguiente vsync (conteo del búfer = 0), repita. El número de buffers en la cola se puede ver en systrace, por lo que el objetivo es tener esta alternativa entre 0 y 1.

El documento que menciono anteriormente presenta a Choreographer como una forma de obtener devoluciones de llamada en cada vsync. Sin embargo, no estoy convencido de que sea suficiente para poder lograr el comportamiento de retraso mínimo que busco. He probado hacer un requestRender () en una devolución de llamada vsync con un onDrawFrame () muy mínimo y, de hecho, muestra el comportamiento de conteo del búfer 0/1. Sin embargo, ¿qué sucede si SurfaceFlinger no puede hacer todo su trabajo dentro de un solo período de fotograma (tal vez aparezca una notificación o lo que sea)? En ese caso, espero que mi renderizador esté felizmente produciendo 1 fotograma por vsync, pero el extremo consumidor de ese BufferQueue ha eliminado un fotograma. Resultado: ahora estamos alternando entre 1 y 2 búferes en nuestra cola, y hemos ganado un marco de retraso entre hacer el renderizado y ver el marco.

El documento parece sugerir mirar el desplazamiento de tiempo entre la hora de sincronización vs reportada y cuando se ejecuta la devolución de llamada. Puedo ver cómo puede ayudar si su devolución de llamada se entrega tarde debido a su hilo principal debido a un pase de diseño o algo así. Sin embargo, no creo que eso permita detectar SurfaceFlinger saltando un tiempo y no consumir un marco. ¿Hay alguna forma en que la aplicación pueda resolver que SurfaceFlinger ha eliminado un marco? También parece que la incapacidad de decir la longitud de la cola rompe la idea de usar la hora de vsync para las actualizaciones del estado del juego, ya que hay una cantidad desconocida de cuadros en la cola antes de que se muestre el que estás representando.

Reducir la longitud máxima de la cola y confiar en la contrapresión sería una forma de lograr esto, pero no creo que haya una API para establecer el número máximo de búferes en el GLSurfaceView BufferQueue?


Gran pregunta

Un poco de fondo para cualquiera que lea esto:

El objetivo aquí es minimizar la latencia de la pantalla, es decir, el tiempo entre el momento en que la aplicación muestra un marco y cuando el panel de la pantalla ilumina los píxeles. Si solo estás lanzando contenido a la pantalla, no importa, porque el usuario no puede notar la diferencia. Sin embargo, si responde a la entrada táctil, cada fotograma de latencia hace que su aplicación se sienta un poco menos receptiva.

El problema es similar a la sincronización A / V, donde necesita que el audio asociado con un cuadro salga por el altavoz mientras el cuadro de video se muestra en la pantalla. En ese caso, la latencia general no importa, siempre y cuando sea siempre igual en las salidas de audio y video. Sin embargo, esto enfrenta problemas muy similares, ya que perderá la sincronización si SurfaceFlinger se detiene y su video se muestra constantemente un cuadro más tarde.

SurfaceFlinger funciona con prioridad elevada y hace relativamente poco trabajo, por lo que no es probable que pierda un solo ritmo ... pero puede suceder. Además, está componiendo marcos de varias fuentes, algunas de las cuales usan cercos para indicar la finalización asíncrona. Si un fotograma de video en tiempo real está compuesto con salida de OpenGL, y el procesamiento de GLES no se ha completado cuando llega la fecha límite, toda la composición se pospondrá a la siguiente VSYNC.

El deseo de minimizar la latencia fue lo suficientemente fuerte como para que la versión de Android KitKat (4.4) introdujera la función "DispSync" en SurfaceFlinger, que elimina la mitad de la latencia del retardo habitual de dos cuadros. (Esto se menciona brevemente en el documento de arquitectura de gráficos, pero no es de uso generalizado).

Así que esa es la situación. En el pasado, esto era un problema menor para el video, ya que el video de 30 fps se actualiza en todos los cuadros. Los hipos se resuelven naturalmente porque no estamos tratando de mantener la cola llena. Sin embargo, estamos empezando a ver videos de 48Hz y 60Hz, así que esto importa más.

La pregunta es, ¿cómo detectamos si los cuadros que enviamos a SurfaceFlinger se muestran lo antes posible, o si estamos pasando un cuadro adicional esperando detrás de un búfer que enviamos anteriormente?

La primera parte de la respuesta es: no puedes. No hay consulta de estado o devolución de llamada en SurfaceFlinger que le dirá cuál es su estado. En teoría, puede consultar el propio BufferQueue, pero eso no necesariamente le dirá lo que necesita saber.

El problema con las consultas y las devoluciones de llamada es que no pueden decirle qué es el estado, solo qué era el estado. Cuando la aplicación recibe la información y actúa sobre ella, la situación puede ser completamente diferente. La aplicación se ejecutará con la prioridad normal, por lo que está sujeta a demoras.

Para la sincronización A / V es un poco más complicado, porque la aplicación no puede conocer las características de la pantalla. Por ejemplo, algunas pantallas tienen "paneles inteligentes" que tienen memoria incorporada. (Si lo que hay en la pantalla no se actualiza con frecuencia, puede ahorrar mucha energía al no hacer que el panel escanee los píxeles a través del bus de memoria 60 veces por segundo). Estos pueden agregar un marco de latencia adicional que debe tenerse en cuenta.

La solución a la que se dirige Android para la sincronización A / V es que la aplicación le diga a SurfaceFlinger cuándo quiere que se muestre el marco. Si SurfaceFlinger no cumple con el plazo, deja caer el marco. Esto se agregó experimentalmente en 4.4, aunque no está realmente destinado a usarse hasta la próxima versión (debería funcionar lo suficientemente bien en "Vista previa de L", aunque no sé si eso incluye todas las piezas necesarias para usarlo por completo) .

La forma en que una aplicación usa esto es llamar a la extensión eglSwapBuffers() antes de eglSwapBuffers() . El argumento de la función es el tiempo de presentación deseado, en nanosegundos, utilizando la misma base de tiempo que Choreographer (específicamente, Linux CLOCK_MONOTONIC ). Entonces, para cada fotograma, toma la marca de tiempo que obtuvo del Coreógrafo, agregue el número deseado de fotogramas multiplicado por la frecuencia de actualización aproximada (que puede obtener al consultar el objeto de visualización - vea MiscUtils#getDisplayRefreshNsec() ), y páselo a EGL. Cuando intercambias buffers, el tiempo de presentación deseado pasa junto con el buffer.

Recuerde que SurfaceFlinger se activa una vez por VSYNC, observa la colección de búferes pendientes y entrega un conjunto al hardware de pantalla a través de Hardware Composer. Si solicita la visualización en el momento T, y SurfaceFlinger cree que un marco que se pasa al hardware de la pantalla se mostrará en el momento T-1 o anterior, el marco se mantendrá (y se volverá a mostrar el marco anterior). Si el marco aparecerá en el momento T, se enviará a la pantalla. Si el marco aparecerá en el tiempo T + 1 o posterior (es decir, perderá su fecha límite), y hay otro marco detrás en la cola que está programado para un momento posterior (por ejemplo, el marco previsto para el tiempo T + 1), entonces El cuadro destinado al tiempo T se caerá.

La solución no se adapta perfectamente a su problema. Para la sincronización A / V, necesita una latencia constante, no una latencia mínima. Si observa la actividad de " intercambio programado " de Grafika, puede encontrar algún código que use eglPresentationTimeANDROID() de una manera similar a como lo haría un reproductor de video. (En su estado actual, es poco más que un "generador de tonos" para crear una salida de systrace, pero las partes básicas están ahí.) La estrategia allí es renderizar unos pocos cuadros por delante, por lo que SurfaceFlinger nunca se seca, pero eso es exactamente incorrecto para su la aplicación

El mecanismo de tiempo de presentación, sin embargo, proporciona una manera de eliminar marcos en lugar de dejarlos retroceder. Si sabe que hay dos cuadros de latencia entre el tiempo informado por Choreographer y el momento en que se puede mostrar su cuadro, puede usar esta función para asegurarse de que los cuadros se eliminen en lugar de ponerlos en cola si están demasiado lejos en el pasado. La actividad Grafika le permite establecer la velocidad de cuadros y la latencia solicitada, y luego ver los resultados en systrace.

Sería útil para una aplicación saber cuántos marcos de latencia tiene SurfaceFlinger en realidad, pero no hay una consulta para eso. (Esto es un poco incómodo de tratar de todos modos, ya que los "paneles inteligentes" pueden cambiar los modos, cambiando así la latencia de la pantalla; pero a menos que esté trabajando en la sincronización A / V, todo lo que realmente importa es minimizar la latencia de SurfaceFlinger). razonablemente seguro asumir dos cuadros en 4.3+. Si no se trata de dos cuadros, es posible que tenga un rendimiento subóptimo, pero el efecto neto no será peor que el que obtendría si no configuró el tiempo de presentación.

Puede intentar establecer el tiempo de presentación deseado igual a la marca de tiempo del Coreógrafo; una marca de tiempo en el pasado reciente significa "mostrar lo antes posible". Esto asegura una latencia mínima, pero puede ser contraproducente en la suavidad. SurfaceFlinger tiene el retardo de dos fotogramas porque le da a todo en el sistema el tiempo suficiente para hacer el trabajo. Si su carga de trabajo es desigual, se tambaleará entre la latencia de fotograma único y de fotograma doble, y la salida tendrá un aspecto inestable en las transiciones. (Esto fue una preocupación para DispSync, que reduce el tiempo total a 1.5 cuadros).

No recuerdo cuándo se agregó la función eglPresentationTimeANDROID() , pero en versiones anteriores no debería estar disponible.

Línea inferior : para ''L'', y hasta cierto punto 4.4, debería poder obtener el comportamiento que desea usando la extensión EGL con dos cuadros de latencia. En versiones anteriores no hay ayuda del sistema. Si desea asegurarse de que no haya un búfer en su camino, puede dejar un marco deliberadamente cada cierto tiempo para permitir que la cola del búfer se vacíe.

Actualización : una forma de evitar poner en cola los marcos es llamar a eglSwapInterval(0) . Si enviaba la salida directamente a una pantalla, la llamada deshabilitaría la sincronización con VSYNC, eliminando la velocidad de fotogramas de la aplicación. Cuando se procesa a través de SurfaceFlinger, esto pone el BufferQueue en "modo asíncrono", lo que hace que se eliminen los marcos si se envían más rápido de lo que el sistema puede mostrarlos.

Tenga en cuenta que todavía tiene un triple búfer: se muestra un búfer, SurfaceFlinger retiene uno para que se muestre en el siguiente giro y la aplicación está utilizando uno.