Introdução
Nesta série de posts, vamos cobrir a mudança de Detroit: Become Human do PlayStation 4 para o PC.
Detroit: Become Human foi lançado para PlayStation 4 em maio de 2018. Começamos a trabalhar na versão para PC em julho de 2018 e a lançamos em dezembro de 2019. Este é um jogo de aventura com três personagens jogáveis e muitas histórias. Possui gráficos de altíssima qualidade e a maior parte da tecnologia gráfica foi desenvolvida pela própria Quantic Dream.
O motor 3D tem excelentes recursos:
- Renderização realista de personagens.
- Iluminação PBR.
- Pós-processamento de alta qualidade como profundidade de campo (DOF), desfoque de movimento e assim por diante.
- Anti-aliasing temporário.

Detroit: Torne-se Humano
Desde o início, o motor 3D do jogo foi projetado especificamente para o PlayStation, e não tínhamos ideia de que mais tarde seria compatível com outras plataformas. Portanto, a versão para PC foi um desafio para nós.
- O líder do motor 3D Ronan Marshalot e os líderes do motor 3D Nicholas Viseri e Jonathan Siret, da Quantic Dream , discutirão os aspectos de renderização do jogo portado. Eles explicarão quais otimizações podem ser facilmente transferidas do PlayStation 4 para o PC e quais dificuldades eles enfrentaram devido às diferenças entre as plataformas.
- Lou Kramer é engenheiro de desenvolvimento de tecnologia da AMD . Ela nos ajudou a otimizar o jogo, então ela falará em detalhes sobre a indexação de recursos não uniforme no PC e, em particular, em placas AMD.
Escolhendo uma API de gráficos
Já tínhamos uma versão OpenGL do motor, que usamos em nossas ferramentas de desenvolvimento.
Mas não queríamos lançar o jogo em OpenGL:
- Tínhamos muitas extensões proprietárias que não estavam abertas a todos os fabricantes de GPU.
- O motor teve desempenho muito baixo em OpenGL, embora, é claro, pudesse ser otimizado.
- Em OpenGL, existem muitas maneiras de implementar diferentes aspectos, por isso foi um pesadelo implementar diferentes aspectos corretamente em todas as plataformas.
- OpenGL . , , .
Devido ao uso intenso de recursos não relacionados, não foi possível portar o jogo para DirectX11. Ele não tem slots de recursos suficientes e seria muito difícil obter um desempenho decente se tivéssemos que refazer os shaders para usar menos recursos.
Estávamos escolhendo entre DirectX 12 e Vulkan, que têm um conjunto de recursos muito semelhante. Vulkan nos permitiria ainda fornecer suporte para Linux e telefones celulares, e DirectX 12 forneceria suporte para o Microsoft Xbox. Sabíamos que, eventualmente, precisaríamos implementar suporte para ambas as APIs, mas faria mais sentido que a porta se concentrasse em apenas uma API.
Vulkan oferece suporte a Windows 7 e Windows 8. Já que queríamos fazer Detroit: Torne-se Humanoacessível a tantos jogadores quanto possível, este se tornou um argumento muito forte. No entanto, a portabilidade demorou um ano, e este argumento já não é importante, porque o Windows 10 agora é muito usado!
Vários conceitos de API de gráficos
OpenGL e versões anteriores do DirectX têm um modelo de controle de GPU muito simples. Essas APIs são fáceis de entender e muito adequadas para o aprendizado. Eles instruem o driver a fazer muitos trabalhos que estão ocultos do desenvolvedor. Portanto, será muito difícil otimizar um mecanismo 3D totalmente funcional neles.
Por outro lado, a API do PlayStation 4 é muito leve e muito próxima do hardware.
Vulkan está em algum lugar no meio. Ele também tem abstrações porque é executado em GPUs diferentes, mas os desenvolvedores têm mais controle. Digamos que temos uma tarefa para implementar gerenciamento de memória ou cache de sombreador. Como resta menos trabalho para o motorista, temos que fazê-lo! Porém, desenvolvemos projetos no PlayStation e, portanto, é mais conveniente para nós quando podemos controlar tudo.
Dificuldades
A CPU do PlayStation 4 é um AMD Jaguar com 8 núcleos. Obviamente mais lento do que o hardware de PC mais recente; no entanto, o PlayStation 4 tem vantagens importantes, em particular, acesso muito rápido ao hardware. Acreditamos que a API gráfica do PlayStation 4 é muito mais eficiente do que todas as APIs no PC. Ele é muito direto e desperdiça poucos recursos. Isso significa que podemos alcançar um grande número de chamadas de desenho por quadro. Sabíamos que as chamadas de alto consumo podem ser um problema em PCs mais lentos.
Outra vantagem importante era que todos os shaders no PlayStation 4 podiam ser compilados antecipadamente, o que significava que eram carregados quase que instantaneamente. Em um PC, o driver deve compilar shaders no momento da inicialização: devido ao grande número de GPU e configurações de driver suportadas, este processo não pode ser feito com antecedência.
Durante o desenvolvimento de Detroit: Become Human no PlayStation 4, os artistas foram capazes de criar árvores shader exclusivas para todos os materiais. Isso produziu uma quantidade insana de sombreadores de vértice e pixel, então sabíamos desde o início da porta que isso seria um grande problema.
Shader pipelines
Como sabemos por nosso mecanismo OpenGL, compilar shaders pode consumir muito tempo em um PC. Durante a produção do jogo, geramos um cache de shader baseado no modelo de GPU de nossas estações de trabalho. Gerar um cache de shaders completo para Detroit: Become Human levou uma noite inteira! Todos os funcionários tiveram acesso a esse cache de shaders pela manhã. Mas o jogo ainda ficou lento, porque o driver precisava converter esse código no código assembler nativo do sombreador da GPU.
Descobriu-se que Vulkan lida com esse problema muito melhor do que OpenGL.
Primeiro, o Vulkan não usa diretamente uma linguagem de sombreador de alto nível como HLSL, mas em vez disso usa uma linguagem de sombreador intermediária chamada SPIR-V. O SPIR-V acelera a compilação do shader e facilita a otimização para o compilador do driver de shader. Na verdade, em termos de desempenho, é comparável ao sistema de cache de shader OpenGL.
No Vulkan, os shaders devem ser vinculados ao formulário
VkPipeline
. Por exemplo, VkPipeline
você pode criar a partir de um sombreador de vértice e pixel. Ele também contém informações de estado de renderização (testes de profundidade, estêncil, combinação, etc.) e formatos de destino de renderização. Essas informações são importantes para o driver para que ele possa compilar sombreadores da forma mais eficiente possível.
No OpenGL, a compilação de sombreadores não conhece o contexto de uso de sombreadores. O driver precisa esperar por uma chamada de desenho para gerar o binário da GPU, e é por isso que a primeira chamada de desenho com um novo sombreador pode demorar muito na CPU.
No Vulkan, o pipeline
VkPipeline
fornece um contexto de uso, de modo que o driver tenha todas as informações de que precisa para gerar o binário da GPU e a primeira chamada de desenho não desperdiça nenhum recurso. Além disso, podemos atualizar VkPipelineCache
na criação VkPipeline
.
Inicialmente, tentamos criar
VkPipelines
na primeira vez que precisamos. Isso causou lentidão semelhante à situação com os drivers OpenGL. Em seguida, ele foi VkPipelineCache
atualizado, e a frenagem desapareceu até a próxima chamada de empate.
Então, previmos que poderíamos criar
VkPipelines
na hora do boot, mas quando VkPipelineCache
era irrelevante, era tão lento que a estratégia de carregamento em segundo plano não poderia ser implementada.
Por fim, decidimos gerar tudo
VkPipeline
durante o primeiro lançamento do jogo. Isso eliminou completamente os problemas de frenagem, mas agora nos deparamos com uma nova dificuldade: a geração VkPipelineCache
demorou muito.
Detroit: Torne-se Humano contém aproximadamente 99.500
VkPipeline
! O jogo usa renderização direta, então os sombreadores de material contêm todo o código de iluminação. Portanto, a compilação de cada sombreador pode levar muito tempo.
Tivemos várias ideias para otimizar o processo:
- , SPIR-V.
- SPIR-V SPIR-V.
- , CPU 100%
VkPipeline
.
Além disso, uma otimização importante foi sugerida por Jeff Boltz da NVIDIA e, em nosso caso, ela se mostrou muito eficaz.
Muitos são
VkPipeline
muito semelhantes. Por exemplo, alguns VkPipeline
podem ter os mesmos sombreadores de vértice e pixel, diferindo apenas em alguns estados de renderização, como parâmetros de estêncil. Nesse caso, o driver pode tratá-los como um pipeline. Mas se os criarmos ao mesmo tempo, um dos threads simplesmente ficará inativo, esperando que o outro conclua a tarefa. Por sua natureza, nosso processo transmitiu todos os processos semelhantes VkPipeline
ao mesmo tempo. Para resolver esse problema, apenas alteramos a ordem de classificação VkPipeline
. Os “clones” foram colocados no final e, como resultado, sua criação passou a demorar muito menos tempo.
Desempenho de criação
VkPipelines
varia bastante. Em particular, é altamente dependente do número de threads de hardware disponíveis. No AMD Ryzen Threadripper com 64 threads de hardware, pode levar até dois minutos. Infelizmente, em PCs fracos, esse processo pode levar mais de 20 minutos.
Este último foi muito longo para nós. Infelizmente, a única maneira de reduzir ainda mais esse tempo era reduzir o número de sombreadores. Precisaríamos mudar a maneira como criamos materiais para que o maior número possível de materiais seja compartilhado. Para Detroit: Become Human, isso era impossível porque os artistas teriam que refazer todos os materiais. Planejamos implementar a instanciação material adequada no próximo jogo, mas era tarde demais para Detroit: Torne-se Humano .
Descritores de indexação
Para otimizar a velocidade das chamadas draw no PC, utilizou-se a indexação dos descritores através da extensão
VK_EXT_descriptor_indexing
. Seu princípio é simples: podemos criar um conjunto de descritores contendo todos os buffers e texturas usados na moldura. Então podemos acessar os buffers e texturas por meio de índices. A principal vantagem disso é que os recursos são limitados apenas uma vez por quadro, mesmo se usados em várias chamadas de desenho. Isso é muito semelhante ao uso de recursos não acoplados em OpenGL.
Criamos matrizes de recursos para todos os tipos de recursos usados:
- Um array para todas as texturas 2D.
- Uma matriz para todas as texturas 3D.
- Um array para todas as texturas cúbicas.
- Um array para todos os buffers de material.
Temos apenas um buffer principal que muda entre as chamadas de desenho (ele é implementado como um buffer circular) contendo um índice descritor que se refere ao buffer de material desejado e às matrizes necessárias. Cada buffer de material contém índices das texturas usadas.

