O conteúdo deste site foi traduzido usando inteligência artificial (IA) ou tecnologia de tradução automática e pode conter erros.

Skip to content

Criação de um cache de pipeline robusto com o Vulkan

Há muitos conceitos novos a serem aprendidos ao criar um renderizador Vulkan. Alguns deles são mais fáceis de lidar do que outros, e uma das adições mais diretas é o cache de pipeline. Para garantir que a criação do pipeline seja o mais eficiente possível, você precisa criar um cache de pipeline e usá-lo sempre que precisar criar um novo pipeline. Para garantir que as execuções subsequentes do seu aplicativo não precisem perder tempo compilando repetidamente o microcódigo do shader, é necessário salvar os dados do cache de pipeline em um arquivo. Em seguida, carregue-os na próxima vez que o aplicativo for iniciado. Quão difícil pode ser?

Bastante difícil, ao que parece.

O que há em um cache de pipeline?

Os dados do cache de pipeline são um blob (quase) opaco; você cria um objeto VkPipelineCache, possivelmente fornecendo a ele o blob inicial para começar, e então, em algum momento, pode recuperar o blob de dados desse objeto.

Embora não saibamos muito sobre o conteúdo do blob, a menos que leiamos o código-fonte do driver gráfico,1 os dados do cache de pipeline certamente começam com uma estrutura que identifica o dispositivo e se parece com isto:

struct VkPipelineCacheHeaderOne<br>
{
 uint32_t length; // == sizeof(VkPipelineCacheHeaderOne)
 uint32_t version; // == VK_PIPELINE_CACHE_HEADER_VERSION_ONE
 uint32_t vendorID;
 uint32_t deviceID;
 uint8_t uuid[VK_UUID_SIZE];
}; 

O cabeçalho é seguido por informações específicas do driver que normalmente contêm bits de microcódigo de shader (cujo formato depende da GPU) e dados auxiliares que podem conter estruturas arbitrárias definidas pelo driver. Alguns drivers tratam esse blob como um fluxo de arquivo estruturado e leem dados dele; outros armazenam estruturas brutas definidas no código-fonte do driver nesse blob e usam `memcpy` ou conversões de ponteiro para navegar pelos dados; nem é preciso dizer que uma atualização do driver pode invalidar a forma como os dados são armazenados.

Em teoria, o aplicativo precisa apenas usar `vkGetPipelineCacheData` para recuperar um blob de dados depois que o aplicativo atinge um estado estável (por exemplo, antes de o aplicativo ser encerrado…), salvar o blob em um arquivo e, em seguida, passar esse blob usando `VkPipelineCacheCreateInfo::pInitialData` ao criar o cache do pipeline na próxima execução. Se o conteúdo do blob não funcionar para a versão atual do driver — talvez o driver tenha sido atualizado ou talvez o usuário tenha mudado para uma GPU diferente —, o driver deve ignorar os dados iniciais e criar um cache de pipeline vazio.

Mas a teoria e a prática são um pouco diferentes. A regra geral na prática é que um driver só será capaz de lidar corretamente com o blob exato que o mesmo driver forneceu ao seu aplicativo anteriormente, e é aí que os problemas começam.2

O driver é o mesmo?

A especificação pressupõe que o cache não é compatível entre dispositivos diferentes (razão pela qual vendorID e deviceID estão presentes no cabeçalho) e depende do driver para estabelecer um UUID de pipeline (que é um GUID de 16 bytes) que identifique com precisão o conjunto completo de fatores que permitem interpretar um blob do cache de pipeline — você pode pensar nisso como um número de versão do formato do cache de pipeline. Durante uma atualização do driver, por exemplo, pode acontecer que o formato do cache de pipeline não seja atualizado; nesse caso, o UUID normalmente não deve mudar e o aplicativo não precisará recompilar os shaders do zero.

No entanto, drivers em uso tendem a apresentar dois tipos de problemas.

Alguns drivers (mais antigos) deixam de verificar o UUID corretamente. Como resultado, durante uma atualização de driver, o aplicativo pode tentar passar o blob com um UUID desatualizado para o driver; o driver tentará interpretar isso como dados recentes e, consequentemente, vkCreatePipelineCache pode travar. Observe que, em geral, vkCreatePipelineCache não oferece garantia de que aceita dados arbitrários e pode lidar com eles corretamente.

