Dicas de criação de perfil de desempenho para desenvolvedores de jogos
Um desempenho suave é essencial para criar experiências de jogo imersivas para os jogadores. Ao traçar o perfil e aprimorar o desempenho do seu jogo em uma ampla variedade de plataformas e dispositivos, você pode expandir sua base de jogadores e aumentar suas chances de sucesso.
Esta página descreve um fluxo de trabalho geral de criação de perfil para desenvolvedores de jogos. Ele foi extraído do e-book Ultimate guide to profiling Unity games, disponível para download gratuito. O e-book foi criado por especialistas externos e internos da Unity em desenvolvimento, criação de perfil e otimização de jogos.
Continue lendo para saber mais sobre metas úteis a serem definidas com a criação de perfis, gargalos comuns de desempenho, como limitação de CPU ou GPU, e como identificar e investigar essas situações com mais detalhes.
Medir a taxa de quadros do seu jogo em quadros por segundo (fps) não é ideal para proporcionar experiências consistentes aos jogadores. Considere o seguinte cenário simplificado:
Durante o tempo de execução, seu jogo renderiza 59 quadros em 0,75 segundos. No entanto, o próximo quadro leva 0,25 segundo para ser renderizado. A taxa de quadros média de 60 fps parece boa, mas, na realidade, os jogadores perceberão um efeito de gagueira, pois o último quadro leva um quarto de segundo para ser renderizado.
Esse é um dos motivos pelos quais é importante ter como meta um orçamento de tempo específico por quadro. Isso lhe dá uma meta sólida para trabalhar ao traçar o perfil e otimizar seu jogo e, por fim, cria uma experiência mais suave e consistente para seus jogadores.
Cada quadro terá um orçamento de tempo com base em sua meta de fps. Um aplicativo que visa 30 fps deve sempre levar menos de 33,33 ms por quadro (1000 ms / 30 fps). Da mesma forma, uma meta de 60 fps deixa 16,66 ms por quadro (1000 ms / 60 fps).
Você pode exceder esse orçamento durante sequências não interativas, por exemplo, ao exibir menus da interface do usuário ou carregar cenas, mas não durante o jogo. Até mesmo um único quadro que exceda o orçamento de quadros alvo causará problemas.
Observação: Uma taxa de quadros consistentemente alta em jogos de RV é essencial para evitar causar náusea ou desconforto aos jogadores. Sem ele, você corre o risco de ser rejeitado pelo detentor da plataforma durante a certificação do seu jogo.
Quadros por segundo: Uma métrica enganosa
Uma maneira comum de os jogadores medirem o desempenho é por meio da taxa de quadros, ou quadros por segundo. No entanto, é recomendável que você use o tempo do quadro em milissegundos. Para entender o motivo, observe o gráfico acima de fps versus tempo de quadro.
Considere estes números:
1000 ms/s / 900 fps = 1,111 ms por quadro
1000 ms/s / 450 fps = 2,222 ms por quadro
1000 ms/s / 60 fps = 16,666 ms por quadro
1000 ms/s / 56,25 fps = 17,777 ms por quadro
Se o seu aplicativo estiver sendo executado a 900 fps, isso se traduz em um tempo de quadro de 1,111 milissegundos por quadro. A 450 fps, isso significa 2,222 milissegundos por quadro. Isso representa uma diferença de apenas 1,111 milissegundos por quadro, embora a taxa de quadros pareça cair pela metade.
Se você observar as diferenças entre 60 fps e 56,25 fps, isso se traduz em 16,666 milissegundos por quadro e 17,777 milissegundos por quadro, respectivamente. Isso também representa 1,111 milissegundos a mais por quadro, mas aqui a queda na taxa de quadros parece muito menos dramática em termos de porcentagem.
É por isso que os desenvolvedores usam o tempo médio de quadro para avaliar a velocidade do jogo em vez de fps.
Não se preocupe com os fps, a menos que você fique abaixo da taxa de quadros desejada. Concentre-se no tempo do quadro para medir a velocidade de execução do jogo e, em seguida, mantenha-se dentro do orçamento de quadros.
Leia o artigo original, "Robert Dunlop's fps versus frame time", para obter mais informações.
O controle térmico é uma das áreas mais importantes a serem otimizadas no desenvolvimento de aplicativos para dispositivos móveis. Se a CPU ou a GPU passar muito tempo trabalhando em aceleração máxima devido a um design ineficiente, esses chips ficarão quentes. Para evitar danos aos chips (e a possibilidade de queimar as mãos do jogador!), o sistema operacional reduzirá a velocidade do relógio do dispositivo para permitir que ele esfrie, causando falhas nos quadros e uma experiência ruim para o usuário. Essa redução de desempenho é conhecida como estrangulamento térmico.
Taxas de quadros mais altas e maior execução de código (ou operações de acesso à DRAM) levam a um maior consumo de bateria e geração de calor. O desempenho ruim também pode excluir segmentos inteiros de dispositivos móveis de baixo custo, o que pode levar à perda de oportunidades de mercado e, portanto, à redução das vendas.
Ao assumir o problema das térmicas, considere o orçamento com o qual você tem que trabalhar como um orçamento de todo o sistema.
Combata o estrangulamento térmico e o consumo de bateria aproveitando uma técnica de criação de perfil inicial para otimizar seu jogo desde o início. Defina as configurações do projeto para o hardware da plataforma de destino para combater problemas térmicos e de consumo de bateria.
Ajuste os orçamentos de quadros no celular
Deixar um tempo de inatividade do quadro em torno de 35% é a recomendação típica para combater os problemas térmicos do dispositivo durante longos períodos de jogo. Isso dá aos chips móveis tempo para esfriar e ajuda a evitar o consumo excessivo da bateria. Usando um tempo de quadro alvo de 33,33 ms por quadro (para 30 fps), um orçamento de quadro típico para dispositivos móveis será de aproximadamente 22 ms por quadro.
O cálculo é o seguinte: (1000 ms / 30) * 0,65 = 21,66 ms
Para atingir 60 fps no celular usando o mesmo cálculo, seria necessário um tempo de quadro alvo de (1000 ms / 60) * 0,65 = 10,83 ms. Isso é difícil de conseguir em muitos dispositivos móveis e consumiria a bateria duas vezes mais rápido do que a segmentação de 30 fps. Por esses motivos, a maioria dos jogos para celular tem como meta 30 fps em vez de 60. Use Application.targetFrameRate para controlar essa configuração e consulte a seção "Definir um orçamento de quadro" no e-book para obter mais detalhes sobre o tempo de quadro.
O escalonamento de frequência em chips móveis pode dificultar a identificação das alocações de orçamento de tempo ocioso do quadro durante a criação de perfis. Seus aprimoramentos e otimizações podem ter um efeito líquido positivo, mas o dispositivo móvel pode estar reduzindo a frequência e, como resultado, funcionando de forma mais fria. Use ferramentas personalizadas, como o FTrace ou o Perfetto, para monitorar as frequências de chips móveis, o tempo ocioso e o dimensionamento antes e depois das otimizações.
Contanto que você permaneça dentro do seu orçamento de tempo total de quadros para a sua meta de fps (33,33 ms para 30 fps) e veja seu dispositivo trabalhando menos ou registrando temperaturas mais baixas para manter essa taxa de quadros, então você está no caminho certo.
Outro motivo para adicionar espaço de manobra ao orçamento de quadros em dispositivos móveis é levar em conta as flutuações de temperatura do mundo real. Em um dia quente, um dispositivo móvel aquece e tem dificuldade para dissipar o calor, o que pode levar a um estrangulamento térmico e a um desempenho ruim nos jogos. Reservar uma porcentagem do orçamento do quadro ajudará a evitar esse tipo de situação.
O acesso à DRAM é normalmente uma operação que consome muita energia em dispositivos móveis. A recomendação de otimização da Arm para conteúdo gráfico em dispositivos móveis diz que o acesso à memória LPDDR4 custa aproximadamente 100 picojoules por byte.
Reduzir o número de operações de acesso à memória por quadro:
- Redução da taxa de quadros
- Reduzir a resolução da tela sempre que possível
- Uso de malhas mais simples com número reduzido de vértices e precisão de atributos
- Uso de compressão de textura e mipmapping
Quando você precisa se concentrar em dispositivos que utilizam hardware Arm ou Arm Mali, as ferramentas do Arm Mobile Studio (especificamente o Streamline Performance Analyzer) incluem alguns contadores de desempenho excelentes para identificar problemas de largura de banda da memória. Os contadores são listados e explicados para cada geração de GPU Arm, por exemplo, Mali-G78. Observe que a criação de perfil de GPU do Mobile Studio requer o Arm Mali.
Estabelecer níveis de hardware para benchmarking
Além de usar ferramentas de criação de perfis específicas da plataforma, estabeleça níveis ou um dispositivo de especificação mais baixa para cada plataforma e nível de qualidade que você deseja suportar e, em seguida, crie perfis e otimize o desempenho para cada uma dessas especificações.
Por exemplo, se estiver visando a plataformas móveis, você pode decidir oferecer suporte a três níveis com controles de qualidade que ativam ou desativam recursos com base no hardware de destino. Em seguida, você otimiza para a menor especificação de dispositivo em cada camada. Como outro exemplo, se estiver desenvolvendo um jogo para o PlayStation 4 e o PlayStation 5, certifique-se de criar um perfil em ambos.
Para obter um guia completo de otimização para dispositivos móveis, dê uma olhada em Otimize o desempenho de seu jogo para celular. Este e-book tem muitas dicas e truques que o ajudarão a reduzir o estrangulamento térmico e aumentar a vida útil da bateria dos dispositivos móveis que executam seus jogos.
Uma abordagem de cima para baixo funciona bem ao criar perfis, começando com o Deep Profiling desativado. Use essa abordagem de alto nível para coletar dados e fazer anotações sobre quais cenários causam alocações gerenciadas indesejadas ou muito tempo de CPU nas principais áreas de loop do jogo.
Você precisará primeiro reunir pilhas de chamadas para marcadores GC.Alloc. Se não estiver familiarizado com esse processo, encontre algumas dicas e truques na seção "Localizando alocações de memória recorrentes durante a vida útil do aplicativo" em Guia definitivo para criação de perfil em jogos Unity.
Se as pilhas de chamadas relatadas não forem detalhadas o suficiente para rastrear a origem das alocações ou outras lentidões, você poderá executar uma segunda sessão de criação de perfil com a opção Deep Profiling ativada para encontrar a origem das alocações.
Ao coletar anotações sobre os "infratores" de tempo do quadro, não se esqueça de observar como eles se comparam em relação ao restante do quadro. Esse impacto relativo será afetado pela ativação do Deep Profiling.
Leia mais sobre a criação de perfis profundos em Guia definitivo para criação de perfil em jogos Unity.
Perfil inicial
Os melhores ganhos com a criação de perfis são obtidos quando você começa logo no início do ciclo de vida de desenvolvimento do projeto.
Crie perfis com antecedência e com frequência para que você e sua equipe entendam e memorizem uma "assinatura de desempenho" para o projeto. Se o desempenho despencar, você poderá identificar facilmente quando as coisas estão erradas e solucionar o problema.
Os resultados mais precisos da criação de perfil sempre vêm da execução e da criação de perfil em dispositivos de destino, juntamente com o aproveitamento de ferramentas específicas da plataforma para analisar as características de hardware de cada plataforma. Essa combinação lhe proporcionará uma visão holística do desempenho dos aplicativos em todos os seus dispositivos de destino.
Faça o download da versão em PDF para impressão dessa tabela aqui.
Em algumas plataformas, é fácil determinar se o aplicativo está vinculado à CPU ou à GPU. Por exemplo, ao executar um jogo para iOS a partir do Xcode, o painel fps mostra um gráfico de barras com o tempo total da CPU e da GPU para que você possa ver qual é o mais alto. O tempo de CPU inclui o tempo gasto aguardando o VSync, que está sempre ativado em dispositivos móveis.
No entanto, em algumas plataformas, pode ser difícil obter dados de tempo da GPU. Felizmente, o Unity Profiler mostra informações suficientes para identificar o local dos gargalos de desempenho. O fluxograma acima ilustra o processo inicial de criação de perfil, e as seções seguintes fornecem informações detalhadas sobre cada etapa. Eles também apresentam capturas do Profiler de projetos reais do Unity para ilustrar os tipos de coisas a serem procuradas.
Para obter uma imagem holística de toda a atividade da CPU, inclusive quando ela estiver aguardando a GPU, use a visualização Timeline no módulo CPU do Profiler. Familiarize-se com os marcadores comuns do Profiler para interpretar as capturas corretamente. Alguns dos marcadores do Profiler podem aparecer de forma diferente dependendo da plataforma de destino, portanto, dedique algum tempo a explorar as capturas do seu jogo em cada uma das plataformas de destino para ter uma ideia de como é uma captura "normal" para o seu projeto.
O desempenho de um projeto é limitado pelo chip e/ou thread que leva mais tempo. Essa é a área em que você deve concentrar seus esforços de otimização. Por exemplo, imagine um jogo com um orçamento de tempo de quadro alvo de 33,33 ms e VSync ativado:
- Se o tempo de quadro da CPU (excluindo o VSync) for de 25 ms e o tempo da GPU for de 20 ms, não há problema! Você está limitado pela CPU, mas tudo está dentro do orçamento, e otimizar as coisas não melhorará a taxa de quadros (a menos que você consiga que a CPU e a GPU fiquem abaixo de 16,66 ms e salte para 60 fps).
- Se o tempo de quadro da CPU for de 40 ms e o da GPU for de 20 ms, você está limitado à CPU e precisará otimizar o desempenho da CPU. A otimização do desempenho da GPU não ajudará; na verdade, talvez você queira transferir parte do trabalho da CPU para a GPU, por exemplo, usando shaders de computação em vez de código C# para algumas coisas, para equilibrar as coisas.
- Se o tempo de quadro da CPU for de 20 ms e o da GPU for de 40 ms, você está vinculado à GPU e precisa otimizar o trabalho da GPU.
- Se a CPU e a GPU estiverem a 40 ms, você estará limitado por ambas e precisará otimizar ambas abaixo de 33,33 ms para atingir 30 fps.
Consulte estes recursos que exploram mais detalhadamente o fato de estar vinculado à CPU ou à GPU:
A criação de perfis e a otimização do seu projeto desde o início e com frequência durante o desenvolvimento o ajudarão a garantir que todos os threads de CPU do aplicativo e o tempo total do quadro da GPU estejam dentro do orçamento do quadro.
Acima está uma imagem de uma captura do Profiler de um jogo Unity para dispositivos móveis desenvolvido por uma equipe que fazia perfis e otimização contínuos. O jogo tem como meta 60 fps em telefones celulares de alta especificação e 30 fps em telefones de média/baixa especificação, como o desta captura.
Observe como quase metade do tempo no quadro selecionado é ocupado pelo marcador amarelo WaitForTargetfps Profiler. O aplicativo definiu Application.targetFrameRate como 30 fps e o VSync está ativado. O trabalho de processamento real no thread principal termina por volta da marca de 19 ms, e o restante do tempo é gasto aguardando o restante dos 33,33 ms antes de iniciar o próximo quadro. Embora esse tempo seja representado por um marcador do Profiler, o thread principal da CPU fica essencialmente ocioso durante esse período, permitindo que a CPU resfrie e usando um mínimo de energia da bateria.
O marcador a ser observado pode ser diferente em outras plataformas ou se o VSync estiver desativado. O importante é verificar se o thread principal está sendo executado dentro do seu orçamento de quadros ou exatamente dentro do seu orçamento de quadros, com algum tipo de marcador que indique que o aplicativo está aguardando o VSync e se os outros threads têm algum tempo ocioso.
O tempo ocioso é representado por marcadores cinza ou amarelos do Profiler. A captura de tela acima mostra que o thread de renderização está ocioso em Gfx.WaitForGfxCommandsFromMainThread, o que indica os momentos em que ele terminou de enviar draw calls para a GPU em um quadro e está aguardando mais solicitações de draw call da CPU no próximo. Da mesma forma, embora o thread Job Worker 0 passe algum tempo em Canvas.GeometryJob, na maior parte do tempo ele está ocioso. Todos esses são sinais de um aplicativo que está confortavelmente dentro do orçamento do quadro.
Se seu jogo estiver dentro do orçamento
Se você estiver dentro do orçamento de quadros, incluindo quaisquer ajustes feitos no orçamento para levar em conta o uso da bateria e o estrangulamento térmico, você terminou a criação de perfil de desempenho até a próxima vez - parabéns. Considere a possibilidade de executar o Memory Profiler para garantir que o aplicativo também esteja dentro do seu orçamento de memória.
A imagem acima mostra um jogo sendo executado confortavelmente dentro do limite de quadros de ~22 ms necessário para 30 fps. Observe o WaitForTargetfps preenchendo o tempo do thread principal até o VSync e os tempos ociosos em cinza no thread de renderização e no thread de trabalho. Observe também que o intervalo VBlank pode ser observado observando os tempos finais de Gfx.Present quadro a quadro, e que você pode desenhar uma escala de tempo na área Timeline ou na régua de tempo na parte superior para medir de um deles para o próximo.
Se o jogo não estiver dentro do orçamento de quadros da CPU, a próxima etapa é investigar qual parte da CPU é o gargalo, em outras palavras, qual thread está mais ocupado. O objetivo da criação de perfil é identificar os gargalos como alvos de otimização; se você confiar em suposições, poderá acabar otimizando partes do jogo que não são gargalos, resultando em pouca ou nenhuma melhoria no desempenho geral. Algumas "otimizações" podem até piorar o desempenho geral do jogo.
É raro que toda a carga de trabalho da CPU seja o gargalo. As CPUs modernas têm vários núcleos diferentes, capazes de realizar o trabalho de forma independente e simultânea. Diferentes threads podem ser executados em cada núcleo da CPU. Um aplicativo Unity completo usa uma série de threads para diferentes finalidades, mas os threads mais comuns para encontrar problemas de desempenho são:
- A linha principal: É aqui que toda a lógica/scripts do jogo executam seu trabalho por padrão e onde a maior parte do tempo é gasta com recursos e sistemas como física, animação, interface do usuário e renderização.
- A linha de renderização: Durante o processo de renderização, o thread principal examina a cena e executa a seleção de câmeras, a classificação de profundidade e o agrupamento de chamadas de desenho, resultando em uma lista de itens a serem renderizados. Essa lista é passada para o thread de renderização, que a traduz da representação independente de plataforma interna do Unity para as chamadas específicas da API de gráficos necessárias para instruir a GPU em uma plataforma específica.
- Os threads de trabalho: Os desenvolvedores podem usar o C# Job System para programar determinados tipos de trabalho a serem executados em threads de trabalho, o que reduz a carga de trabalho no thread principal. Alguns dos sistemas e recursos do Unity também fazem uso do sistema de trabalho, como física, animação e renderização.
Linha principal
A imagem acima mostra como as coisas podem parecer em um projeto vinculado ao thread principal. Este projeto está sendo executado em um Meta Quest 2, que normalmente tem como meta orçamentos de quadros de 13,88 ms (72 fps) ou até mesmo 8,33 ms (120 fps), porque altas taxas de quadros são importantes para evitar enjoo em dispositivos de RV. No entanto, mesmo que o objetivo do jogo fosse atingir 30 fps, está claro que o projeto está com problemas.
Embora o thread de renderização e os threads de trabalho sejam semelhantes ao exemplo que está dentro do orçamento do quadro, o thread principal está claramente ocupado com o trabalho durante todo o quadro. Mesmo considerando a pequena quantidade de sobrecarga do Profiler no final do quadro, o thread principal fica ocupado por mais de 45 ms, o que significa que esse projeto atinge taxas de quadros inferiores a 22 fps. Não há nenhum marcador que mostre a thread principal esperando o VSync ociosamente; ela está ocupada durante todo o quadro.
A próxima etapa da investigação é identificar as partes do quadro que levam mais tempo e entender por que isso acontece. Nesse quadro, PostLateUpdate.FinishFrameRendering leva 16,23 ms, mais do que todo o orçamento do quadro. Uma inspeção mais detalhada revela que há cinco instâncias de um marcador chamado Inl_RenderCameraStack, indicando que há cinco câmeras ativas e renderizando a cena. Como cada câmera no Unity invoca todo o pipeline de renderização, incluindo seleção, classificação e agrupamento, a tarefa de maior prioridade para esse projeto é reduzir o número de câmeras ativas, idealmente para apenas uma.
BehaviourUpdate, o marcador que engloba todos os métodos MonoBehaviour Update(), leva 7,27 ms, e as seções magenta da linha do tempo indicam onde os scripts alocam a memória heap gerenciada. Mudar para a visualização Hierarchy e filtrar digitando GC.Alloc na barra de pesquisa mostra que a alocação dessa memória leva cerca de 0,33 ms nesse quadro. No entanto, essa é uma medida imprecisa do impacto que as alocações de memória têm no desempenho da CPU.
Os marcadores GC.Alloc não são realmente cronometrados medindo-se o tempo de um ponto inicial a um ponto final. Para manter a sobrecarga pequena, eles são registrados apenas com o carimbo de data e hora de início, mais o tamanho da alocação. O Profiler atribui uma quantidade mínima de tempo a eles para garantir que estejam visíveis. A alocação real pode demorar mais, especialmente se for necessário solicitar um novo intervalo de memória ao sistema. Para ver o impacto com mais clareza, coloque marcadores do Profiler em torno do código que faz a alocação e, na criação profunda de perfil, as lacunas entre as amostras GC.Alloc de cor magenta na visualização da linha do tempo fornecem alguma indicação de quanto tempo elas podem ter levado.
Além disso, a alocação de nova memória pode ter efeitos negativos no desempenho que são mais difíceis de medir e atribuir diretamente a eles:
- A solicitação de nova memória do sistema pode afetar o orçamento de energia em um dispositivo móvel, o que pode fazer com que o sistema diminua a velocidade da CPU ou da GPU.
- A nova memória provavelmente precisa ser carregada no cache L1 da CPU e, portanto, empurra as linhas de cache existentes.
- A coleta de lixo incremental ou síncrona pode ser acionada diretamente ou com um atraso, pois o espaço livre existente na memória gerenciada acaba sendo excedido.
No início do quadro, quatro instâncias de Physics.FixedUpdate somam 4,57 ms. Posteriormente, o LateBehaviourUpdate (chamadas para MonoBehaviour.LateUpdate()) leva 4 ms, e os Animators são responsáveis por cerca de 1 ms.
Para garantir que esse projeto atinja o orçamento e a taxa de quadros desejados, todos esses problemas de thread principal precisam ser investigados para encontrar otimizações adequadas. Os maiores ganhos de desempenho serão obtidos com a otimização das coisas que levam mais tempo.
As áreas a seguir costumam ser lugares frutíferos para procurar otimização em projetos vinculados ao thread principal:
- Física
- Atualizações do script MonoBehaviour
- Alocação e/ou coleta de lixo
- Seleção e renderização de câmeras
- Loteamento ruim de chamadas de sorteio
- Atualizações, layouts e reconstruções da interface do usuário
- Animação
Dependendo do problema que você deseja investigar, outras ferramentas também podem ser úteis:
- Para scripts MonoBehaviour que demoram muito, mas não mostram exatamente por que isso acontece, adicione marcadores do Profiler ao código ou tente criar um perfil profundo para ver a pilha de chamadas completa.
- Para scripts que alocam memória gerenciada, ative as pilhas de chamadas de alocação para ver exatamente de onde vêm as alocações. Como alternativa, ative o Deep Profiling ou use o Project Auditor, que mostra problemas de código filtrados por memória, para que você possa identificar todas as linhas de código que resultam em alocações gerenciadas.
- Use o depurador de quadros para investigar as causas da má distribuição de chamadas de desenho.
Para obter dicas abrangentes sobre como otimizar seu jogo, baixe estes guias especializados do Unity gratuitamente:
- Otimize o desempenho do seu jogo para dispositivos móveis
- Otimize o desempenho do seu jogo para console e PC
A captura de tela acima é de um projeto que está vinculado ao seu thread de renderização. Este é um jogo de console com um ponto de vista isométrico e um orçamento de quadro alvo de 33,33 ms.
A captura do Profiler mostra que, antes que a renderização possa começar no quadro atual, o thread principal aguarda o thread de renderização, conforme indicado pelo marcador Gfx.WaitForPresentOnGfxThread. A thread de renderização ainda está enviando comandos de chamada de desenho do quadro anterior e não está pronta para aceitar novas chamadas de desenho da thread principal; a thread de renderização está gastando tempo em Camera.Render.
É possível distinguir entre os marcadores relacionados ao quadro atual e os marcadores de outros quadros, pois os últimos aparecem mais escuros. Você também pode ver que, quando o thread principal consegue continuar e começa a emitir chamadas de desenho para o thread de renderização processar, o thread de renderização leva mais de 100 ms para processar o quadro atual, o que também cria um gargalo durante o próximo quadro.
Uma investigação mais aprofundada mostrou que esse jogo tinha uma configuração de renderização complexa, envolvendo nove câmeras diferentes e muitas passagens extras causadas por shaders de substituição. O jogo também estava renderizando mais de 130 luzes pontuais usando um caminho de renderização avançado, o que pode adicionar várias chamadas de desenho transparentes adicionais para cada luz. No total, esses problemas se combinaram para criar mais de 3.000 chamadas de desenho por quadro.
A seguir, estão as causas comuns a serem investigadas em projetos que são renderizados com thread-bound:
- Loteamento de chamadas de desenho ruim, especialmente em APIs gráficas mais antigas, como OpenGL ou DirectX 11
- Muitas câmeras. A menos que esteja criando um jogo multijogador com tela dividida, é provável que você tenha apenas uma câmera ativa.
- Seleção deficiente, resultando em um número excessivo de coisas desenhadas. Investigue as dimensões de frustum da sua câmera e as máscaras de camada de seleção. Considere a possibilidade de ativar o Occlusion Culling. Talvez até mesmo crie seu próprio sistema simples de oclusão com base no que você sabe sobre como os objetos estão dispostos em seu mundo. Observe quantos objetos que projetam sombras existem na cena - a seleção de sombras ocorre em uma passagem separada da seleção "regular".
O módulo Rendering Profiler mostra uma visão geral do número de lotes de chamadas de desenho e de chamadas SetPass a cada quadro. A melhor ferramenta para investigar quais lotes de draw call seu thread de renderização está emitindo para a GPU é o Frame Debugger.
Projetos vinculados a threads de CPU que não sejam os threads principais ou de renderização não são tão comuns. No entanto, isso pode ocorrer se o seu projeto usar a pilha de tecnologia orientada a dados (DOTS), especialmente se o trabalho for transferido do thread principal para threads de trabalho usando o sistema de trabalho C#.
A captura vista acima é do modo Play no Editor, mostrando um projeto DOTS executando uma simulação de fluido de partículas na CPU.
À primeira vista, parece um sucesso. Os threads de trabalho estão repletos de trabalhos compilados pelo Burst, indicando que uma grande quantidade de trabalho foi removida do thread principal. Normalmente, essa é uma decisão acertada.
No entanto, nesse caso, o tempo de quadro de 48,14 ms e o marcador cinza WaitForJobGroupID de 35,57 ms no thread principal são sinais de que nem tudo está bem. WaitForJobGroupID indica que o thread principal programou trabalhos para serem executados de forma assíncrona em threads de trabalho, mas precisa dos resultados desses trabalhos antes que os threads de trabalho terminem de executá-los. Os marcadores azuis do Profiler abaixo de WaitForJobGroupID mostram o thread principal executando trabalhos enquanto espera, em uma tentativa de garantir que os trabalhos terminem mais cedo.
Embora os trabalhos sejam compilados pelo Burst, eles ainda estão fazendo muito trabalho. Talvez a estrutura de consulta espacial usada por esse projeto para descobrir rapidamente quais partículas estão próximas umas das outras deva ser otimizada ou trocada por uma estrutura mais eficiente. Ou, os trabalhos de consulta espacial podem ser agendados para o final do quadro em vez do início, com os resultados não necessários até o início do próximo quadro. Talvez esse projeto esteja tentando simular um número excessivo de partículas. É necessária uma análise mais aprofundada do código dos trabalhos para encontrar a solução, portanto, adicionar marcadores do Profiler mais detalhados pode ajudar a identificar as partes mais lentas.
Os trabalhos em seu projeto podem não ser tão paralelos como neste exemplo. Talvez você tenha apenas um trabalho longo em execução em um único thread de trabalho. Não há problema, desde que o tempo entre o agendamento do trabalho e o momento em que ele precisa ser concluído seja longo o suficiente para que o trabalho seja executado. Se não for, você verá o thread principal parar enquanto aguarda a conclusão do trabalho, como na captura de tela acima.
As causas comuns de pontos de sincronização e gargalos de thread de trabalho incluem:
- Trabalhos que não estão sendo compilados pelo compilador Burst
- Trabalhos de longa duração em um único thread de trabalho em vez de serem paralelizados em vários threads de trabalho
- Tempo insuficiente entre o momento no quadro em que um trabalho é programado e o momento em que o resultado é necessário
- Vários "pontos de sincronização" em um quadro, que exigem que todos os trabalhos sejam concluídos imediatamente
Você pode usar o recurso Flow Events na visualização Timeline do módulo CPU Usage Profiler para investigar quando os trabalhos são agendados e quando seus resultados são esperados pelo thread principal. Para obter mais informações sobre como escrever um código DOTS eficiente, consulte a seção Práticas recomendadas do DOTS do DOTS.
Seu aplicativo está vinculado à GPU se o thread principal passar muito tempo em marcadores do Profiler, como Gfx.WaitForPresentOnGfxThread, e seu thread de renderização exibir simultaneamente marcadores como Gfx.PresentFrame ou <GraphicsAPIName>.WaitForLastPresent.
A captura a seguir foi feita em um Samsung Galaxy S7, usando a API gráfica Vulkan. Embora parte do tempo gasto em Gfx.PresentFrame nesse exemplo possa estar relacionado à espera pelo VSync, a duração extrema desse marcador do Profiler indica que a maior parte desse tempo é gasta aguardando que a GPU termine de renderizar o quadro anterior.
Nesse jogo, determinados eventos de jogabilidade acionaram o uso de um sombreador que triplicou o número de draw calls renderizados pela GPU. Os problemas comuns a serem investigados ao criar perfis de desempenho da GPU incluem:
- Efeitos caros de pós-processamento em tela cheia, incluindo os culpados mais comuns, como Ambient Occlusion e Bloom
- Shaders de fragmentos caros causados por:
- Lógica de ramificação
- Usar precisão total de flutuação em vez de meia precisão
- Uso excessivo de registros que afetam a ocupação da frente de onda das GPUs
- Excesso de desenho na fila de renderização Transparente causado por efeitos ineficientes de interface do usuário, sistemas de partículas ou pós-processamento
- Resoluções de tela excessivamente altas, como as encontradas em telas 4K ou telas Retina em dispositivos móveis
- Micro triângulos causados por geometria de malha densa ou falta de LODs, o que é um problema específico em GPUs móveis, mas também pode afetar GPUs de PC e console
- Falhas de cache e desperdício de largura de banda da memória da GPU causados por texturas não compactadas ou texturas de alta resolução sem mipmaps
- Shaders de geometria ou de tesselação, que podem ser executados várias vezes por quadro se as sombras dinâmicas estiverem ativadas
Se o seu aplicativo parecer estar vinculado à GPU, você poderá usar o depurador de quadros como uma maneira rápida de entender os lotes de draw call que estão sendo enviados à GPU. No entanto, essa ferramenta não pode apresentar nenhuma informação específica de tempo da GPU, apenas como a cena geral é construída.
A melhor maneira de investigar a causa dos gargalos da GPU é examinar uma captura de GPU de um profiler de GPU adequado. A ferramenta a ser usada depende do hardware de destino e da API gráfica escolhida.
Baixe o e-book, Ultimate guide to profiling Unity games, gratuitamente para obter todas as dicas e práticas recomendadas.