Como carrego recursos gráficos de forma assíncrona?

9

Vamos pensar como independentes de plataforma: quero carregar alguns recursos gráficos enquanto o resto do jogo está em execução.

Em princípio, posso carregar os arquivos reais em um thread separado ou usando E / S assíncrona. Mas com objetos gráficos, terei que enviá-los para a GPU, e isso (normalmente) só pode ser feito no thread principal.

Eu posso mudar o loop do meu jogo para algo parecido com isto:

while true do
    update()
    for each pending resource do
        load resource to gpu
    end
    draw()
end

enquanto um thread separado carrega recursos do disco para a RAM.

No entanto, se houver muitos recursos grandes para carregar, isso poderá me levar a perder o prazo de um quadro e, eventualmente, ser descartado. Então eu posso mudar o loop para isso:

while true do
    update()
    if there are pending resources then
        load one resource to gpu
        remove that resource from the pending list
    end
    draw()
end

Carregando efetivamente apenas um recurso por quadro. No entanto, se houver muitos recursos pequenos para carregar, carregar todos eles levará muitos quadros e haverá muito tempo perdido.

Idealmente, gostaria de cronometrar meu carregamento da seguinte maneira:

while true do
    time_start = get_time()
    update()
    while there are pending resources then
        current_time = get_time()
        if (current_time - time_start) + time_to_load(resource) >= 1/60 then
            break
        load one resource to gpu
        remove that resource from the pending list
    end
    draw()
end

Dessa forma, eu só carregaria um recurso se pudesse fazê-lo dentro do tempo que tenho para esse quadro. Infelizmente, isso requer uma maneira de estimar o tempo necessário para carregar um determinado recurso e, tanto quanto eu sei, geralmente não há maneiras de fazer isso.

O que estou perdendo aqui? Como muitos jogos carregam todas as suas coisas completamente assíncronas e sem quadros perdidos ou tempos de carregamento extremamente longos?

Panda Pajama
fonte

Respostas:

7

Vamos começar assumindo um mundo perfeito. Há duas etapas para carregar um recurso: primeiro, você o retira da mídia de armazenamento e entra na memória no formato correto; depois, transfere-o pelo barramento de memória para a memória de vídeo. Nenhuma dessas duas etapas realmente precisa usar o tempo em seu encadeamento principal - ele só precisa se envolver para emitir um comando de E / S. Tanto a CPU quanto a GPU podem continuar fazendo outras coisas enquanto o recurso está sendo copiado. O único recurso real sendo consumido é a largura de banda da memória.

Se você estiver usando uma plataforma sem muita camada de abstração entre você e o hardware, a API provavelmente expõe esses conceitos diretamente. Mas se você estiver em um PC, provavelmente há um driver entre você e a GPU, e ele quer fazer as coisas do seu jeito. Dependendo da API, você pode criar uma textura que é apoiada pela memória que você possui, mas é mais provável que chamar a API "create texture" copie a textura em alguma memória que o driver possua. Nesse caso, a criação de uma textura terá uma sobrecarga fixa e algum tempo proporcional ao tamanho da textura. Depois disso, o driver pode fazer qualquer coisa - pode transferir a textura proativamente para o VRAM, ou pode não se importar em carregar a textura até você tentar renderizá-la pela primeira vez.

Você pode ou não conseguir fazer algo sobre isso, mas pode estimar a quantidade de tempo que leva para fazer a chamada "criar textura". É claro que todos os números mudarão dependendo do hardware e do software, portanto, provavelmente não vale a pena gastar muito tempo fazendo a engenharia reversa deles. Então, tente e veja! Escolha uma métrica: "número de texturas por quadro" ou "tamanho total de texturas por quadro", escolha uma cota (digamos, 4 texturas por quadro) e comece a testá-lo.

Em casos patológicos, você pode precisar acompanhar as duas cotas ao mesmo tempo (por exemplo, limite de 4 texturas por quadro ou 2 MB de texturas por quadro, o que for menor). Mas o verdadeiro truque para a maioria das transmissões de texturas é descobrir quais texturas você deseja ajustar em sua memória limitada, e não quanto tempo leva para copiá-las.

Além disso, casos patológicos para criação de textura - como muitas texturas minúsculas sendo necessárias de uma só vez - tendem a ser casos patológicos para outras áreas também. Vale a pena obter uma implementação de trabalho simples antes de se preocupar exatamente com quantos microssegundos uma textura leva para copiar. (Além disso, o impacto real no desempenho pode não ser incorrido como tempo de CPU na chamada "criar textura", mas como tempo de GPU no primeiro quadro, você usa a textura.)

John Calsbeek
fonte
Essa é uma boa explicação. Muitas coisas que eu não sabia, mas que faz muito sentido. Em vez de testá-lo com estresse, eu media a sobrecarga da criação de textura em tempo de execução, começava suavemente e acelerava, dizendo, 80% do tempo de execução disponível para deixar espaço para discrepâncias.
Panda Pyjama
@PandaPajama Sou um pouco cético quanto a isso. Eu esperaria que o estado estacionário fosse "sem texturas sendo copiadas" e uma enorme quantidade de variação. E, como eu disse, suspeito que parte do acerto é o primeiro quadro de renderização que usa a textura, que é muito mais difícil de medir dinamicamente sem afetar o desempenho.
John Calsbeek
Além disso, aqui está uma apresentação da NVIDIA sobre transferências de textura assíncronas. A principal coisa que ele está dirigindo para casa, pelo que eu estou lendo, é que o uso de uma textura logo após o upload será interrompido. developer.download.nvidia.com/GTC/PDF/GTC2012/PresentationPDF/…
John Calsbeek
Eu não sou motorista de desenvolvimento, mas isso é comum? Não faz muito sentido implementar drivers dessa maneira, porque é muito provável que os primeiros usos da textura ocorram picos (como no início de cada nível) em vez de espaçados ao longo da linha do tempo.
Panda Pyjama
@PandaPajama Também é comum que os aplicativos criem mais texturas do que a VRAM disponível, e criem texturas e nunca as usem. Um caso comum é "criar um monte de texturas e depois desenhar imediatamente uma cena que as use"; nesse caso, a preguiça ajuda o motorista, porque pode descobrir quais texturas são realmente usadas e que o primeiro quadro vai atrapalhar de qualquer maneira . Mas também não sou motorista de desenvolvimento, leve-o com um pouco de sal (e teste-o!).
John Calsbeek