Alguns drivers, incluindo alguns bastante recentes, podem deixar de atualizar o UUID em uma atualização de driver que, na verdade, quebra a compatibilidade do binário do pipeline de shader. Isso pode acontecer durante uma atualização de versão do driver (embora seja raro) ou (algo que ocorre com frequência nos drivers atuais de pelo menos um grande fornecedor) entre binários de driver que são compilados a partir da mesma versão para diferentes ABIs. Se um driver de 32 bits e um driver de 64 bits fornecidos no mesmo sistema tiverem o mesmo UUID de pipeline, salvar o cache de uma versão de 32 bits do aplicativo e carregá-lo a partir de uma versão de 64 bits pode causar a falha do driver — o que é exatamente o que acontece quando você distribui uma versão de 32 bits do seu aplicativo e depois a atualiza para 64 bits seguindo as diretrizes do Google.

Os dados são os mesmos?

Agora que sabemos o que nos espera quando se trata de validação de cabeçalho, o próximo passo é validar os dados. Após chamar `vkGetPipelineCacheData`, o aplicativo salva o blob e carrega exatamente o mesmo blob na próxima execução.

Acontece que salvar dados em um arquivo é basicamente impossível de se fazer bem. Problemas no sistema de arquivos, bem como problemas de estabilidade do processo, podem, em alguns casos, levar a arquivos que são parcialmente gravados, têm trechos preenchidos com zeros no final (ou até mesmo com lixo), ou (como um caso especial) são criados, mas permanecem com tamanho zero. Em dispositivos móveis, isso pode ser complicado pelo fato de que o aplicativo provavelmente será encerrado abruptamente em um momento arbitrário pelo usuário ou pelo sistema operacional, algo que ocorre com menos frequência em desktops. No Android, também é comum usar aplicativos multiprocessos (multiatividades) e, se o código de cache do pipeline for executado em ambos os processos e compartilhar o mesmo arquivo de saída, esses desafios se tornam ainda mais difíceis de resolver.

A razão pela qual arquivos de tamanho zero são particularmente interessantes é que há pelo menos uma versão de driver com a qual nos deparamos em que passar um `pInitialData` e um `initialDataSize == 0` não nulos retorna um erro durante a criação do cache do pipeline. O que nos leva à advertência final.

O tratamento de erros é difícil

Embora a especificação diga que vkCreatePipelineCache deve basicamente sempre ser bem-sucedido, a menos que haja falta de memória, tais afirmações na especificação raramente são precisas. Ao criar o cache do pipeline, o driver deve ignorar os dados iniciais se eles forem incompatíveis. Isso pode ocorrer se o tamanho for zero, se o UUID armazenado não corresponder ao UUID esperado ou se a desserialização falhar por qualquer outro motivo. Alguns drivers, em vez disso, falham ao criar o cache do pipeline.

O usuário definitivamente não tem culpa aqui, portanto, abortar o aplicativo não seria educado. Embora geralmente seja possível prosseguir sem um cache de pipeline, isso costuma ser uma péssima ideia, pois significa que cada pipeline precisa ser recompilado do zero. Ou seja, os caches de pipeline têm utilidade mesmo que não sejam serializados em disco, pois permitem que o driver armazene em cache os resultados da compilação entre objetos de pipeline na memória.

Tudo isso naturalmente leva a…

Não é paranóia se eles realmente estiverem atrás de você

… a solução. Ao serializar os dados do cache do pipeline para o arquivo, usamos um cabeçalho preenchido com informações suficientes para validar os dados, com os dados do cache do pipeline seguindo imediatamente em seguida:

  struct PipelineCachePrefixHeader<br> 
{<br> 
  uint32_t magic; // an arbitrary magic header to make sure this is actually our file<br> 
  uint32_t dataSize; // equal to *pDataSize returned by vkGetPipelineCacheData<br> 
  uint64_t dataHash; // a hash of pipeline cache data, including the header<br> 
  uint32_t vendorID; // equal to VkPhysicalDeviceProperties::vendorID<br> 
  uint32_t deviceID; // equal to VkPhysicalDeviceProperties::deviceID<br> 
  uint32_t driverVersion; // equal to VkPhysicalDeviceProperties::driverVersion<br> 
  uint32_t driverABI; // equal to sizeof(void*)<br> 
  uint8_t uuid[VK_UUID_SIZE]; // equal to VkPhysicalDeviceProperties::pipelineCacheUUID<br> 
};

