Biblioteca Frame Pacing Parte do Android Game Development Kit

A biblioteca Frame Pacing do Android, também conhecida como Swappy, faz parte das bibliotecas do AGDK. Ela ajuda jogos OpenGL e Vulkan a ter uma renderização uniforme e a corrigir o ritmo de frame no Android. Este documento define o ritmo de frame, descreve as situações em que o ritmo de frame é necessário e mostra como a biblioteca lida com essas situações. Caso queira pular diretamente para a implementação do ritmo de frame no seu jogo, consulte a Próxima etapa.

Contexto

O ritmo de frame é a sincronização da lógica e do loop de renderização de um jogo com o subsistema e o hardware de exibição de um SO. O subsistema de exibição do Android foi projetado para evitar artefatos visuais (conhecidos como tearing) que podem ocorrer quando o hardware de exibição muda para um novo frame no meio de uma atualização. Para evitar esses artefatos, o subsistema de exibição faz o seguinte:

  • Armazena frames anteriores em buffer internamente
  • Detecta envios de frames atrasados
  • Repete a exibição de frames anteriores quando frames atrasados são detectados

Um jogo informa o SurfaceFlinger, o compositor no subsistema de exibição, que enviou todas as chamadas de desenho necessárias para um frame (chamando eglSwapBuffers ou vkQueuePresentKHR). O SurfaceFlinger sinaliza a disponibilidade de um frame para o hardware de exibição usando uma trava. Em seguida, o hardware exibe o frame fornecido. O hardware de exibição faz marcações a uma taxa constante, por exemplo, de 60 Hz. Caso não haja nenhum novo frame quando o hardware precisar de um, o hardware exibirá o frame anterior novamente.

Muitas vezes, tempos para o renderização do frame inconsistentes ocorrem quando um loop de renderização do jogo é renderizado a uma taxa diferente do hardware de exibição nativo. Se um jogo em execução a 30 FPS tentar renderizar um dispositivo que tenha compatibilidade nativa com 60 FPS, o loop de renderização do jogo não saberá que um frame repetido permanece na tela por mais 16 milissegundos. Essa desconexão geralmente cria uma inconsistência substancial nos tempos para a renderização do frame, como: 49 milissegundos, 16 milissegundos, 33 milissegundos. Cenas extremamente complexas agravam esse problema, porque causam a perda de frames.

Soluções não ideais

As soluções a seguir para o ritmo de frame foram usadas por jogos no passado e, geralmente, resultam em tempos para a renderização inconsistentes e em aumento na latência de entrada.

Enviar frames com a rapidez da API de renderização

Essa abordagem vincula um jogo a uma atividade do SurfaceFlinger variável e introduz um frame extra de latência. O pipeline de exibição contém uma fila de frames, normalmente de 2, que será preenchida se o jogo estiver tentando apresentar frames muito rapidamente. Quando não há mais espaço na fila, o loop de jogo (ou pelo menos a linha de execução de renderização) é bloqueado por uma chamada de OpenGL ou Vulkan. Em seguida, o jogo é forçado a esperar até que o hardware de exibição mostre um frame e essa pressão de retorno sincronize os dois componentes. Essa situação é conhecida como buffer-stuffing ou queue-stuffing. O processo do renderizador não percebe o que está acontecendo, portanto, a inconsistência de frame rate fica pior. Se o jogo criar amostras das entradas antes do frame, a latência de entrada ficará pior.

Usar o Android Choreographer sozinho

Os jogos também usam o Android Choreographer para sincronização. Esse componente, disponível no Java da API 16 e em C++ da API 24, oferece marcações regulares na mesma frequência que o subsistema de exibição. Ainda há sutilezas sobre o momento em que essa marcação é entregue em relação ao hardware VSYNC real, e esses deslocamentos variam de acordo com o dispositivo. O buffer-stuffing ainda pode ocorrer para frames longos.

Vantagens da biblioteca Frame Pacing

A biblioteca Frame Pacing usa o Android Choreographer para sincronização e lida com a variabilidade na entrega de marcações. Ela usa carimbos de data/hora de apresentação para garantir que os frames sejam apresentados no momento adequado e nos limites de sincronização para evitar o buffer-stuffing. A biblioteca usa o NDK Choreographer, se disponível. Caso contrário, ela opta pelo Java Choreographer.

A biblioteca processa várias taxas de atualização, se compatíveis com o dispositivo, o que oferece ao jogo mais flexibilidade para apresentar um frame. Por exemplo, para um dispositivo que é compatível com uma taxa de atualização de 60 Hz e 90 Hz, um jogo que não pode produzir 60 frames por segundo pode diminuir para 45 FPS em vez de 30 FPS para continuar funcionando de maneira uniforme. A biblioteca detecta o frame rate esperado do jogo e ajusta automaticamente os tempos de apresentação de frame. A biblioteca Frame Pacing também melhora a duração da bateria, porque evita atualizações de tela desnecessárias. Por exemplo, se um jogo estiver renderizado a 60 QPS, mas a tela for atualizada a 120 Hz, ela será atualizada duas vezes para cada frame. A biblioteca Frame Pacing evita isso definindo a taxa de atualização para o valor compatível com o dispositivo mais próximo do frame rate desejado.

Como funciona

As seções a seguir mostram como a biblioteca Frame Pacing processa frames de jogos longos e curtos para atingir o ritmo de frame correto.

Ajustar o ritmo de frame para 30 Hz

A situação ideal no Android ao renderizar a 30 Hz em um dispositivo de 60 Hz é apresentada na figura 1. O SurfaceFlinger trava novos buffers gráficos, se presente (NB no diagrama indica que "nenhum buffer" está presente e o anterior é repetido).