Graças a esta estratégia, conseguimos manter um pequeno número de conjuntos de descritores comuns a todas as chamadas de desenho e contendo todas as informações necessárias para desenhar o quadro.
Otimizando as atualizações do conjunto de descritores
Mesmo com um pequeno número de conjuntos de descritores, atualizá-los ainda era um gargalo. Atualizar um conjunto de descritores pode ser muito caro se ele contiver muitos recursos. Por exemplo, em um quadro de Detroit: Torne-se Humano, pode haver mais de quatro mil texturas.
Implementamos atualizações incrementais nos conjuntos de descritores, mantendo o controle dos recursos que se tornam visíveis e invisíveis no quadro atual. Além disso, isso limita o tamanho das matrizes de descritores, porque elas têm capacidade suficiente para lidar com os recursos visíveis no momento. Rastrear a visibilidade desperdiça poucos recursos porque não usamos um algoritmo caro para calcular cruzamentos com
O(n.log(n))
... Em vez disso, usamos duas listas, uma para o quadro atual e outra para o anterior. Mover os recursos visíveis restantes de uma lista para outra e examinar os recursos restantes na primeira lista ajuda a determinar quais recursos entram e desaparecem da pirâmide.
Os deltas obtidos durante esses cálculos são armazenados por quatro quadros - usamos buffer triplo e, para calcular os vetores de movimento de objetos com esfola, é necessário mais um quadro. O conjunto de descritores deve permanecer inalterado por pelo menos quatro quadros antes de ser modificado novamente, pois ainda pode ser útil para a GPU. Portanto, aplicamos deltas a grupos de quatro quadros.
Em última análise, essa otimização reduziu o tempo de atualização para conjuntos de descritores em uma a duas ordens de magnitude.
Butching primitivas
Usar a indexação do descritor nos permite agrupar em lote várias primitivas em uma única chamada de desenho usando
vkCmdDrawIndexedIndirect
. Usamos gl_InstanceID
para acessar os índices desejados no buffer principal. Os primitivos podem ser agrupados em lotes se tiverem o mesmo conjunto de descritores, o mesmo pipeline de sombreador e o mesmo buffer de vértice. Isso é muito eficaz, especialmente durante passagens de profundidade e sombra. O número total de chamadas de sorteio é reduzido em 60%.
Isso conclui a primeira parte da série de artigos. Na Parte 2, o engenheiro de tecnologia Lou Kramer falará sobre a indexação de recursos heterogêneos em PCs e placas AMD em particular.