O hash dos dados do cache do pipeline nos permitirá validar a integridade dos dados. Para reduzir a chance de um erro de E/S realmente causar um problema de integridade, criamos um arquivo temporário e gravamos esse cabeçalho no arquivo, seguido pelos dados do cache do pipeline; em seguida, movemos o arquivo para o local de destino usando rename.3

Ao carregar o cache do pipeline, lemos o cabeçalho, lemos os dados, validamos os dados lidos usando dataSize e dataHash e, em seguida, verificamos se os dados podem ser passados com segurança para o driver comparando os campos restantes com as propriedades do dispositivo.4

Se os dados forem válidos, `vkCreatePipelineCache` é chamado com os dados iniciais corretos. Fundamentalmente, se essa chamada falhar, isso sugere que o driver implementa verificações adicionais que nossa lógica não detectou por conta própria. Portanto, em vez de prosseguir sem o cache do pipeline, criamos um cache do pipeline vazio nesse caso, chamando `vkCreatePipelineCache` novamente sem dados iniciais.

Também criamos o cache de pipeline vazio se o arquivo de cache de pipeline não for encontrado ou se nossa lógica de validação classificar os dados como inutilizáveis.

Observação: como incorporamos driverVersion no cabeçalho, qualquer atualização do driver fará com que o cache do pipeline seja reconstruído; incluímos essa verificação porque isso elimina completamente os problemas em que o UUID do cache do pipeline não é atualizado mesmo quando deveria — normalmente, driverVersion é atualizado como parte do processo de compilação, enquanto a atualização do UUID é mais manual. Para aplicativos voltados exclusivamente para desktop, isso pode ser muito agressivo — em geral, os drivers de desktop tendem a se comportar melhor no que diz respeito ao tratamento da validade do cache do pipeline, portanto, nem todas essas recomendações se aplicam.

Conclusão

Infelizmente, os drivers Vulkan nem sempre estão corretos e nem sempre seguem a especificação à risca. Os dados do cache do pipeline são uma parte especialmente frágil do renderizador Vulkan porque a E/S é difícil de acertar, e muitas vezes há verificações de integridade mínimas ou inexistentes no driver. No entanto, com validação suficiente no lado do aplicativo, você pode eliminar na prática os problemas de estabilidade decorrentes do tratamento do cache do pipeline — isso apenas exige trabalho.

  1. O que você pode fazer com certeza hoje em dia! Por exemplo, aqui está uma implementação de vkGetPipelineCacheData para o radv. ↩
  2. O restante deste artigo baseia-se na experiência de lançar continuamente o cliente Roblox no Android com suporte a Vulkan e sobreviver a várias atualizações do sistema operacional Android, atualizações de drivers e, em geral, lidar com drivers Vulkan antigos e atuais de todos os principais fornecedores. ↩
  3. Em teoria, a renomeação deveria ser atômica, mas, na prática, a semântica exata e as garantias variam de acordo com o sistema de arquivos; o hash é útil como forma de realizar uma comparação robusta. ↩
  4. Dependendo da aplicação, você pode querer usar nomes de arquivo diferentes com base, por exemplo, no vendorID ou no driverABI; isso é mais interessante em desktops e menos interessante em dispositivos móveis. ↩

Publicado originalmente em: https://zeux.io/2019/07/17/serializing-pipeline-cache/

Arseny Kapoulkine trabalha com tecnologia de jogos há uma década. Tendo atuado em renderização, simulação física, tempos de execução de linguagens, multithreading e muitas outras áreas, ele ainda descobre problemas empolgantes no desenvolvimento de jogos que exigem raciocínio de baixo nível. Depois de ajudar a lançar muitos títulos para PS3, incluindo vários jogos da série FIFA, ele ingressou na Roblox em 2012 e, desde então, vem trabalhando no motor interno da empresa, ajudando jovens desenvolvedores de jogos a realizar seus sonhos.

Nem a Roblox Corporation nem este blog endossam ou apoiam qualquer empresa ou serviço. Além disso, não são feitas garantias ou promessas quanto à precisão, confiabilidade ou integridade das informações contidas neste blog.

©2021 Roblox Corporation. Roblox, o logotipo da Roblox e Powering Imagination estão entre nossas marcas registradas e não registradas nos EUA e em outros países.