Ritmo ideal de frame a 30 Hz em um dispositivo de 60 Hz

Figura 1. Ritmo ideal de frame a 30 Hz em um dispositivo de 60 Hz

Frames curtos de jogos causam renderização lenta

Na maioria dos dispositivos modernos, os mecanismos de jogos contam com a plataforma Choreographer para fornecer marcações que incitem os envios de frames. Entretanto, ainda há a possibilidade de erro no ritmo devido a frames curtos, como mostrado na figura 2. Frames curtos seguidos por frames longos são percebidos pelo jogador como renderização lenta.

Frames de jogo curtos

Figura 2. Um frame de jogo curto C faz com que o frame B apresente apenas um frame, seguido de vários frames C

A biblioteca Frame Pacing resolve esse problema usando carimbos de data/hora da apresentação. A biblioteca usa as extensões de carimbo de data/hora da apresentação EGL_ANDROID_presentation_time e VK_GOOGLE_display_timing para que os frames não sejam apresentados antecipadamente, como mostrado na Figura 3.

Carimbos de data/hora da apresentação

Figura 3. Frame de jogo B apresentado duas vezes para uma exibição mais uniforme

Frames longos causam renderização lenta e latência

Quando a carga de trabalho de exibição demora mais que a carga de trabalho do aplicativo, frames extras são adicionados a uma fila. Isso causa, mais uma vez, renderização lenta e pode levar a um frame extra de latência devido ao buffer-stuffing (veja a figura 4). A biblioteca remove a renderização lenta e o frame extra de latência.

Frames de jogo longos

Figura 4. O frame longo B causa um ritmo incorreto para dois frames: A e B

A biblioteca resolve isso usando limites de sincronização (EGL_KHR_fence_sync e VkFence) para injetar esperas no aplicativo que permitem que o pipeline de exibição seja atualizado, em vez de permitir que a pressão de retorno aumente. O frame A ainda apresenta um frame extra, mas o frame B agora é apresentado corretamente, como mostrado na figura 5.

Esperas adicionadas à camada do aplicativo

Figura 5. Os frames C e D aguardam a apresentação

Modos operacionais compatíveis

Você pode configurar a biblioteca Frame Pacing para operar em um dos três modos a seguir:

  • Modo automático desativado + pipeline
  • Modo automático ativado + pipeline
  • Modo automático ativado + modo de pipeline automático (pipeline/não pipeline)

Você pode testar os modos automático e de pipeline, mas é necessário começar desativando ambos os modos e incluindo a seguinte informação depois de inicializar o Swappy:

  swappyAutoSwapInterval(false);
  swappyAutoPipelineMode(false);
  swappyEnableStats(false);
  swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);

Modo de pipeline

Para coordenar as cargas de trabalho do mecanismo, a biblioteca normalmente usa um modelo de pipeline que separa as cargas de trabalho da CPU e da GPU nos limites do VSYNC.

Modo de pipeline

Figura 6. Modo de pipeline

Modo não pipeline

Em geral, essa abordagem resulta em uma latência de tela de entrada mais baixa e previsível. Nos casos em que um jogo tem um tempo para a renderização do frame muito baixo, as cargas de trabalho da CPU e da GPU podem caber em um único intervalo de troca. Nesse caso, o uso do modo não pipeline forneceria uma latência de tela de entrada menor.

Modo não pipeline

Figura 7. Modo não pipeline

Modo automático

A maioria dos jogos não sabe como escolher o intervalo de troca, que é a duração da apresentação de cada frame (por exemplo, 33,3 ms para 30 Hz). Um jogo pode ser renderizado a 60 FPS em alguns dispositivos e cair para um valor mais baixo em outros. O modo automático mede os tempos de CPU e GPU para fazer o seguinte:

  • Selecionar automaticamente intervalos de troca: jogos que apresentam 30 Hz em algumas cenas e 60 Hz em outras podem permitir que a biblioteca ajuste esse intervalo de forma dinâmica.
  • Desativar o pipeline para frames em alta velocidade: apresenta a latência de tela de entrada ideal em todos os casos.

Diversas taxas de atualização

Os dispositivos compatíveis com várias taxas de atualização oferecem mais flexibilidade para escolher um intervalo de troca com uma aparência uniforme:

  • Em dispositivos de 60 Hz: 60 FPS / 30 FPS / 20 FPS
  • Em dispositivos de 60 Hz + 90 Hz: 90 FPS / 60 FPS / 45 FPS / 30 FPS
  • Em 60 Hz + 90 Hz + 120 Hz: 120 FPS / 90 FPS / 60 FPS / 45 FPS / 40 FPS / 30 FPS

A biblioteca escolhe a taxa de atualização que melhor corresponde à duração real da renderização dos frames do jogo, proporcionando uma melhor experiência visual.

Para ver mais informações sobre o ritmo de frame com diversas taxas de atualização, consulte a postagem do blog Renderização de alta taxa de atualização no Android (link em inglês).

Estatísticas de frames

A biblioteca Frame Pacing oferece as seguintes estatísticas para fins de depuração e caracterização de perfil:

  • Um histograma do número de atualizações de tela que um frame aguardou na fila do compositor após a conclusão da renderização.
  • Um histograma do número de atualizações de tela que ocorreram entre o momento de solicitação da apresentação e a hora atual.
  • Um histograma do número de atualizações de tela entre dois frames consecutivos.
  • Um histograma do número de atualizações de tela entre o início do trabalho da CPU para esse frame e a hora atual.

Próxima etapa

Consulte um dos guias a seguir para integrar a biblioteca Android Frame Pacing ao seu jogo: